Compare commits
289 Commits
Author | SHA1 | Date | |
---|---|---|---|
dafd958f69 | |||
f51af6c61c | |||
254db22063 | |||
8be4256278 | |||
8e8669d63f | |||
4625ff92f1 | |||
6aa84326af | |||
9a384d81fe | |||
0cbe36c048 | |||
7f16aa8c7e | |||
872f8a6229 | |||
9b261daa6d | |||
c46c15c258 | |||
a8ba1ed1ed | |||
ff4056d4f3 | |||
ae152c3ffa | |||
e2ff33d7db | |||
ce94c05fd3 | |||
9cde4dc7e2 | |||
ca571cd756 | |||
e5eb0c79c0 | |||
43bd6587d3 | |||
3bb059ab74 | |||
4c963d6edf | |||
396bc7f7b4 | |||
2896a9b26f | |||
9267a45449 | |||
c430d470c4 | |||
3921a3ca22 | |||
1ff0a98d30 | |||
f0efd52cb7 | |||
bb8fa88688 | |||
4b976c13c1 | |||
f68d4efcdd | |||
fea247b218 | |||
f419c56a3c | |||
a5fca7a1c4 | |||
e18d0b5d51 | |||
9952cdca7f | |||
6278145374 | |||
84018a5caa | |||
d7785fe2d2 | |||
e1751c4d91 | |||
913da79ff4 | |||
a4fbb2de7e | |||
b5601ed5e6 | |||
42c4f15f22 | |||
6fbd9b2628 | |||
d04bfb58a2 | |||
cded2548f5 | |||
dcc859a86a | |||
9bec38559f | |||
2856454d41 | |||
b3c4fc4003 | |||
c2bbc04c4c | |||
db40c7bc32 | |||
60707fdbd1 | |||
f05614f4da | |||
a10c382bd4 | |||
da2fb876cb | |||
3c58bff803 | |||
a28814bc0e | |||
3cff8261ae | |||
b16e8f7b76 | |||
57ab001c0c | |||
3d85dace38 | |||
7a04c2974f | |||
d459839bf7 | |||
657cfe1b23 | |||
f4eaa0f01f | |||
1e2ffcadf0 | |||
dcc05af02e | |||
4b7f78f38b | |||
f94ff4cc74 | |||
b750663a1f | |||
4c4b76e995 | |||
da19d2c1a7 | |||
fb15c5b354 | |||
6ffe1cfcab | |||
87678c58ac | |||
feab4cc48a | |||
712946f512 | |||
a7bfceae05 | |||
8a26cd549a | |||
1cf3ce0617 | |||
73c65fada2 | |||
92ea923c03 | |||
e7db453717 | |||
10ee09f052 | |||
be7d91a138 | |||
3278c80d3f | |||
65e1edb0b8 | |||
e05c88370f | |||
15c29f8419 | |||
fc722731d3 | |||
1c9c564e90 | |||
872b60f8ea | |||
0d3364b3da | |||
fed53661b3 | |||
e86b4d89ca | |||
c5cb32f6dd | |||
deb56e16ec | |||
b5626ef01c | |||
e39d9067f2 | |||
43d34d5d35 | |||
7341be76bb | |||
0abd62dfe8 | |||
735012e3d7 | |||
b5efb8d2e6 | |||
1dbeabb716 | |||
671f9e56e2 | |||
dc6c189948 | |||
4501824f3f | |||
4568d2a98e | |||
f5d81334f8 | |||
f3ed90399b | |||
fada01cec9 | |||
1b4b9fb4cc | |||
6eeef8a866 | |||
24979a0af2 | |||
be1a44f018 | |||
9fcc2903fc | |||
06df63b283 | |||
0f1efc16f5 | |||
da8a06952c | |||
38d810cef7 | |||
393a3a2b8f | |||
aaddc580d1 | |||
957d478865 | |||
023913a852 | |||
6c51d83f61 | |||
0edaedb6ab | |||
13f21aa0d6 | |||
929a0c37bd | |||
058ccf56d0 | |||
162ac572da | |||
c7f3fdb46d | |||
29af07b3f9 | |||
758436a428 | |||
e0cadb4f62 | |||
013dfa1b61 | |||
e0f1c50534 | |||
d50dc2e68e | |||
8b5b18c97e | |||
f7383b4cc8 | |||
1bc32285ba | |||
f12114f9aa | |||
2b6faa8d20 | |||
45b7df6ac9 | |||
5a43ce2719 | |||
fe31dc8606 | |||
f0615482d9 | |||
03c47e6f7d | |||
4111b8a5a3 | |||
5b5a2e8c25 | |||
9ec0c23c52 | |||
9a5034c13c | |||
b1fcf4524a | |||
87d384dba5 | |||
4f5a8f7953 | |||
8728356698 | |||
9c30476fc8 | |||
09beb57eaf | |||
76a36d1829 | |||
0d4036efa2 | |||
cb4562aad5 | |||
0084d4766b | |||
74ddcfa01e | |||
ed36fba0d7 | |||
af015d435b | |||
ec59980e6f | |||
f0f4247c5d | |||
b562094956 | |||
4afd55c441 | |||
d7404f418d | |||
893410911c | |||
556b581b6a | |||
1685ccaca8 | |||
522abcfdfd | |||
ed1827ff28 | |||
214b2d1c1c | |||
322518e9dc | |||
ea2dd536b4 | |||
6a1eca760a | |||
29513d4ded | |||
57daf27fdd | |||
e698d90e3c | |||
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 | |||
8cafa8a483 | |||
724af44e41 | |||
bac9ef4f93 | |||
ada6f3b844 | |||
c8a26ce952 | |||
6cf80b7533 | |||
79df523bb2 | |||
e921f9757a | |||
bed9737d64 | |||
2583eb15ec | |||
1879ea55e8 |
.circleci
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csChangellyTests.csChargeTester.csLightningDTester.cs
Lnd
README.mdRateRulesTest.csServerTester.csTestAccount.csUnitTest1.csUnitTestPeusa.csdocker-compose.ymlxunit.runner.jsonBTCPayServer
BTCPayNetworkProvider.Dash.csBTCPayNetworkProvider.Groestlcoin.csBTCPayNetworkProvider.csBTCPayServer.csprojPaymentMethodExtensions.csProgram.csWalletId.csZoneLimits.cs
DockerfileREADME.mdglobal.jsonConfiguration
Controllers
AccountController.csAppsController.PointOfSale.csAppsController.csAppsPublicController.csChangellyController.csHomeController.csInvoiceController.API.csInvoiceController.PaymentProtocol.csInvoiceController.UI.csInvoiceController.csPaymentRequestActionResult.csPublicController.csRateController.csServerController.csStoresController.BTCLike.csStoresController.Changelly.csStoresController.LightningLike.csStoresController.csUserStoresController.csWalletsController.cs
CorsPolicies.csData
APIKeyData.csApplicationDbContext.csApplicationDbContextFactory.csHistoricalAddressInvoiceData.csInvoiceData.csInvoiceEventData.csPairedSINData.csPendingInvoiceData.csStoreData.cs
DerivationStrategy.csExtensions.csFilters
HostedServices
CheckConfigurationHostedService.csInvoiceNotificationManager.csMigratorHostedService.csNBXplorerWaiter.csRatesHostedService.cs
Hosting
JsonConverters
Logging
Migrations
20180719095626_CanDeleteStores.Designer.cs20180719095626_CanDeleteStores.csApplicationDbContextModelSnapshot.cs
ModelBinders
Models
AppViewModels
InvoicingModels
ServerViewModels
LndGrpcServicesViewModel.csLndRestServicesViewModel.csMaintenanceViewModel.csSSHServiceViewModel.csServicesViewModel.cs
StoreViewModels
CheckoutExperienceViewModel.csDerivationSchemeViewModel.csLightningNodeViewModel.csPayButtonViewModel.csRatesViewModel.csStoreViewModel.csStoresViewModel.csUpdateChangellySettingsViewModel.cs
WalletViewModels
Payments
Bitcoin
Changelly
Changelly.csChangellyCalculationHelper.csChangellyClientProvider.csChangellyException.csChangellySettings.cs
IPaymentFilter.csIPaymentMethodHandler.csModels
Lightning
CLightning
Charge
ILightningInvoiceClient.csLightMoney.csLightningClientFactory.csLightningConnectionString.csLightningLikePaymentData.csLightningLikePaymentHandler.csLightningListener.csLightningSupportedPaymentMethod.csLnd
Properties
Rating
SSH
Security
Services
Fees
HardwareWalletService.csInvoices
LanguageService.csLightningConfigurationProvider.csMigrationSettings.csRates
BTCPayRateProviderFactory.csBackgroundFetcherRateProvider.csBitpayRateProvider.csCachedRateProvider.csCoinAverageRateProvider.csCurrencyNameTable.csExchangeSharpRateProvider.csFallbackRateProvider.csIHasExchangeName.csKrakenExchangeRateProvider.csNullRateProvider.csQuadrigacxRateProvider.csRateFetcher.csRateProviderFactory.cs
SettingsRepository.csStores
Wallets
Views
Account
Apps
AppsPublic
Home
Invoice
Manage
Public
Server
ListUsers.cshtmlLndGrpcServices.cshtmlLndRestServices.cshtmlMaintenance.cshtmlSSHService.cshtmlServerNavPages.csServices.cshtml_Nav.cshtml
Shared
Stores
AddDerivationScheme.cshtmlAddLightningNode.cshtmlCheckoutExperience.cshtmlCreateToken.cshtmlListTokens.cshtmlPayButton.cshtmlPayButtonEnable.cshtmlRates.cshtmlStoreNavPages.csStoreUsers.cshtmlUpdateChangellySettings.cshtmlUpdateStore.cshtml_Nav.cshtml_ViewImports.cshtml
UserStores
Wallets
wwwroot
checkout
css
js
img/paybutton
imlegacy
js
paybutton
vendor/vuejs-vee-validate
29
.circleci/config.yml
Normal file
29
.circleci/config.yml
Normal file
@ -0,0 +1,29 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
machine:
|
||||
docker_layer_caching: true
|
||||
steps:
|
||||
- checkout
|
||||
test:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
lsb_release -a
|
||||
wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
sudo apt-get install apt-transport-https
|
||||
sudo apt-get update
|
||||
sudo apt-get install dotnet-sdk-2.1
|
||||
dotnet build /p:TreatWarningsAsErrors=true
|
||||
cd BTCPayServer.Tests
|
||||
dotnet test --filter Fast=Fast
|
||||
docker-compose up -d dev
|
||||
dotnet test --filter Integration=Integration
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- test
|
@ -9,13 +9,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -25,6 +24,12 @@
|
||||
<None Update="docker-compose.yml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
@ -35,6 +36,12 @@ using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public enum TestDatabases
|
||||
{
|
||||
Postgres,
|
||||
MySQL,
|
||||
}
|
||||
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
private string _Directory;
|
||||
@ -57,6 +64,11 @@ namespace BTCPayServer.Tests
|
||||
set;
|
||||
}
|
||||
|
||||
public string MySQL
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
@ -68,6 +80,10 @@ namespace BTCPayServer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
public TestDatabases TestDatabase
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool MockRates { get; set; } = true;
|
||||
|
||||
@ -94,7 +110,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (Postgres != null)
|
||||
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
|
||||
config.AppendLine($"mysql=" + MySQL);
|
||||
else if (!String.IsNullOrEmpty(Postgres))
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
var confPath = Path.Combine(chainDirectory, "settings.config");
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
@ -120,30 +138,67 @@ 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();
|
||||
if (MockRates)
|
||||
{
|
||||
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
|
||||
rateProvider.Providers.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_USD"),
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
|
||||
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.Providers.Add("coinaverage", coinAverageMock);
|
||||
|
||||
var bitflyerMock = new MockRateProvider();
|
||||
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bitflyer",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
|
||||
BidAsk = new BidAsk(700000m)
|
||||
});
|
||||
rateProvider.Providers.Add("bitflyer", bitflyerMock);
|
||||
|
||||
var quadrigacx = new MockRateProvider();
|
||||
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "quadrigacx",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(6000m)
|
||||
});
|
||||
rateProvider.Providers.Add("quadrigacx", quadrigacx);
|
||||
|
||||
var bittrex = new MockRateProvider();
|
||||
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bittrex",
|
||||
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
|
||||
BidAsk = new BidAsk(0.004m)
|
||||
});
|
||||
rateProvider.Providers.Add("bittrex", bittrex);
|
||||
}
|
||||
}
|
||||
|
||||
public string HostName
|
||||
@ -152,6 +207,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; }
|
||||
|
||||
@ -160,17 +216,21 @@ namespace BTCPayServer.Tests
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
context.Request.Host = new HostString("127.0.0.1", Port);
|
||||
context.Request.Scheme = "http";
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
|
||||
if (additionalClaims != null)
|
||||
claims.AddRange(additionalClaims);
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
|
||||
}
|
||||
if(storeId != null)
|
||||
if (storeId != null)
|
||||
{
|
||||
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||
}
|
||||
|
253
BTCPayServer.Tests/ChangellyTests.cs
Normal file
253
BTCPayServer.Tests/ChangellyTests.cs
Normal file
@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChangellyTests
|
||||
{
|
||||
public ChangellyTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
var storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.Null(storeBlob.ChangellySettings);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
|
||||
updateModel.ApiSecret);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
|
||||
updateModel.ChangellyMerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanToggleChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var changellyController =
|
||||
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
|
||||
changellyController.IsTest = true;
|
||||
|
||||
//test non existing payment method
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
var updateModel = CreateDefaultChangellyParams(false);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
//set payment method but disabled
|
||||
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
updateModel.Enabled = true;
|
||||
//test with enabled method
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsNotType<BitpayErrorModel>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateChangellySettingsViewModel CreateDefaultChangellyParams(bool enabled)
|
||||
{
|
||||
return new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiKey = "6ed02cdf1b614d89a8c0ceb170eebb61",
|
||||
ApiSecret = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483",
|
||||
ChangellyMerchantId = "804298eb5753",
|
||||
Enabled = enabled
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanGetCurrencyListFromChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
//save changelly settings
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
//confirm saved
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = new MockHttpClientFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
var result = Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value as IEnumerable<CurrencyFull>;
|
||||
Assert.True(result.Any());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanCalculateToAmountForChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = new MockHttpClientFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeBaseAmount()
|
||||
{
|
||||
Assert.Equal(1, ChangellyCalculationHelper.ComputeBaseAmount(1, 1));
|
||||
Assert.Equal(0.5m, ChangellyCalculationHelper.ComputeBaseAmount(1, 0.5m));
|
||||
Assert.Equal(2, ChangellyCalculationHelper.ComputeBaseAmount(0.5m, 1));
|
||||
Assert.Equal(4m, ChangellyCalculationHelper.ComputeBaseAmount(1, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeCorrectAmount()
|
||||
{
|
||||
Assert.Equal(1, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 2));
|
||||
Assert.Equal(0.25m, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 0.5m));
|
||||
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
public class MockHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
return new HttpClient();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
|
||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
}
|
||||
|
||||
public CLightningRPCClient RPC { get; }
|
||||
public CLightningClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using BTCPayServer.Lightning.LND;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
@ -16,12 +16,12 @@ namespace BTCPayServer.Tests.Lnd
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndInvoiceClient(Swagger);
|
||||
Client = new LndClient(Swagger, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndInvoiceClient Client { get; set; }
|
||||
public LndClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
||||
|
@ -1,226 +0,0 @@
|
||||
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;
|
||||
|
||||
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();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,8 @@ You can run the tests inside a container by running
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
|
||||
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
|
||||
|
||||
## How to manually test payments
|
||||
|
||||
### Using the test bitcoin-cli
|
||||
|
@ -11,6 +11,21 @@ namespace BTCPayServer.Tests
|
||||
public class RateRulesTest
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void SecondDuplicatedRuleIsIgnored()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = 1.1");
|
||||
builder.AppendLine("DOGE_X = 1.2");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
|
||||
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
|
||||
rule.Reevaluate();
|
||||
Assert.True(!rule.HasError);
|
||||
Assert.Equal(1.1m, rule.BidAsk.Ask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
// Check happy path
|
||||
@ -46,8 +61,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
rules.GlobalMultiplier = 2.32m;
|
||||
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).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
|
||||
@ -104,7 +119,7 @@ namespace BTCPayServer.Tests
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m);
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
@ -113,22 +128,22 @@ namespace BTCPayServer.Tests
|
||||
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||
builder.AppendLine("DOGE_BTC = 2000");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rules.GlobalMultiplier = 1.1m;
|
||||
rules.Spread = 0.1m;
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
|
||||
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)) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
|
||||
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))) * 1.1", rule2.ToString());
|
||||
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))) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
|
||||
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
|
||||
@ -147,12 +162,12 @@ namespace BTCPayServer.Tests
|
||||
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.Value.Value);
|
||||
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.Value.Value);
|
||||
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
|
||||
|
||||
// Make sure the inverse has more priority than X_X or CDNT_X
|
||||
builder = new StringBuilder();
|
||||
|
@ -18,10 +18,10 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -49,10 +49,10 @@ 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 = (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);
|
||||
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=https://127.0.0.1:54938/;api-token=foiewnccewuify", "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);
|
||||
|
||||
@ -60,7 +60,9 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
LTCNBXplorerUri = LTCExplorerClient.Address,
|
||||
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver"),
|
||||
IntegratedLightning = MerchantCharge.Client.Uri
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
@ -78,88 +80,24 @@ namespace BTCPayServer.Tests
|
||||
PayTester.Start();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
||||
{
|
||||
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>
|
||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
||||
public Task EnsureChannelsSetup()
|
||||
{
|
||||
bool awaitingLocking = false;
|
||||
while (true)
|
||||
{
|
||||
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 => !SKIPPED_STATES.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
|
||||
switch (channel?.State)
|
||||
{
|
||||
case null:
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
await Task.Delay(1000);
|
||||
|
||||
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(awaitingLocking ? 1 : 10);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
awaitingLocking = true;
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
default:
|
||||
throw new NotSupportedException(channel?.State ?? "");
|
||||
}
|
||||
}
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
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);
|
||||
}
|
||||
yield return CustomerLightningD;
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
|
||||
private IEnumerable<ILightningClient> GetLightningDestClients()
|
||||
{
|
||||
var merchantInfo = await client.GetInfo();
|
||||
if (merchantInfo.BlockHeight == blockCount)
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
return null;
|
||||
yield return MerchantLightningD;
|
||||
yield return MerchantLnd.Client;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
@ -171,12 +109,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerLightningD.SendAsync(bolt11);
|
||||
await CustomerLightningD.Pay(bolt11);
|
||||
}
|
||||
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
public ILightningClient CustomerLightningD { get; set; }
|
||||
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ILightningClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
@ -214,9 +152,14 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ using Xunit;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -65,15 +68,16 @@ namespace BTCPayServer.Tests
|
||||
var store = this.GetController<UserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
parent.Stores.Add(StoreId);
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public void RegisterDerivationScheme(string crytoCode)
|
||||
public WalletId RegisterDerivationScheme(string crytoCode)
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
return RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
@ -88,6 +92,8 @@ namespace BTCPayServer.Tests
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
@ -131,7 +137,7 @@ namespace BTCPayServer.Tests
|
||||
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;
|
||||
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
|
@ -35,10 +35,15 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Validation;
|
||||
using ExchangeSharp;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -51,6 +56,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanHandleUriValidation()
|
||||
{
|
||||
var attribute = new UriAttribute();
|
||||
@ -72,6 +78,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue2()
|
||||
{
|
||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
|
||||
@ -129,6 +136,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
@ -253,6 +261,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanAcceptInvoiceWithTolerance()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
@ -280,6 +289,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanAcceptInvoiceWithTolerance2()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -320,6 +330,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
foreach (var test in new[]
|
||||
@ -330,12 +341,13 @@ namespace BTCPayServer.Tests
|
||||
(0.1m, "$0.10 (USD)"),
|
||||
})
|
||||
{
|
||||
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
|
||||
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD");
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanPayUsingBIP70()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -386,22 +398,13 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUseLightMoney()
|
||||
{
|
||||
var light = LightMoney.MilliSatoshis(1);
|
||||
Assert.Equal("0.00000000001", light.ToString());
|
||||
|
||||
light = LightMoney.MilliSatoshis(200000);
|
||||
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSetLightningServer()
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetLightningServer()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var storeController = user.GetController<StoresController>();
|
||||
@ -410,7 +413,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri,
|
||||
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
|
||||
}, "test", "BTC").GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(storeController.ModelState.IsValid);
|
||||
@ -432,136 +436,27 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseLightningURL()
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentCLightning()
|
||||
{
|
||||
LightningConnectionString conn = null;
|
||||
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal(i == 0, conn.IsLegacy);
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("tcp://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=tcp://test/a", conn.ToString());
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://aaa:bbb@test/a", conn.ToString());
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
|
||||
Assert.Equal("aaa", conn.Username);
|
||||
Assert.Equal("bbb", conn.Password);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
|
||||
}
|
||||
|
||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
|
||||
|
||||
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
|
||||
|
||||
var certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
|
||||
|
||||
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
|
||||
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
|
||||
|
||||
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
|
||||
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
|
||||
|
||||
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
|
||||
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
|
||||
Assert.Equal(conn2.ToString(), conn.ToString());
|
||||
Assert.Equal(lndUri, conn.ToString());
|
||||
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
|
||||
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
|
||||
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
|
||||
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
|
||||
|
||||
// AllowInsecure can be set to allow http
|
||||
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentCLightning()
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentCharge()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentCharge()
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendLightningPaymentLnd()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
await ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentLnd()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||
}
|
||||
|
||||
void ProcessLightningPayment(LightningConnectionType type)
|
||||
async Task ProcessLightningPayment(LightningConnectionType type)
|
||||
{
|
||||
// For easier debugging and testing
|
||||
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
|
||||
@ -569,16 +464,15 @@ namespace BTCPayServer.Tests
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", type);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
tester.PrepareLightning(type);
|
||||
|
||||
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
|
||||
await CanSendLightningPaymentCore(tester, user);
|
||||
|
||||
Task.WaitAll(Enumerable.Range(0, 5)
|
||||
await Task.WhenAll(Enumerable.Range(0, 5)
|
||||
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
||||
.ToArray());
|
||||
}
|
||||
@ -608,6 +502,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanUseServerInitiatedPairingCode()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -633,6 +528,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanSendIPN()
|
||||
{
|
||||
using (var callbackServer = new CustomServer())
|
||||
@ -669,6 +565,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CantPairTwiceWithSamePubkey()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -690,6 +587,74 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanRescanWallet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
var btcDerivationScheme = acc.DerivationScheme;
|
||||
acc.RegisterDerivationScheme("LTC");
|
||||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(acc.UserId);
|
||||
WalletId walletId = new WalletId(acc.StoreId, "LTC");
|
||||
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
Assert.False(rescan.Ok);
|
||||
Assert.True(rescan.IsFullySync);
|
||||
Assert.False(rescan.IsSupportedByCurrency);
|
||||
Assert.False(rescan.IsServerAdmin);
|
||||
|
||||
walletId = new WalletId(acc.StoreId, "BTC");
|
||||
var serverAdminClaim = new[] { new Claim(Policies.CanModifyServerSettings.Key, "true") };
|
||||
walletController = tester.PayTester.GetController<WalletsController>(acc.UserId, additionalClaims: serverAdminClaim);
|
||||
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
Assert.True(rescan.Ok);
|
||||
Assert.True(rescan.IsFullySync);
|
||||
Assert.True(rescan.IsSupportedByCurrency);
|
||||
Assert.True(rescan.IsServerAdmin);
|
||||
|
||||
rescan.GapLimit = 100;
|
||||
|
||||
// Sending a coin
|
||||
var txId = tester.ExplorerNode.SendToAddress(btcDerivationScheme.Derive(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
||||
tester.ExplorerNode.Generate(1);
|
||||
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
Assert.Empty(transactions.Transactions);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
||||
|
||||
while(true)
|
||||
{
|
||||
rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
if(rescan.Progress == null && rescan.LastSuccess != null)
|
||||
{
|
||||
if (rescan.LastSuccess.Found == 0)
|
||||
continue;
|
||||
// Scan over
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Null(rescan.TimeOfScan);
|
||||
Assert.NotNull(rescan.RemainingTime);
|
||||
Assert.NotNull(rescan.Progress);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
Assert.Null(rescan.PreviousError);
|
||||
Assert.NotNull(rescan.TimeOfScan);
|
||||
Assert.Equal(1, rescan.LastSuccess.Found);
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
var tx = Assert.Single(transactions.Transactions);
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanListInvoices()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -730,6 +695,48 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanGetRates()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
acc.RegisterDerivationScheme("LTC");
|
||||
|
||||
var rateController = acc.GetController<RateController>();
|
||||
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
|
||||
Assert.NotNull(GetBaseCurrencyRatesResult);
|
||||
Assert.NotNull(GetBaseCurrencyRatesResult.Data);
|
||||
Assert.Equal(2, GetBaseCurrencyRatesResult.Data.Length);
|
||||
Assert.Single(GetBaseCurrencyRatesResult.Data.Where(o => o.Code == "LTC"));
|
||||
|
||||
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
|
||||
Assert.NotNull(GetRatesResult);
|
||||
Assert.NotNull(GetRatesResult.Data);
|
||||
Assert.Equal(2, GetRatesResult.Data.Length);
|
||||
|
||||
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate>>();
|
||||
|
||||
Assert.NotNull(GetCurrencyPairRateResult);
|
||||
Assert.NotNull(GetCurrencyPairRateResult.Data);
|
||||
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
|
||||
|
||||
// Should be OK because the request is signed, so we can know the store
|
||||
var rates = acc.BitPay.GetRates();
|
||||
HttpClient client = new HttpClient();
|
||||
// Unauthentified requests should also be ok
|
||||
var response = client.GetAsync($"http://127.0.0.1:{tester.PayTester.Port}/api/rates?storeId={acc.StoreId}").GetAwaiter().GetResult();
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
|
||||
{
|
||||
var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>().ListInvoices(filter).Result).Model;
|
||||
@ -737,6 +744,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanRBFPayment()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -793,6 +801,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
var filter = "storeid:abc status:abed blabhbalh ";
|
||||
@ -814,6 +823,24 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseFingerprint()
|
||||
{
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("4e343c6fc6cfbf9339c02d06a151e1dd", out var unused));
|
||||
Assert.Equal("4e:34:3c:6f:c6:cf:bf:93:39:c0:2d:06:a1:51:e1:dd", unused.ToString());
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("4e:34:3c:6f:c6:cf:bf:93:39:c0:2d:06:a1:51:e1:dd", out unused));
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", out unused));
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out unused));
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out unused));
|
||||
Assert.Equal("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", unused.ToString());
|
||||
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out var f1));
|
||||
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", out var f2));
|
||||
Assert.Equal(f1.ToString(), f2.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void TestAccessBitpayAPI()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -880,6 +907,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanUseExchangeSpecificRate()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -891,8 +919,8 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
List<decimal> rates = new List<decimal>();
|
||||
rates.Add(CreateInvoice(tester, user, "coinaverage"));
|
||||
var bitflyer = CreateInvoice(tester, user, "bitflyer");
|
||||
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
|
||||
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
|
||||
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
|
||||
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
|
||||
rates.Add(bitflyer);
|
||||
|
||||
@ -903,7 +931,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
|
||||
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
|
||||
{
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
@ -912,7 +940,7 @@ namespace BTCPayServer.Tests
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
Currency = currency,
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
@ -922,16 +950,15 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanTweakRate()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.PayTester.MockRates = false;
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
@ -942,12 +969,12 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
|
||||
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
Assert.Equal(1.0, vm.RateMultiplier);
|
||||
vm.RateMultiplier = 0.5;
|
||||
Assert.Equal(0.0, vm.Spread);
|
||||
vm.Spread = 40;
|
||||
storeController.Rates(vm).Wait();
|
||||
|
||||
|
||||
@ -961,11 +988,14 @@ namespace BTCPayServer.Tests
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
|
||||
var expectedRate = 5000.0m * 0.6m;
|
||||
var expectedCoins = invoice2.Price / expectedRate;
|
||||
Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanHaveLTCOnlyStore()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -1030,6 +1060,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanModifyRates()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -1043,7 +1074,7 @@ namespace BTCPayServer.Tests
|
||||
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
Assert.False(rateVm.ShowScripting);
|
||||
Assert.Equal("coinaverage", rateVm.PreferredExchange);
|
||||
Assert.Equal(1.0, rateVm.RateMultiplier);
|
||||
Assert.Equal(0.0, rateVm.Spread);
|
||||
Assert.Null(rateVm.TestRateRules);
|
||||
|
||||
rateVm.PreferredExchange = "bitflyer";
|
||||
@ -1052,13 +1083,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("bitflyer", rateVm.PreferredExchange);
|
||||
|
||||
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
|
||||
rateVm.RateMultiplier = 1.1;
|
||||
rateVm.Spread = 10;
|
||||
store = user.GetController<StoresController>();
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||
Assert.NotNull(rateVm.TestRateRules);
|
||||
Assert.Equal(2, rateVm.TestRateRules.Count);
|
||||
Assert.False(rateVm.TestRateRules[0].Error);
|
||||
Assert.StartsWith("(bitflyer(BTC_JPY)) * 1.10 =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(rateVm.TestRateRules[1].Error);
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
|
||||
@ -1071,25 +1102,26 @@ namespace BTCPayServer.Tests
|
||||
rateVm.ScriptTest = "BTC_JPY";
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
Assert.Contains("(bitflyer(BTC_JPY)) * 1.10 = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
|
||||
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
|
||||
"X_CAD = quadrigacx(X_CAD);\n" +
|
||||
"X_X = gdax(X_X);";
|
||||
rateVm.RateMultiplier = 0.5;
|
||||
"X_X = coinaverage(X_X);";
|
||||
rateVm.Spread = 50;
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
|
||||
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
store = user.GetController<StoresController>();
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
Assert.Equal(0.5, rateVm.RateMultiplier);
|
||||
Assert.Equal(50, rateVm.Spread);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanPayWithTwoCurrencies()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -1201,6 +1233,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseCurrencyValue()
|
||||
{
|
||||
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
|
||||
@ -1221,6 +1254,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseDerivationScheme()
|
||||
{
|
||||
var parser = new DerivationSchemeParser(Network.TestNet);
|
||||
@ -1261,7 +1295,8 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSetPaymentMethodLimits()
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanDisablePaymentMethods()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
@ -1269,6 +1304,66 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.5m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Equal(3, invoice.CryptoInfo.Length);
|
||||
|
||||
var controller = user.GetController<StoresController>();
|
||||
var lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
|
||||
Assert.True(lightningVM.Enabled);
|
||||
lightningVM.Enabled = false;
|
||||
controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult();
|
||||
lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
|
||||
Assert.False(lightningVM.Enabled);
|
||||
|
||||
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
Assert.True(derivationVM.Enabled);
|
||||
derivationVM.Enabled = false;
|
||||
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
// Confirmation
|
||||
controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult();
|
||||
Assert.False(derivationVM.Enabled);
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
Assert.False(derivationVM.Enabled);
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.5m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetPaymentMethodLimits()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
|
||||
vm.LightningMaxValue = "2 USD";
|
||||
@ -1304,6 +1399,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanUsePoSApp()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -1330,14 +1426,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
|
||||
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
|
||||
Assert.Equal("hello", vmpos.Title);
|
||||
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.ViewPointOfSale(appId).Result).Model);
|
||||
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
|
||||
Assert.Equal("hello", vmview.Title);
|
||||
Assert.Equal(2, vmview.Items.Length);
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
Assert.IsType<RedirectResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
var invoice = user.BitPay.GetInvoices().First();
|
||||
Assert.Equal(10.00m, invoice.Price);
|
||||
Assert.Equal("CAD", invoice.Currency);
|
||||
@ -1346,6 +1444,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateAndDeleteApps()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -1369,6 +1468,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
|
||||
@ -1381,6 +1481,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -1537,6 +1638,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CheckQuadrigacxRateProvider()
|
||||
{
|
||||
var quadri = new QuadrigacxRateProvider();
|
||||
@ -1550,17 +1652,20 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanQueryDirectProviders()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
var factory = CreateBTCPayRateFactory();
|
||||
|
||||
foreach (var result in factory
|
||||
.DirectProviders
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider)
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
|
||||
.ToList())
|
||||
{
|
||||
result.Fetcher.InvalidateCache();
|
||||
var exchangeRates = result.ResultAsync.Result;
|
||||
result.Fetcher.InvalidateCache();
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
@ -1573,68 +1678,120 @@ namespace BTCPayServer.Tests
|
||||
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
|
||||
);
|
||||
}
|
||||
// Kraken emit one request only after first GetRates
|
||||
factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
|
||||
var factory = CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var pairs =
|
||||
provider.GetAll()
|
||||
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
|
||||
.ToHashSet();
|
||||
|
||||
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||
var result = factory.FetchRates(pairs, rules);
|
||||
var result = fetcher.FetchRates(pairs, rules);
|
||||
foreach (var value in result)
|
||||
{
|
||||
var rateResult = value.Value.GetAwaiter().GetResult();
|
||||
Assert.NotNull(rateResult.Value);
|
||||
Assert.NotNull(rateResult.BidAsk);
|
||||
}
|
||||
}
|
||||
|
||||
private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider)
|
||||
public static RateProviderFactory CreateBTCPayRateFactory()
|
||||
{
|
||||
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
|
||||
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
|
||||
}
|
||||
|
||||
private static MemoryCacheOptions CreateMemoryCache()
|
||||
{
|
||||
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
|
||||
}
|
||||
|
||||
class SpyRateProvider : IRateProvider
|
||||
{
|
||||
public bool Hit { get; set; }
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
Hit = true;
|
||||
var rates = new ExchangeRates();
|
||||
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
|
||||
return Task.FromResult(rates);
|
||||
}
|
||||
|
||||
public void AssertHit()
|
||||
{
|
||||
Assert.True(Hit, "Should have hit the provider");
|
||||
Hit = false;
|
||||
}
|
||||
public void AssertNotHit()
|
||||
{
|
||||
Assert.False(Hit, "Should have not hit the provider");
|
||||
Hit = false;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var coinAverage = new CoinAverageRateProvider();
|
||||
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
|
||||
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
|
||||
|
||||
var spy = new SpyRateProvider();
|
||||
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
|
||||
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
factory.CacheSpan = TimeSpan.FromSeconds(10);
|
||||
var factory = CreateBTCPayRateFactory();
|
||||
factory.Providers.Clear();
|
||||
factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
|
||||
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
|
||||
factory.CacheSpan = TimeSpan.FromSeconds(1);
|
||||
|
||||
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.False(fetchedRate.Cached);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.True(fetchedRate.Cached);
|
||||
var fetcher = new RateFetcher(factory);
|
||||
|
||||
Thread.Sleep(11000);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.False(fetchedRate.Cached);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.True(fetchedRate.Cached);
|
||||
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
|
||||
Thread.Sleep(3000);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
// Should cache at exchange level so this should hit the cache
|
||||
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.True(fetchedRate.Cached);
|
||||
Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value);
|
||||
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
Assert.Null(fetchedRate2.BidAsk);
|
||||
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
|
||||
|
||||
// Should cache at exchange level this should not hit the cache as it is different exchange
|
||||
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
|
||||
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
Assert.False(fetchedRate.Cached);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
|
||||
factory.Providers.Clear();
|
||||
var fetch = new BackgroundFetcherRateProvider(spy);
|
||||
fetch.DoNotAutoFetchIfExpired = true;
|
||||
factory.Providers.Add("bittrex", fetch);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
|
||||
Thread.Sleep(1020);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
|
||||
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetch.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Thread.Sleep(1000);
|
||||
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
|
@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,9 @@ services:
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_DB: "Postgres"
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
@ -34,14 +36,16 @@ services:
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- mysql
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
@ -49,21 +53,24 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- mysql
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.8
|
||||
image: nicolasdorier/nbxplorer:1.1.0.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -87,31 +94,32 @@ services:
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
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
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "28332" # ZMQ
|
||||
- "28333" # ZMQ
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
image: nicolasdorier/clightning:v0.6.1-1-dev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -122,6 +130,7 @@ services:
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
@ -134,7 +143,8 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.15
|
||||
image: shesek/lightning-charge:0.4.3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -153,7 +163,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
image: nicolasdorier/clightning:v0.6.1-1-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -198,9 +208,19 @@ services:
|
||||
- "39372:5432"
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0.12
|
||||
expose:
|
||||
- "3306"
|
||||
ports:
|
||||
- "33036:3306"
|
||||
environment:
|
||||
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
@ -208,11 +228,13 @@ services:
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
@ -224,7 +246,8 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
@ -232,11 +255,13 @@ services:
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
|
3
BTCPayServer.Tests/xunit.runner.json
Normal file
3
BTCPayServer.Tests/xunit.runner.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"parallelizeTestCollections": false
|
||||
}
|
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ namespace BTCPayServer
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
|
@ -50,6 +50,7 @@ namespace BTCPayServer
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitDash();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
|
@ -2,9 +2,12 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.49</Version>
|
||||
<Version>1.0.3.2</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
@ -30,32 +33,38 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
|
||||
<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.27" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.66" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.13" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.3.3" />
|
||||
<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.14" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
|
||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.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.App" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -114,6 +123,55 @@
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\Apps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LndRestServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButtonEnable.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButton.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Public\PayButtonHandle.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\WalletRescan.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>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
@ -11,6 +11,11 @@ using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -74,41 +79,152 @@ 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, true, 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, " + 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 (!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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
void externalLnd<T>(string code, string lndType)
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>(code, string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
var instanceType = typeof(T);
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
|
||||
}
|
||||
};
|
||||
|
||||
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
|
||||
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
{
|
||||
int waitTime = 0;
|
||||
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
|
||||
{
|
||||
if (waitTime++ < 5)
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
else
|
||||
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))
|
||||
{
|
||||
if (!SSHFingerprint.TryParse(fingerprint, out var f))
|
||||
throw new ConfigException($"Invalid ssh fingerprint format {fingerprint}");
|
||||
TrustedFingerprints.Add(f);
|
||||
}
|
||||
}
|
||||
|
||||
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 SSHSettings ParseSSHConfiguration(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;
|
||||
}
|
||||
|
||||
internal bool IsTrustedFingerprint(byte[] fingerPrint, byte[] hostKey)
|
||||
{
|
||||
return TrustedFingerprints.Any(f => f.Match(fingerPrint, hostKey));
|
||||
}
|
||||
|
||||
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
|
||||
@ -116,6 +232,11 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string MySQLConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
@ -126,6 +247,12 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
|
@ -31,15 +31,23 @@ namespace BTCPayServer.Configuration
|
||||
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("--mysql", $"Connection string to a MySQL 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 public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{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;
|
||||
}
|
||||
@ -101,6 +109,7 @@ namespace BTCPayServer.Configuration
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Database ###");
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine("#mysql=User ID=root;Password=myPassword;Host=localhost;Port=3306;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
|
||||
|
35
BTCPayServer/Configuration/External/ExternalLnd.cs
vendored
Normal file
35
BTCPayServer/Configuration/External/ExternalLnd.cs
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public abstract class ExternalLnd : ExternalService
|
||||
{
|
||||
public ExternalLnd(LightningConnectionString connectionString, LndTypes type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public LndTypes Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public enum LndTypes
|
||||
{
|
||||
gRPC, Rest
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, LndTypes.gRPC) { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, LndTypes.Rest) { }
|
||||
}
|
||||
}
|
22
BTCPayServer/Configuration/External/ExternalService.cs
vendored
Normal file
22
BTCPayServer/Configuration/External/ExternalService.cs
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -69,6 +71,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
@ -87,7 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
@ -236,23 +239,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)
|
||||
{
|
||||
@ -274,7 +279,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
|
||||
|
@ -1,24 +1,11 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Newtonsoft.Json;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using System.IO;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -86,7 +73,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = Parse(settings.Template, settings.Currency);
|
||||
var items = _AppsHelper.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\" />");
|
||||
@ -108,11 +95,11 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
Parse(vm.Template, vm.Currency);
|
||||
_AppsHelper.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -137,137 +124,6 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId && us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/pos")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
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>();
|
||||
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 = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
|
||||
private async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAppSettings(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
|
@ -1,44 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class AppsController : Controller
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
CurrencyNameTable _Currencies;
|
||||
InvoiceController _InvoiceController;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
CurrencyNameTable currencies,
|
||||
InvoiceController invoiceController)
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
AppsHelper appsHelper)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
_NetworkProvider = networkProvider;
|
||||
_AppsHelper = appsHelper;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private AppsHelper _AppsHelper;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
public string CreatedAppId { get; set; }
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
@ -102,7 +105,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var appData = new AppData() { Id = id };
|
||||
@ -113,7 +116,7 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
|
||||
CreatedAppId = id;
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -134,21 +137,9 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = await ctx.UserStore
|
||||
.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;
|
||||
}
|
||||
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
private async Task<StoreData[]> GetOwnedStores()
|
||||
|
202
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
202
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_InvoiceController = invoiceController;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private InvoiceController _InvoiceController;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _AppsHelper.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 = _AppsHelper.Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[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 _AppsHelper.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>();
|
||||
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 = _AppsHelper.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 _AppsHelper.GetStore(app);
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppsHelper
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
CurrencyNameTable _Currencies;
|
||||
|
||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
|
||||
}
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
||||
{
|
||||
return _Currencies.GetCurrencyData(currency, useFallback);
|
||||
}
|
||||
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
|
||||
{
|
||||
if (userId == null || appId == null)
|
||||
return null;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = await ctx.UserStore
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
119
BTCPayServer/Controllers/ChangellyController.cs
Normal file
119
BTCPayServer/Controllers/ChangellyController.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("[controller]/{storeId}")]
|
||||
public class ChangellyController : Controller
|
||||
{
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly RateFetcher _RateProviderFactory;
|
||||
|
||||
public ChangellyController(ChangellyClientProvider changellyClientProvider,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
RateFetcher rateProviderFactory)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("currencies")]
|
||||
public async Task<IActionResult> GetCurrencyList(string storeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var client = await TryGetChangellyClient(storeId);
|
||||
|
||||
return Ok(await client.GetCurrenciesFull());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await TryGetChangellyClient(storeId);
|
||||
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
|
||||
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
|
||||
while (true)
|
||||
{
|
||||
if (callCounter > 10)
|
||||
{
|
||||
BadRequest();
|
||||
}
|
||||
|
||||
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
|
||||
callCounter++;
|
||||
if (computedAmount < toCurrencyAmount)
|
||||
{
|
||||
currentAmount =
|
||||
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
|
||||
toCurrencyAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(currentAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Changelly> TryGetChangellyClient(string storeId)
|
||||
{
|
||||
var store = IsTest? null: HttpContext.GetStoreData();
|
||||
storeId = storeId ?? store?.Id;
|
||||
|
||||
return await _changellyClientProvider.TryGetChangellyClient(storeId, store);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
}
|
||||
|
||||
public bool IsTest { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -1,26 +1,20 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
|
@ -1,16 +1,14 @@
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -84,4 +82,36 @@ namespace BTCPayServer.Controllers
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,26 @@
|
||||
using BTCPayServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -34,7 +32,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId,
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
@ -61,7 +58,8 @@ namespace BTCPayServer.Controllers
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
NotificationEmail = invoice.NotificationEmail,
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
@ -213,10 +211,9 @@ 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)
|
||||
@ -231,7 +228,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
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();
|
||||
@ -244,6 +245,18 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
|
||||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
|
||||
var changellyAmountDue = changelly != null
|
||||
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
|
||||
(1m + (changelly.AmountMarkupPercentage / 100m)))
|
||||
: (decimal?)null;
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
@ -283,7 +296,10 @@ namespace BTCPayServer.Controllers
|
||||
Status = invoice.Status,
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
ChangellyEnabled = changelly != null,
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
@ -307,7 +323,7 @@ namespace BTCPayServer.Controllers
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " - Lightning";
|
||||
network.DisplayName : network.DisplayName + " (Lightning)";
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
@ -323,39 +339,12 @@ namespace BTCPayServer.Controllers
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
||||
}
|
||||
|
||||
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
||||
{
|
||||
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})";
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -432,23 +421,17 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
var model = new InvoicesModel();
|
||||
var filterString = new SearchString(searchTerm);
|
||||
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
SearchTerm = searchTerm,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
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
|
||||
}))
|
||||
Count = count,
|
||||
StatusMessage = StatusMessage
|
||||
};
|
||||
|
||||
var list = await ListInvoicesProcess(searchTerm, skip, count);
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
@ -460,12 +443,29 @@ namespace BTCPayServer.Controllers
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
model.Skip = skip;
|
||||
model.Count = count;
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
var filterString = new SearchString(searchTerm);
|
||||
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
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
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
@ -499,7 +499,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
StatusMessage = null;
|
||||
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
||||
return View(model);
|
||||
@ -528,6 +528,7 @@ namespace BTCPayServer.Controllers
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationEmail = model.NotificationEmail,
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
|
@ -1,47 +1,25 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin.Payment;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -49,7 +27,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
ContentSecurityPolicies _CSP;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
RateFetcher _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
@ -62,7 +40,7 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayRateProviderFactory rateProvider,
|
||||
RateFetcher rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
@ -84,6 +62,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
@ -101,6 +83,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
@ -121,11 +104,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
|
||||
await UpdateCLightningConnectionStringIfNeeded(store);
|
||||
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.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));
|
||||
@ -138,7 +120,9 @@ namespace BTCPayServer.Controllers
|
||||
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,
|
||||
@ -146,59 +130,26 @@ namespace BTCPayServer.Controllers
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList();
|
||||
|
||||
List<string> invoiceLogs = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
foreach (var pair in fetchingByCurrencyPair)
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
invoiceLogs.Add($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
invoiceLogs.Add($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
invoiceLogs.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
if (rateResult.ExchangeExceptions.Count != 0)
|
||||
{
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
invoiceLogs.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
throw new PaymentMethodUnavailableException("Payment method unavailable");
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
invoiceLogs.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
invoiceLogs.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 invoiceLogs)
|
||||
foreach (var error in logs.ToList())
|
||||
{
|
||||
errors.AppendLine(error);
|
||||
errors.AppendLine(error.ToString());
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
@ -206,87 +157,106 @@ namespace BTCPayServer.Controllers
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, invoiceLogs, _NetworkProvider);
|
||||
|
||||
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 UpdateCLightningConnectionStringIfNeeded(StoreData store)
|
||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
||||
{
|
||||
var lightning = method.GetLightningUrl();
|
||||
if (lightning.IsLegacy)
|
||||
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)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
needUpdate = true;
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
}
|
||||
if(needUpdate)
|
||||
await _StoreRepository.UpdateStore(store);
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
if (rate.Value == null)
|
||||
return null;
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.Value.Value;
|
||||
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)
|
||||
try
|
||||
{
|
||||
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.Value.HasValue)
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
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, preparePayment);
|
||||
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)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(errorMessage);
|
||||
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;
|
||||
}
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
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)
|
||||
|
@ -1,40 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
62
BTCPayServer/Controllers/PublicController.cs
Normal file
62
BTCPayServer/Controllers/PublicController.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PublicController : Controller
|
||||
{
|
||||
public PublicController(InvoiceController invoiceController,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
private InvoiceController _InvoiceController;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/v1/invoices")]
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
ModelState.AddModelError("Store", "Invalid store");
|
||||
else
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
|
||||
}
|
||||
|
||||
if (model == null || model.Price <= 0)
|
||||
ModelState.AddModelError("Price", "Price must be greater than 0");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
NotificationEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,33 +10,85 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Authentication;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[AllowAnonymous]
|
||||
public class RateController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
RateFetcher _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
|
||||
public TokenRepository TokenRepository { get; }
|
||||
|
||||
public RateController(
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
RateFetcher rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
TokenRepository = tokenRepository;
|
||||
_StoreRepo = storeRepo;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
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 = await GetStoreId(storeId);
|
||||
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 currencyPairs, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
@ -44,19 +96,37 @@ namespace BTCPayServer.Controllers
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
private async Task<string> GetStoreId(string storeId)
|
||||
{
|
||||
if (storeId != null && this.HttpContext.GetStoreData()?.Id == storeId)
|
||||
return storeId;
|
||||
if(storeId == null)
|
||||
{
|
||||
var tokens = await this.TokenRepository.GetTokens(this.User.GetSIN());
|
||||
storeId = tokens.Select(s => s.StoreId).Where(s => s != null).FirstOrDefault();
|
||||
}
|
||||
if (storeId == null)
|
||||
return null;
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return null;
|
||||
this.HttpContext.SetStoreData(store);
|
||||
return storeId;
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
if(storeId == null || currencyPairs == null)
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
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)
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
@ -64,12 +134,30 @@ namespace BTCPayServer.Controllers
|
||||
result.StatusCode = 404;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
|
||||
|
||||
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(','))
|
||||
foreach (var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if(!CurrencyPair.TryParse(currency, out var pair))
|
||||
if (!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
@ -81,7 +169,7 @@ namespace BTCPayServer.Controllers
|
||||
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().Value))
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
@ -94,6 +182,20 @@ namespace BTCPayServer.Controllers
|
||||
}).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
|
||||
{
|
||||
|
||||
|
@ -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,6 +22,10 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -24,15 +34,27 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
Configuration.BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_Options = options;
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -74,7 +96,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if(service != null)
|
||||
if (service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
@ -134,6 +156,179 @@ 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 (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
|
||||
return View(vm);
|
||||
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;
|
||||
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
|
||||
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
|
||||
await Task.WhenAll(addresses1, addresses2);
|
||||
|
||||
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
|
||||
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
|
||||
if (!hasCommonAddress)
|
||||
{
|
||||
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.HostKeyName} 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, e.HostKey);
|
||||
if (!e.CanTrust)
|
||||
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} 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);
|
||||
@ -188,6 +383,7 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
await _StoreRepository.CleanUnreachableStores();
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
@ -220,6 +416,170 @@ 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<ExternalLnd>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = grpcService.Type,
|
||||
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<ExternalLnd>(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
|
||||
{
|
||||
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/lnd-rest/{cryptoCode}/{index}")]
|
||||
public IActionResult LndRestServices(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 LndRestServicesViewModel();
|
||||
|
||||
model.BaseApiUrl = external.BaseUri.ToString();
|
||||
if (external.CertificateThumbprint != null)
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
if (external.Macaroon != null)
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
@ -243,7 +603,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!model.Settings.IsComplete())
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
|
@ -34,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);
|
||||
@ -44,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)
|
||||
@ -59,7 +60,7 @@ 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 = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
@ -78,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,11 +99,39 @@ namespace BTCPayServer.Controllers
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
if (!vm.Confirmation && strategy != null)
|
||||
return ShowAddresses(vm, strategy);
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is setting a new derivation scheme
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
|
||||
// - The user is clicking on continue without changing anything
|
||||
(!vm.Confirmation && willBeExcluded == wasExcluded);
|
||||
|
||||
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
|
||||
if (!showAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
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
|
||||
@ -123,26 +157,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)
|
||||
@ -161,274 +177,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 = HttpContext.GetStoreData();
|
||||
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.GetKeyPath(network, strategy) == 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(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 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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 }; }
|
||||
finally { hw.Dispose(); }
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
96
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
96
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/changelly")]
|
||||
public IActionResult UpdateChangellySettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
|
||||
{
|
||||
|
||||
var existing = store.GetStoreBlob().ChangellySettings;
|
||||
if (existing == null) return;
|
||||
vm.ApiKey = existing.ApiKey;
|
||||
vm.ApiSecret = existing.ApiSecret;
|
||||
vm.ApiUrl = existing.ApiUrl;
|
||||
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
vm.ShowFiat = existing.ShowFiat;
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/changelly")]
|
||||
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (vm.Enabled)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var changellySettings = new ChangellySettings()
|
||||
{
|
||||
ApiKey = vm.ApiKey,
|
||||
ApiSecret = vm.ApiSecret,
|
||||
ApiUrl = vm.ApiUrl,
|
||||
ChangellyMerchantId = vm.ChangellyMerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage,
|
||||
ShowFiat = vm.ShowFiat
|
||||
};
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.ChangellySettings = changellySettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Changelly settings modified";
|
||||
_changellyClientProvider.InvalidateClient(storeId);
|
||||
return RedirectToAction(nameof(UpdateStore), new {
|
||||
storeId});
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
vm.StatusMessage = "Test Successful";
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,12 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using System.Net;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -24,9 +24,11 @@ namespace BTCPayServer.Controllers
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
@ -34,6 +36,7 @@ namespace BTCPayServer.Controllers
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -81,6 +84,12 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
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 ||
|
||||
@ -105,7 +114,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
@ -128,39 +137,41 @@ 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)
|
||||
{
|
||||
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 succeeded ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,16 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@ -11,21 +18,13 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -35,50 +34,47 @@ namespace BTCPayServer.Controllers
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateFactory;
|
||||
RateFetcher _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,
|
||||
RateFetcher rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env)
|
||||
ChangellyClientProvider changellyClientProvider,
|
||||
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_LangService = langService;
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_TokenController = tokenController;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
}
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
IServiceProvider _ServiceProvider;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private ExplorerClientProvider _ExplorerProvider;
|
||||
private MvcJsonOptions _MvcJsonOptions;
|
||||
private IFeeProviderFactory _FeeRateProvider;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
AccessTokenController _TokenController;
|
||||
@ -86,7 +82,9 @@ namespace BTCPayServer.Controllers
|
||||
TokenRepository _TokenRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private LanguageService _LangService;
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
IHostingEnvironment _Env;
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
@ -94,21 +92,6 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public IActionResult Wallet(string cryptoCode)
|
||||
{
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(StoreData.Id);
|
||||
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()
|
||||
@ -200,7 +183,7 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
@ -225,7 +208,7 @@ namespace BTCPayServer.Controllers
|
||||
model.AvailableExchanges = GetSupportedExchanges();
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
@ -283,7 +266,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CurrencyPair = fetch.Key.ToString(),
|
||||
Error = testResult.Errors.Count != 0,
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
@ -338,11 +321,10 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.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;
|
||||
@ -373,7 +355,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
bool needUpdate = false;
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
@ -386,7 +368,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
@ -423,8 +404,10 @@ namespace BTCPayServer.Controllers
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, storeBlob, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
@ -433,8 +416,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
@ -446,7 +430,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))
|
||||
});
|
||||
}
|
||||
|
||||
@ -458,20 +444,29 @@ 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)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
Enabled = changellyEnabled,
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(StoreViewModel model)
|
||||
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
|
||||
{
|
||||
AddPaymentMethods(StoreData, model);
|
||||
|
||||
bool needUpdate = false;
|
||||
if (StoreData.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
@ -490,6 +485,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
@ -511,11 +507,34 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
await _Repo.DeleteStore(StoreData.Id);
|
||||
StatusMessage = "Success: Store successfully deleted";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
|
||||
private CoinAverageExchange[] GetSupportedExchanges()
|
||||
{
|
||||
return _RateFactory.GetSupportedExchanges()
|
||||
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
@ -761,5 +780,54 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: Need to have talk about how architect default currency implementation
|
||||
// For now we have also hardcoded USD for Store creation and then Invoice creation
|
||||
const string DEFAULT_CURRENCY = "USD";
|
||||
|
||||
[Route("{storeId}/paybutton")]
|
||||
public IActionResult PayButton()
|
||||
{
|
||||
var store = StoreData;
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
{
|
||||
return View("PayButtonEnable", null);
|
||||
}
|
||||
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
||||
var model = new PayButtonViewModel
|
||||
{
|
||||
Price = 10,
|
||||
Currency = DEFAULT_CURRENCY,
|
||||
ButtonSize = 2,
|
||||
UrlRoot = appUrl,
|
||||
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
|
||||
StoreId = store.Id
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/paybutton")]
|
||||
public async Task<IActionResult> PayButton(bool enableStore)
|
||||
{
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = enableStore;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(PayButton), new
|
||||
{
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,33 +23,16 @@ namespace BTCPayServer.Controllers
|
||||
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 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"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
@ -63,8 +46,23 @@ 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();
|
||||
@ -84,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];
|
||||
@ -103,8 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
|
||||
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
@ -127,22 +113,6 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
560
BTCPayServer/Controllers/WalletsController.cs
Normal file
560
BTCPayServer/Controllers/WalletsController.cs
Normal file
@ -0,0 +1,560 @@
|
||||
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 NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Controllers.StoresController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("wallets")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class WalletsController : Controller
|
||||
{
|
||||
public StoreRepository Repository { get; }
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public ExplorerClientProvider ExplorerClientProvider { get; }
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
|
||||
private readonly NBXplorerDashboard _dashboard;
|
||||
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
public RateFetcher RateFetcher { get; }
|
||||
CurrencyNameTable _currencyTable;
|
||||
public WalletsController(StoreRepository repo,
|
||||
CurrencyNameTable currencyTable,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
NBXplorerDashboard dashboard,
|
||||
RateFetcher rateProvider,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
Repository = repo;
|
||||
RateFetcher = rateProvider;
|
||||
NetworkProvider = networkProvider;
|
||||
_userManager = userManager;
|
||||
_mvcJsonOptions = mvcJsonOptions;
|
||||
_dashboard = dashboard;
|
||||
ExplorerClientProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListWallets()
|
||||
{
|
||||
var wallets = new ListWalletsViewModel();
|
||||
var stores = await Repository.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 Repository.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, string defaultDestination = null, string defaultAmount = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.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()
|
||||
{
|
||||
DefaultAddress = defaultDestination,
|
||||
DefaultAmount = defaultAmount,
|
||||
ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase),
|
||||
CryptoCurrency = walletId.CryptoCode
|
||||
};
|
||||
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await RateFetcher.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);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new RescanWalletModel();
|
||||
vm.IsFullySync = _dashboard.IsFullySynched();
|
||||
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
|
||||
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
|
||||
if(scanProgress != null)
|
||||
{
|
||||
vm.PreviousError = scanProgress.Error;
|
||||
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
|
||||
{
|
||||
if (scanProgress.Progress == null)
|
||||
{
|
||||
vm.Progress = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Progress = scanProgress.Progress.OverallProgress;
|
||||
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
|
||||
}
|
||||
}
|
||||
if (scanProgress.Status == ScanUTXOStatus.Complete)
|
||||
{
|
||||
vm.LastSuccess = scanProgress.Progress;
|
||||
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
|
||||
}
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/rescan")]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings.Key)]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, RescanWalletModel vm)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
|
||||
{
|
||||
|
||||
}
|
||||
return RedirectToAction();
|
||||
}
|
||||
|
||||
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 = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
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);
|
||||
}
|
||||
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 = ExplorerClientProvider.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";
|
||||
}
|
||||
}
|
@ -19,5 +19,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -102,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
|
||||
{
|
||||
@ -117,9 +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);
|
||||
|
||||
@ -133,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);
|
||||
@ -141,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
|
||||
{
|
||||
@ -156,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
|
||||
{
|
||||
|
@ -17,7 +17,8 @@ namespace BTCPayServer.Data
|
||||
public enum DatabaseType
|
||||
{
|
||||
Sqlite,
|
||||
Postgres
|
||||
Postgres,
|
||||
MySQL,
|
||||
}
|
||||
public class ApplicationDbContextFactory
|
||||
{
|
||||
@ -29,6 +30,15 @@ namespace BTCPayServer.Data
|
||||
_Type = type;
|
||||
}
|
||||
|
||||
|
||||
public DatabaseType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Type;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
@ -86,6 +96,8 @@ namespace BTCPayServer.Data
|
||||
builder
|
||||
.UseNpgsql(_ConnectionString)
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
else if (_Type == DatabaseType.MySQL)
|
||||
builder.UseMySql(_ConnectionString);
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -34,12 +35,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
|
||||
{
|
||||
@ -165,24 +167,25 @@ namespace BTCPayServer.Data
|
||||
public Claim[] GetClaims()
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.AddRange(AdditionalClaims);
|
||||
#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)
|
||||
|
||||
if(role == StoreRoles.Owner || role == StoreRoles.Guest || GetStoreBlob().AnyoneCanInvoice)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, Id));
|
||||
}
|
||||
return claims.ToArray();
|
||||
}
|
||||
|
||||
public bool HasClaim(string claim)
|
||||
{
|
||||
return GetClaims().Any(c => c.Type == claim);
|
||||
return GetClaims().Any(c => c.Type == claim && c.Value == Id);
|
||||
}
|
||||
|
||||
public byte[] StoreBlob
|
||||
@ -192,11 +195,16 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto()
|
||||
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
|
||||
{
|
||||
return DefaultCrypto ?? "BTC";
|
||||
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
{
|
||||
@ -254,11 +262,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
@ -278,23 +281,9 @@ namespace BTCPayServer.Data
|
||||
set;
|
||||
}
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule_Obsolete>();
|
||||
RateRules.Add(new RateRule_Obsolete() { 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;
|
||||
|
||||
[Obsolete]
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
@ -313,6 +302,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public string RateScript { get; set; }
|
||||
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
public ChangellySettings ChangellySettings { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
public string LightningDescriptionTemplate
|
||||
@ -333,15 +326,15 @@ namespace BTCPayServer.Data
|
||||
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
|
||||
{
|
||||
return GetDefaultRateRules(networkProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
rules.GlobalMultiplier = GetRateMultiplier();
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
@ -367,8 +360,37 @@ namespace BTCPayServer.Data
|
||||
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
|
||||
|
||||
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
|
||||
rules.GlobalMultiplier = GetRateMultiplier();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -31,6 +31,7 @@ using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -82,6 +83,15 @@ namespace BTCPayServer
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
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))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
@ -127,6 +137,23 @@ 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 =
|
||||
|
@ -28,6 +28,28 @@ namespace BTCPayServer.Filters
|
||||
}
|
||||
}
|
||||
|
||||
public class MediaTypeAcceptConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public MediaTypeAcceptConstraintAttribute(string mediaType)
|
||||
{
|
||||
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
if (!context.RouteContext.HttpContext.Request.Headers.ContainsKey("Accept"))
|
||||
return false;
|
||||
return context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().StartsWith(MediaType, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
|
||||
|
@ -23,7 +23,10 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
if (context.IsEffectivePolicy<XFrameOptionsAttribute>(this))
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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, e.HostKey))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -52,24 +53,44 @@ namespace BTCPayServer.HostedServices
|
||||
EventAggregator _EventAggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
IEmailSender _EmailSender;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<InvoiceNotificationManager> logger)
|
||||
ILogger<InvoiceNotificationManager> logger,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_EmailSender = emailSender;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
|
||||
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
|
||||
{
|
||||
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
|
||||
var ipn = new
|
||||
{
|
||||
invoice.Id,
|
||||
invoice.Status,
|
||||
invoice.StoreId
|
||||
};
|
||||
// TODO: Consider adding info on ItemDesc and payment info (amount)
|
||||
|
||||
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
|
||||
await _EmailSender.SendEmailAsync(
|
||||
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
@ -202,7 +223,8 @@ namespace BTCPayServer.HostedServices
|
||||
PaymentSubtotals = dto.PaymentSubtotals,
|
||||
PaymentTotals = dto.PaymentTotals,
|
||||
AmountPaid = dto.AmountPaid,
|
||||
ExchangeRates = dto.ExchangeRates
|
||||
ExchangeRates = dto.ExchangeRates,
|
||||
|
||||
};
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
@ -263,15 +285,15 @@ namespace BTCPayServer.HostedServices
|
||||
sendRequest()
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if(t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
completion.TrySetResult(t.Result);
|
||||
}
|
||||
if(t.Status == TaskStatus.Faulted)
|
||||
if (t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
if(t.Status == TaskStatus.Canceled)
|
||||
if (t.Status == TaskStatus.Canceled)
|
||||
{
|
||||
completion.TrySetCanceled();
|
||||
}
|
||||
@ -288,7 +310,7 @@ namespace BTCPayServer.HostedServices
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
|
||||
if(executing2 == sending)
|
||||
if (executing2 == sending)
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
@ -309,6 +331,8 @@ namespace BTCPayServer.HostedServices
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async 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
|
||||
|
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -47,7 +47,11 @@ namespace BTCPayServer.HostedServices
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
||||
public NBXplorerSummary Get(string cryptoCode)
|
||||
{
|
||||
_Summaries.TryGetValue(cryptoCode, out var summary);
|
||||
return summary;
|
||||
}
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
|
@ -18,9 +18,9 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
RateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
RateProviderFactory rateProviderFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
@ -33,16 +33,41 @@ namespace BTCPayServer.HostedServices
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
|
||||
CreateLoopTask(RefreshCoinAverageSettings)
|
||||
CreateLoopTask(RefreshCoinAverageSettings),
|
||||
CreateLoopTask(RefreshRates)
|
||||
};
|
||||
}
|
||||
async Task RefreshRates()
|
||||
{
|
||||
|
||||
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
|
||||
{
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(_RateProviderFactory.Providers
|
||||
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
|
||||
{
|
||||
if (t.Result.Exception != null)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
|
||||
}
|
||||
}, TaskScheduler.Default))
|
||||
.ToArray()).WithCancellation(timeout.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach(var item in tickers
|
||||
foreach (var item in tickers
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
{
|
||||
@ -54,7 +79,6 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
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))
|
||||
|
@ -38,7 +38,12 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NicolasDorier.RateLimits;
|
||||
using Npgsql;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -51,6 +56,7 @@ namespace BTCPayServer.Hosting
|
||||
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
|
||||
factory.ConfigureBuilder(o);
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
@ -71,20 +77,25 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
ApplicationDbContextFactory dbContext = null;
|
||||
if (opts.PostgresConnectionString == null)
|
||||
if (!String.IsNullOrEmpty(opts.PostgresConnectionString))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
|
||||
}
|
||||
else if(!String.IsNullOrEmpty(opts.MySQLConnectionString))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"MySQL DB used ({opts.MySQLConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.MySQL, opts.MySQLConnectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
|
||||
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
|
||||
}
|
||||
|
||||
return dbContext;
|
||||
});
|
||||
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
|
||||
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
@ -92,6 +103,9 @@ namespace BTCPayServer.Hosting
|
||||
return opts.NetworkProvider;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<AppsHelper>();
|
||||
|
||||
services.TryAddSingleton<LightningConfigurationProvider>();
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
@ -104,14 +118,23 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
|
||||
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>();
|
||||
|
||||
services.AddSingleton<ChangellyClientProvider>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
@ -127,7 +150,8 @@ namespace BTCPayServer.Hosting
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
services.TryAddSingleton<BTCPayRateProviderFactory>();
|
||||
services.TryAddSingleton<RateProviderFactory>();
|
||||
services.TryAddSingleton<RateFetcher>();
|
||||
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
@ -144,11 +168,19 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var opts = provider.GetRequiredService<BTCPayServerOptions>();
|
||||
var bundle = new BundleOptions();
|
||||
bundle.UseMinifiedFiles = opts.BundleJsCss;
|
||||
bundle.UseBundles = opts.BundleJsCss;
|
||||
bundle.AppendVersion = true;
|
||||
return bundle;
|
||||
});
|
||||
|
||||
services.AddCors(options=>
|
||||
{
|
||||
options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
var rateLimits = new RateLimitService();
|
||||
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
|
||||
services.AddSingleton(rateLimits);
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -169,7 +201,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
static void Retry(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(1000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
@ -177,7 +209,9 @@ namespace BTCPayServer.Hosting
|
||||
act();
|
||||
return;
|
||||
}
|
||||
catch when(!cts.IsCancellationRequested)
|
||||
// Starting up
|
||||
catch (PostgresException ex) when (ex.SqlState == "57P03") { Thread.Sleep(1000); }
|
||||
catch when (!cts.IsCancellationRequested)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
@ -83,14 +83,14 @@ namespace BTCPayServer.Hosting
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
@ -100,13 +100,13 @@ namespace BTCPayServer.Hosting
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
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"))
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@ -140,13 +140,9 @@ namespace BTCPayServer.Hosting
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
|
||||
}
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
|
@ -100,6 +100,9 @@ namespace BTCPayServer.Hosting
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
});
|
||||
|
||||
services.AddHangfire((o) =>
|
||||
@ -155,6 +158,7 @@ namespace BTCPayServer.Hosting
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
@ -165,6 +169,7 @@ namespace BTCPayServer.Hosting
|
||||
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
|
||||
});
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
app.UseMvc(routes =>
|
||||
{
|
||||
routes.MapRoute(
|
||||
|
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class LightMoneyJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
Type longType = typeof(long).GetTypeInfo();
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
reader.TokenType == JsonToken.Integer ?
|
||||
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
|
||||
: new LightMoney(long.MaxValue) :
|
||||
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
||||
: null;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(((LightMoney)value).MilliSatoshi);
|
||||
}
|
||||
}
|
||||
}
|
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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.2-rtm-10011");
|
||||
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -202,8 +198,7 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -442,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);
|
||||
@ -463,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;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string AppName { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
|
||||
public string UpdateAction { get { return "Update" + AppType; } }
|
||||
public string ViewAction { get { return "View" + AppType; } }
|
||||
}
|
||||
|
@ -53,6 +53,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[EmailAddress]
|
||||
public string NotificationEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Uri]
|
||||
public string NotificationUrl
|
||||
{
|
||||
|
@ -142,5 +142,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public AddressModel[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
public string NotificationEmail { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,10 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string PaymentMethodName { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
|
||||
public bool AllowCoinConversion { get; set; }
|
||||
public bool ChangellyEnabled { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string PeerInfo { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal? ChangellyAmountDue { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class LndGrpcServicesViewModel
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public bool SSL { get; set; }
|
||||
public string Macaroon { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string QRCode { get; set; }
|
||||
public string QRCodeLink { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class LndRestServicesViewModel
|
||||
{
|
||||
public string BaseApiUrl { get; set; }
|
||||
public string Macaroon { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
}
|
||||
}
|
37
BTCPayServer/Models/ServerViewModels/MaintenanceViewModel.cs
Normal file
37
BTCPayServer/Models/ServerViewModels/MaintenanceViewModel.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.SSH;
|
||||
using Renci.SshNet;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class MaintenanceViewModel
|
||||
{
|
||||
public bool ExposedSSH { get; set; }
|
||||
[Required]
|
||||
public string UserName { get; set; }
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
[Display(Name = "Change domain")]
|
||||
public string DNSDomain { get; set; }
|
||||
public SshClient CreateSSHClient(string host)
|
||||
{
|
||||
return new SshClient(host, UserName, Password);
|
||||
}
|
||||
|
||||
internal void SetConfiguredSSH(SSHSettings settings)
|
||||
{
|
||||
if(settings != null)
|
||||
{
|
||||
ExposedSSH = true;
|
||||
UserName = "unknown";
|
||||
Password = "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
BTCPayServer/Models/ServerViewModels/SSHServiceViewModel.cs
Normal file
15
BTCPayServer/Models/ServerViewModels/SSHServiceViewModel.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class SSHServiceViewModel
|
||||
{
|
||||
public string CommandLine { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string KeyFilePassword { get; set; }
|
||||
public bool HasKeyFile { get; set; }
|
||||
}
|
||||
}
|
20
BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs
Normal file
20
BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration.External;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class ServicesViewModel
|
||||
{
|
||||
public class LNDServiceViewModel
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public LndTypes Type { get; set; }
|
||||
public int Index { get; set; }
|
||||
}
|
||||
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
|
||||
public bool HasSSH { get; set; }
|
||||
}
|
||||
}
|
@ -23,11 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
@ -27,6 +27,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Hint address")]
|
||||
public string HintAddress { get; set; }
|
||||
public bool Confirmation { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public string ServerUrl { get; set; }
|
||||
public string StatusMessage { get; internal set; }
|
||||
|
@ -24,5 +24,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string StatusMessage { get; set; }
|
||||
public string InternalLightningNode { get; internal set; }
|
||||
public bool SkipPortTest { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
32
BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs
Normal file
32
BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class PayButtonViewModel
|
||||
{
|
||||
public decimal Price { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
[Required]
|
||||
public string Currency { get; set; }
|
||||
public string CheckoutDesc { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public int ButtonSize { get; set; }
|
||||
[Url]
|
||||
public string ServerIpn { get; set; }
|
||||
[Url]
|
||||
public string BrowserRedirect { get; set; }
|
||||
[EmailAddress]
|
||||
public string NotifyEmail { get; set; }
|
||||
|
||||
public string StoreId { get; set; }
|
||||
|
||||
// Data that influences Pay Button UI, but not invoice creation
|
||||
public string UrlRoot { get; set; }
|
||||
public List<string> CurrencyDropdown { get; set; }
|
||||
public string PayButtonImageUrl { get; set; }
|
||||
}
|
||||
}
|
@ -44,9 +44,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string ScriptTest { get; set; }
|
||||
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
[Display(Name = "Add a spread on exchange rate of ... %")]
|
||||
[Range(0.0, 100.0)]
|
||||
public double Spread
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -18,13 +18,22 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string Value { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class ThirdPartyPaymentMethod
|
||||
{
|
||||
public string Provider { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Action { get; set; }
|
||||
}
|
||||
|
||||
public StoreViewModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool CanDelete { get; set; }
|
||||
public string Id { get; set; }
|
||||
[Display(Name = "Store Name")]
|
||||
[Required]
|
||||
@ -44,8 +53,14 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Allow anyone to create invoice")]
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public List<ThirdPartyPaymentMethod> ThirdPartyPaymentMethods { get; set; } =
|
||||
new List<ThirdPartyPaymentMethod>();
|
||||
|
||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||
[Range(1, 60 * 24 * 24)]
|
||||
public int InvoiceExpiration
|
||||
@ -81,6 +96,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string Address { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
public List<LightningNode> LightningNodes
|
||||
{
|
||||
|
@ -34,10 +34,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string[] Balances
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.InteropServices;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class UpdateChangellySettingsViewModel
|
||||
{
|
||||
[Required] public string ApiKey { get; set; }
|
||||
|
||||
[Required] public string ApiSecret { get; set; }
|
||||
|
||||
[Required] public string ApiUrl { get; set; } = "https://api.changelly.com";
|
||||
|
||||
[Display(Name = "Optional, Changelly Merchant Id")]
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
|
||||
[Display(Name = "Show Fiat Currencies as option in conversion")]
|
||||
public bool ShowFiat { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Range(0, 100)]
|
||||
[Display(Name =
|
||||
"Percentage to multiply amount requested at Changelly to avoid underpaid situations due to Changelly not guaranteeing rates. ")]
|
||||
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class ListTransactionsViewModel
|
||||
{
|
||||
public class TransactionViewModel
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Link { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public string Balance { get; set; }
|
||||
}
|
||||
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
|
||||
}
|
||||
}
|
22
BTCPayServer/Models/WalletViewModels/ListWalletsViewModel.cs
Normal file
22
BTCPayServer/Models/WalletViewModels/ListWalletsViewModel.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class ListWalletsViewModel
|
||||
{
|
||||
public class WalletViewModel
|
||||
{
|
||||
public string StoreName { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
public WalletId Id { get; set; }
|
||||
}
|
||||
|
||||
public List<WalletViewModel> Wallets { get; set; } = new List<WalletViewModel>();
|
||||
}
|
||||
}
|
31
BTCPayServer/Models/WalletViewModels/RescanWalletModel.cs
Normal file
31
BTCPayServer/Models/WalletViewModels/RescanWalletModel.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class RescanWalletModel
|
||||
{
|
||||
public bool IsServerAdmin { get; set; }
|
||||
public bool IsSupportedByCurrency { get; set; }
|
||||
public bool IsFullySync { get; set; }
|
||||
public bool Ok => IsServerAdmin && IsSupportedByCurrency && IsFullySync;
|
||||
|
||||
[Range(1000, 10_000)]
|
||||
public int BatchSize { get; set; } = 3000;
|
||||
[Range(0, 10_000_000)]
|
||||
public int StartingIndex { get; set; } = 0;
|
||||
|
||||
[Range(100, 100000)]
|
||||
public int GapLimit { get; set; } = 10000;
|
||||
|
||||
public int? Progress { get; set; }
|
||||
public string PreviousError { get; set; }
|
||||
public ScanUTXOProgress LastSuccess { get; internal set; }
|
||||
public string TimeOfScan { get; internal set; }
|
||||
public string RemainingTime { get; internal set; }
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletModel
|
||||
{
|
||||
@ -15,5 +15,12 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DefaultAddress { get; set; }
|
||||
public string DefaultAmount { get; set; }
|
||||
|
||||
public decimal? Rate { get; set; }
|
||||
public int Divisibility { get; set; }
|
||||
public string Fiat { get; set; }
|
||||
public string RateError { get; set; }
|
||||
}
|
||||
}
|
@ -27,16 +27,30 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
|
||||
class Prepare
|
||||
{
|
||||
public Task<FeeRate> GetFeeRate;
|
||||
public Task<BitcoinAddress> ReserveAddress;
|
||||
}
|
||||
|
||||
public override object PreparePayment(DerivationStrategy supportedPaymentMethod, StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
return new Prepare()
|
||||
{
|
||||
GetFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
|
||||
ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase)
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
if (!_ExplorerProvider.IsAvailable(network))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
|
||||
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
|
||||
var prepare = (Prepare)preparePaymentObject;
|
||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||
onchainMethod.FeeRate = await getFeeRate;
|
||||
onchainMethod.FeeRate = await prepare.GetFeeRate;
|
||||
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||
onchainMethod.DepositAddress = (await getAddress).ToString();
|
||||
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
|
||||
return onchainMethod;
|
||||
}
|
||||
}
|
||||
|
@ -207,6 +207,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
|
||||
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
|
||||
.Select(p => p.Outpoint.Hash)
|
||||
@ -315,6 +317,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
foreach (var invoiceId in invoices)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
if (invoice == null)
|
||||
continue;
|
||||
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
|
||||
var strategy = GetDerivationStrategy(invoice, network);
|
||||
if (strategy == null)
|
||||
@ -332,8 +336,12 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
if (payment != null)
|
||||
{
|
||||
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
|
||||
totalPayment++;
|
||||
if(invoice == null)
|
||||
continue;
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalPayment;
|
||||
@ -350,6 +358,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
|
109
BTCPayServer/Payments/Changelly/Changelly.cs
Normal file
109
BTCPayServer/Payments/Changelly/Changelly.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SshNet.Security.Cryptography;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class Changelly
|
||||
{
|
||||
private readonly string _apisecret;
|
||||
private readonly bool _showFiat;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
{
|
||||
_apisecret = apiSecret;
|
||||
_showFiat = showFiat;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
_httpClient.BaseAddress = new Uri(apiUrl);
|
||||
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
|
||||
}
|
||||
|
||||
|
||||
private static string ToHexString(byte[] array)
|
||||
{
|
||||
var hex = new StringBuilder(array.Length * 2);
|
||||
foreach (var b in array)
|
||||
{
|
||||
hex.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", b);
|
||||
}
|
||||
|
||||
return hex.ToString();
|
||||
}
|
||||
|
||||
private async Task<ChangellyResponse<T>> PostToApi<T>(string message)
|
||||
{
|
||||
var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(_apisecret));
|
||||
var hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
|
||||
var sign = ToHexString(hashMessage);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Headers.Add("sign", sign);
|
||||
request.Content = new StringContent(message, Encoding.UTF8, "application/json");
|
||||
|
||||
var result = await _httpClient.SendAsync(request);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new ChangellyException(result.ReasonPhrase);
|
||||
var content =
|
||||
await result.Content.ReadAsStringAsync();
|
||||
return JObject.Parse(content).ToObject<ChangellyResponse<T>>();
|
||||
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
|
||||
{
|
||||
const string message = @"{
|
||||
""jsonrpc"": ""2.0"",
|
||||
""id"": 1,
|
||||
""method"": ""getCurrenciesFull"",
|
||||
""params"": []
|
||||
}";
|
||||
|
||||
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
|
||||
var appendedResult = _showFiat
|
||||
? result.Result.Concat(new[]
|
||||
{
|
||||
new CurrencyFull()
|
||||
{
|
||||
Enable = true,
|
||||
Name = "EUR",
|
||||
FullName = "Euro",
|
||||
PayInConfirmations = 0,
|
||||
ImageLink = "https://changelly.com/api/coins/eur.png"
|
||||
},
|
||||
new CurrencyFull()
|
||||
{
|
||||
Enable = true,
|
||||
Name = "USD",
|
||||
FullName = "US Dollar",
|
||||
PayInConfirmations = 0,
|
||||
ImageLink = "https://changelly.com/api/coins/usd.png"
|
||||
}
|
||||
})
|
||||
: result.Result;
|
||||
return appendedResult;
|
||||
}
|
||||
|
||||
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
|
||||
string toCurrency,
|
||||
decimal amount)
|
||||
{
|
||||
var message =
|
||||
$"{{\"id\": \"test\",\"jsonrpc\": \"2.0\",\"method\": \"getExchangeAmount\",\"params\":{{\"from\": \"{fromCurrency}\",\"to\": \"{toCurrency}\",\"amount\": \"{amount}\"}}}}";
|
||||
|
||||
var result = await PostToApi<string>(message);
|
||||
|
||||
return Convert.ToDecimal(result.Result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public static class ChangellyCalculationHelper
|
||||
{
|
||||
public static decimal ComputeBaseAmount(decimal baseRate, decimal toAmount)
|
||||
{
|
||||
return (1m / baseRate) * toAmount;
|
||||
}
|
||||
|
||||
public static decimal ComputeCorrectAmount(decimal currentFromAmount, decimal currentAmount,
|
||||
decimal expectedAmount)
|
||||
{
|
||||
return (currentFromAmount / currentAmount) * expectedAmount;
|
||||
}
|
||||
}
|
||||
}
|
70
BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs
Normal file
70
BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellyClientProvider
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Changelly> _clientCache =
|
||||
new ConcurrentDictionary<string, Changelly>();
|
||||
|
||||
public ChangellyClientProvider(StoreRepository storeRepository, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public void InvalidateClient(string storeId)
|
||||
{
|
||||
if (_clientCache.ContainsKey(storeId))
|
||||
{
|
||||
_clientCache.Remove(storeId, out var value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<Changelly> TryGetChangellyClient(string storeId, StoreData storeData = null)
|
||||
{
|
||||
if (_clientCache.ContainsKey(storeId))
|
||||
{
|
||||
return _clientCache[storeId];
|
||||
}
|
||||
|
||||
if (storeData == null)
|
||||
{
|
||||
storeData = await _storeRepository.FindStore(storeId);
|
||||
if (storeData == null)
|
||||
{
|
||||
throw new ChangellyException("Store not found");
|
||||
}
|
||||
}
|
||||
|
||||
var blob = storeData.GetStoreBlob();
|
||||
var changellySettings = blob.ChangellySettings;
|
||||
|
||||
|
||||
if (changellySettings == null || !changellySettings.IsConfigured())
|
||||
{
|
||||
throw new ChangellyException("Changelly not configured for this store");
|
||||
}
|
||||
|
||||
if (!changellySettings.Enabled)
|
||||
{
|
||||
throw new ChangellyException("Changelly not enabled for this store");
|
||||
}
|
||||
|
||||
var changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl, changellySettings.ShowFiat);
|
||||
_clientCache.AddOrReplace(storeId, changelly);
|
||||
return changelly;
|
||||
}
|
||||
}
|
||||
}
|
11
BTCPayServer/Payments/Changelly/ChangellyException.cs
Normal file
11
BTCPayServer/Payments/Changelly/ChangellyException.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellyException : Exception
|
||||
{
|
||||
public ChangellyException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user