Compare commits
110 Commits
Author | SHA1 | Date | |
---|---|---|---|
00c11c7ee9 | |||
82126b85d2 | |||
4f428c8ed1 | |||
9868af4db8 | |||
0a5d7c5efa | |||
c2754b324d | |||
014d08f38a | |||
7998ea142b | |||
a4051dac72 | |||
e3a8892d24 | |||
ea02d77e69 | |||
4f582a6712 | |||
4769b1d452 | |||
17b18d820f | |||
26f34e75c2 | |||
6f50ac50ec | |||
5261cfcdd3 | |||
675920697f | |||
24699bf2ba | |||
5ab92ed794 | |||
8e83f0faa1 | |||
30d5add2ea | |||
6e47babf45 | |||
9b95fa1f20 | |||
c67aa14a87 | |||
23f296ef34 | |||
c6ce676ad3 | |||
fafb02b0dc | |||
53cecc8c8a | |||
db0e9ee8f8 | |||
2fac794d96 | |||
ffcd716906 | |||
85f50724db | |||
808a995741 | |||
4deb853914 | |||
470ec3354e | |||
053c2da9f1 | |||
c0e28ce66e | |||
08dd94e267 | |||
7497865d1f | |||
1888e4fe2b | |||
6746a5cbd5 | |||
baecb7bb0c | |||
28bf4b42bb | |||
c73dc425ad | |||
2138b7dcb8 | |||
8b6c4a9383 | |||
344755cbd0 | |||
63a975267c | |||
3c7d93e88d | |||
75974037bc | |||
e8a346182b | |||
e96d34f741 | |||
7dad814f19 | |||
7e67ca1413 | |||
603263549b | |||
a82b971ce7 | |||
b58c8ef2f0 | |||
274533bfdf | |||
cd6ce401e1 | |||
0c0809101d | |||
4b342376a8 | |||
fd963b9ad0 | |||
06406c0695 | |||
2b567de5c1 | |||
ef46d03760 | |||
b174f299fa | |||
09837966b9 | |||
465dce1d02 | |||
067dbad546 | |||
522970fdb9 | |||
e67aa499a6 | |||
3b68d81507 | |||
051248f2fc | |||
9a239f99f4 | |||
86c431d66e | |||
b35fe0e8e3 | |||
54905f5ceb | |||
1c9b05d992 | |||
a89c71df38 | |||
3db1f2af12 | |||
5399ff2751 | |||
bcea6027e9 | |||
a9722df7e4 | |||
474be6f7be | |||
ada9a7264b | |||
e991b302d0 | |||
eef301c6ec | |||
3fdfd0adfd | |||
358f1ffc43 | |||
349c3409df | |||
0263a2950c | |||
0364a57cae | |||
e232dd7d7e | |||
fee936b569 | |||
420115c54d | |||
312e961098 | |||
223213857f | |||
c0da81557b | |||
81945c0737 | |||
9664e3d6a1 | |||
0a8bd38e76 | |||
013054fb82 | |||
d898f716d1 | |||
1d3f144d21 | |||
de29d87487 | |||
ebef085a9c | |||
2c1f159d72 | |||
1ef59e05a5 | |||
5e09992637 |
.github/ISSUE_TEMPLATE
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csprojProgram.csbundleconfig.json
Dockerfile.linuxamd64Dockerfile.linuxarm32v7Configuration
BTCPayServerOptions.csConfigurationExtensions.csDefaultConfiguration.cs
External
ExternalConnectionString.csExternalService.csSparkConnectionString.csControllers
AccessTokenController.csAppsController.Crowdfund.csAppsController.csAppsPublicController.csChangellyController.csHomeController.csInvoiceController.API.csInvoiceController.UI.csInvoiceController.csMacaroons.csPaymentRequestController.csPublicController.csPublicLightningNodeInfoController.csRateController.csServerController.csStoresController.Changelly.csStoresController.LightningLike.csStoresController.csWalletsController.cs
Data
EndpointParser.csExplorerClientProvider.csExtensions.csHostedServices
Hosting
JsonConverters
ModelBinders
Models
AppViewModels
InvoicingModels
PaymentRequestViewModels
ServerViewModels
StoreViewModels
PaymentRequest
Payments
Changelly
Lightning
Properties
Services
Apps
BTCPayServerEnvironment.csInvoices
Mails
PoliciesSettings.csRates
BackgroundFetcherRateProvider.csBitpayRateProvider.csByllsRateProvider.csCachedRateProvider.csCoinAverageRateProvider.csExchangeSharpRateProvider.csFallbackRateProvider.csIRateProvider.csKrakenExchangeRateProvider.csNullRateProvider.csQuadrigacxRateProvider.csRateFetcher.csRateProviderFactory.cs
SocketFactory.csTorServices.csTorrc.csTor
Views
Apps
AppsPublic
Home
Invoice
PaymentRequest
PublicLightningNodeInfo
Server
Shared
Stores
wwwroot
checkout
crowdfund
img
locales
main
modal
payment-request
vendor
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Report a problem
|
||||
about: File a technical problem or report a bug
|
||||
---
|
||||
|
||||
**Describe the problem/bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Your environment**
|
||||
* Version of BTCPay Server:
|
||||
* Deployment method:
|
||||
* Other relevant environment details:
|
||||
|
||||
**Logs (if applicable)**
|
||||
Basic logs can be found in Server Settings > Logs.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual behavior**
|
||||
Tell us what happens instead
|
||||
|
||||
**Screenshots/Links**
|
||||
If applicable, add screenshots or links to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Ideas and feature requests
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Provide examples**
|
||||
If applicable provide examples, wireframes, sketches or images to better explain your idea.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
@ -129,6 +129,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
|
@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
@ -118,7 +118,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
@ -130,7 +130,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
//Scenario 4: Enabled But End Date < Now - Not Allowed
|
||||
|
||||
@ -142,7 +142,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
|
||||
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
|
||||
@ -156,13 +156,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -10,7 +11,7 @@ namespace BTCPayServer.Tests.Mocks
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
|
@ -52,6 +52,11 @@ using NBitpayClient.Extensions;
|
||||
using BTCPayServer.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Security;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net;
|
||||
using BTCPayServer.Tor;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -143,6 +148,67 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseEndpoint()
|
||||
{
|
||||
Assert.False(EndPointParser.TryParse("126.2.2.2", out var endpoint));
|
||||
Assert.True(EndPointParser.TryParse("126.2.2.2:20", out endpoint));
|
||||
var ipEndpoint = Assert.IsType<IPEndPoint>(endpoint);
|
||||
Assert.Equal("126.2.2.2", ipEndpoint.Address.ToString());
|
||||
Assert.Equal(20, ipEndpoint.Port);
|
||||
Assert.True(EndPointParser.TryParse("toto.com:20", out endpoint));
|
||||
var dnsEndpoint = Assert.IsType<DnsEndPoint>(endpoint);
|
||||
Assert.IsNotType<OnionEndpoint>(endpoint);
|
||||
Assert.Equal("toto.com", dnsEndpoint.Host.ToString());
|
||||
Assert.Equal(20, dnsEndpoint.Port);
|
||||
Assert.False(EndPointParser.TryParse("toto invalid hostname:2029", out endpoint));
|
||||
Assert.True(EndPointParser.TryParse("toto.onion:20", out endpoint));
|
||||
var onionEndpoint = Assert.IsType<OnionEndpoint>(endpoint);
|
||||
Assert.Equal("toto.onion", onionEndpoint.Host.ToString());
|
||||
Assert.Equal(20, onionEndpoint.Port);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseTorrc()
|
||||
{
|
||||
var nl = "\n";
|
||||
var input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81";
|
||||
nl = Environment.NewLine;
|
||||
var expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81" + nl;
|
||||
Assert.True(Torrc.TryParse(input, out var torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
nl = "\r\n";
|
||||
input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:81";
|
||||
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
|
||||
input = "# For the hidden service BTCPayServer" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:80" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
|
||||
"# Redirecting to nginx" + nl +
|
||||
"HiddenServicePort 80 172.19.0.11:80";
|
||||
nl = Environment.NewLine;
|
||||
expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
|
||||
"HiddenServicePort 80 172.19.0.10:80" + nl +
|
||||
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
|
||||
"HiddenServicePort 80 172.19.0.11:80" + nl;
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue()
|
||||
@ -586,7 +652,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
|
||||
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
|
||||
{
|
||||
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule).GetAwaiter().GetResult();
|
||||
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult();
|
||||
Assert.NotNull(result.BidAsk);
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
@ -715,20 +781,32 @@ namespace BTCPayServer.Tests
|
||||
acc.RegisterDerivationScheme("LTC");
|
||||
|
||||
var rateController = acc.GetController<RateController>();
|
||||
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId)
|
||||
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId, default)
|
||||
.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)
|
||||
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
|
||||
// We don't have any default currencies, so this should be failing
|
||||
Assert.Null(GetRatesResult?.Data);
|
||||
|
||||
var store = acc.GetController<StoresController>();
|
||||
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates(acc.StoreId)).Model);
|
||||
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
|
||||
store.Rates(ratesVM).Wait();
|
||||
store = acc.GetController<StoresController>();
|
||||
rateController = acc.GetController<RateController>();
|
||||
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
|
||||
// Now we should have a result
|
||||
Assert.NotNull(GetRatesResult);
|
||||
Assert.NotNull(GetRatesResult.Data);
|
||||
Assert.Equal(2, GetRatesResult.Data.Length);
|
||||
|
||||
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId)
|
||||
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId, default)
|
||||
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate>>();
|
||||
|
||||
Assert.NotNull(GetCurrencyPairRateResult);
|
||||
@ -866,7 +944,7 @@ namespace BTCPayServer.Tests
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
foreach(var req in new[]
|
||||
foreach (var req in new[]
|
||||
{
|
||||
"invoices/",
|
||||
"invoices",
|
||||
@ -1003,7 +1081,7 @@ namespace BTCPayServer.Tests
|
||||
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;
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
|
||||
vm.PreferredExchange = exchange;
|
||||
storeController.Rates(vm).Wait();
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
@ -1041,7 +1119,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
|
||||
|
||||
var storeController = user.GetController<StoresController>();
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
|
||||
Assert.Equal(0.0, vm.Spread);
|
||||
vm.Spread = 40;
|
||||
storeController.Rates(vm).Wait();
|
||||
@ -1140,7 +1218,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var store = user.GetController<StoresController>();
|
||||
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.False(rateVm.ShowScripting);
|
||||
Assert.Equal("coinaverage", rateVm.PreferredExchange);
|
||||
Assert.Equal(0.0, rateVm.Spread);
|
||||
@ -1148,7 +1226,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
rateVm.PreferredExchange = "bitflyer";
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.Equal("bitflyer", rateVm.PreferredExchange);
|
||||
|
||||
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
|
||||
@ -1165,7 +1243,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
|
||||
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
store = user.GetController<StoresController>();
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.Equal(rateVm.StoreId, user.StoreId);
|
||||
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
rateVm.ScriptTest = "BTC_JPY";
|
||||
@ -1182,7 +1261,7 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
|
||||
Assert.Equal(50, rateVm.Spread);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||
@ -2236,7 +2315,7 @@ donation:
|
||||
foreach (var result in factory
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider)
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
|
||||
.ToList())
|
||||
{
|
||||
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
|
||||
@ -2253,12 +2332,13 @@ donation:
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
|
||||
&& 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();
|
||||
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -2274,7 +2354,7 @@ donation:
|
||||
.ToHashSet();
|
||||
|
||||
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||
var result = fetcher.FetchRates(pairs, rules);
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach (var value in result)
|
||||
{
|
||||
var rateResult = value.Value.GetAwaiter().GetResult();
|
||||
@ -2296,7 +2376,7 @@ donation:
|
||||
class SpyRateProvider : IRateProvider
|
||||
{
|
||||
public bool Hit { get; set; }
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Hit = true;
|
||||
var rates = new ExchangeRates();
|
||||
@ -2332,6 +2412,62 @@ donation:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CanExpandExternalConnectionString()
|
||||
{
|
||||
var unusedUri = new Uri("https://toto.com");
|
||||
Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error));
|
||||
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("https://toto.com/test"), expanded.Server);
|
||||
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server);
|
||||
await Assert.ThrowsAsync<SecurityException>(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge));
|
||||
|
||||
// Make sure absolute paths are not expanded
|
||||
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
|
||||
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
|
||||
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
|
||||
|
||||
// Error if directory not exists
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
|
||||
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));
|
||||
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest));
|
||||
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
|
||||
|
||||
var macaroonDirectory = CreateDirectory();
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error));
|
||||
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC);
|
||||
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
|
||||
Assert.NotNull(expanded.Macaroons);
|
||||
Assert.Null(expanded.MacaroonFilePath);
|
||||
Assert.Null(expanded.Macaroons.AdminMacaroon);
|
||||
Assert.Null(expanded.Macaroons.InvoiceMacaroon);
|
||||
Assert.Null(expanded.Macaroons.ReadonlyMacaroon);
|
||||
|
||||
File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa });
|
||||
File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab });
|
||||
File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac });
|
||||
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
|
||||
Assert.NotNull(expanded.Macaroons.AdminMacaroon);
|
||||
Assert.NotNull(expanded.Macaroons.InvoiceMacaroon);
|
||||
Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex);
|
||||
Assert.Equal(0xab, expanded.Macaroons.InvoiceMacaroon.Bytes[0]);
|
||||
Assert.NotNull(expanded.Macaroons.ReadonlyMacaroon);
|
||||
|
||||
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error));
|
||||
File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken");
|
||||
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
|
||||
Assert.Equal("apitoken", expanded.APIToken);
|
||||
}
|
||||
|
||||
private string CreateDirectory([CallerMemberName] string caller = null)
|
||||
{
|
||||
var name = $"{caller}-{NBitcoin.RandomUtils.GetUInt32()}";
|
||||
Directory.CreateDirectory(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CheckRatesProvider()
|
||||
@ -2347,47 +2483,47 @@ donation:
|
||||
|
||||
var fetcher = new RateFetcher(factory);
|
||||
|
||||
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
|
||||
Thread.Sleep(3000);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
// Should cache at exchange level so this should hit the cache
|
||||
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules, default).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 = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).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();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
|
||||
fetch.UpdateIfNecessary(default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
|
||||
Thread.Sleep(1020);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
|
||||
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
|
||||
fetch.UpdateIfNecessary(default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetch.GetRatesAsync().GetAwaiter().GetResult();
|
||||
fetch.GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
Thread.Sleep(1000);
|
||||
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
|
||||
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
image: nicolasdorier/nbxplorer:2.0.0.15
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -119,7 +119,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -165,7 +165,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.71</Version>
|
||||
<Version>1.0.3.86</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -33,7 +33,7 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.9" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.14" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
@ -45,10 +45,10 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.86" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.5" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
|
@ -15,7 +15,6 @@ using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
@ -49,11 +48,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
@ -128,85 +123,7 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"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: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
else
|
||||
{
|
||||
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");
|
||||
|
||||
{
|
||||
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
|
||||
if (spark.Length != 0)
|
||||
{
|
||||
if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
$"Error: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
else
|
||||
{
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
|
||||
if (rtl.Length != 0)
|
||||
{
|
||||
if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
$"Error: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
else
|
||||
{
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
|
||||
if (charge.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
|
||||
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
|
||||
|
||||
if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
$"Error: {chargeError ?? string.Empty}" + Environment.NewLine +
|
||||
$"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
else
|
||||
{
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
|
||||
}
|
||||
}
|
||||
ExternalServices.Load(net.CryptoCode, conf);
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
@ -220,13 +137,18 @@ namespace BTCPayServer.Configuration
|
||||
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
|
||||
Link: p.p.Substring(p.SeparatorIndex + 1))))
|
||||
{
|
||||
ExternalServices.AddOrReplace(service.Name, service.Link);
|
||||
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
|
||||
OtherExternalServices.AddOrReplace(service.Name, uri);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
SocksEndpoint = conf.GetOrDefault<EndPoint>("socksendpoint", null);
|
||||
if (SocksEndpoint is Tor.OnionEndpoint)
|
||||
throw new ConfigException($"socksendpoint should not be a tor endpoint");
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
@ -325,9 +247,9 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
|
||||
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
@ -351,5 +273,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TorrcFile { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,12 @@ namespace BTCPayServer.Configuration
|
||||
return (T)(object)str;
|
||||
else if (typeof(T) == typeof(IPAddress))
|
||||
return (T)(object)IPAddress.Parse(str);
|
||||
else if (typeof(T) == typeof(EndPoint))
|
||||
{
|
||||
if (EndPointParser.TryParse(str, out var endpoint))
|
||||
return (T)(object)endpoint;
|
||||
throw new FormatException("Invalid endpoint");
|
||||
}
|
||||
else if (typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
|
@ -32,7 +32,6 @@ namespace BTCPayServer.Configuration
|
||||
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("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", 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);
|
||||
@ -41,6 +40,8 @@ namespace BTCPayServer.Configuration
|
||||
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("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalCharge : ExternalService
|
||||
{
|
||||
public ExternalCharge(LightningConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
public LightningConnectionString ConnectionString { get; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
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, string type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalRTL : ExternalService, IAccessKeyService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalRTL(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<string> ExtractAccessKey()
|
||||
{
|
||||
if (ConnectionString?.CookeFile == null)
|
||||
throw new FormatException("Invalid connection string");
|
||||
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public interface IAccessKeyService
|
||||
{
|
||||
SparkConnectionString ConnectionString { get; }
|
||||
Task<string> ExtractAccessKey();
|
||||
}
|
||||
public class ExternalSpark : ExternalService, IAccessKeyService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalSpark(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<string> ExtractAccessKey()
|
||||
{
|
||||
if (ConnectionString?.CookeFile == null)
|
||||
throw new FormatException("Invalid connection string");
|
||||
var cookie = (ConnectionString.CookeFile == "fake"
|
||||
? "fake:fake:fake" // Hacks for testing
|
||||
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
return cookie[2];
|
||||
}
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
}
|
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalConnectionString
|
||||
{
|
||||
public Uri Server { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public Macaroons Macaroons { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string MacaroonDirectoryPath { get; set; }
|
||||
public string APIToken { get; set; }
|
||||
public string CookieFilePath { get; set; }
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a connectionString which does not depends on external resources or information like relative path or file path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
|
||||
{
|
||||
var connectionString = this.Clone();
|
||||
// Transform relative URI into absolute URI
|
||||
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
|
||||
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
||||
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
|
||||
connectionString.MacaroonDirectoryPath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the MacaroonFilePath
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
isFake = connectionString.CookieFilePath == "fake";
|
||||
connectionString.CookieFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Charge)
|
||||
{
|
||||
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private Uri ToRelative(Uri absoluteUrlBase, string path)
|
||||
{
|
||||
if (path.StartsWith('/'))
|
||||
path = path.Substring(1);
|
||||
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
public ExternalConnectionString Clone()
|
||||
{
|
||||
return new ExternalConnectionString()
|
||||
{
|
||||
MacaroonFilePath = MacaroonFilePath,
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
Macaroon = Macaroon,
|
||||
MacaroonDirectoryPath = MacaroonDirectoryPath,
|
||||
Server = Server,
|
||||
APIToken = APIToken,
|
||||
CookieFilePath = CookieFilePath,
|
||||
AccessKey = AccessKey,
|
||||
Macaroons = Macaroons?.Clone()
|
||||
};
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new ExternalConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
|
||||
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
|
||||
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookieFilePath != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookieFilePath = kv[1];
|
||||
break;
|
||||
case "macaroondirectorypath":
|
||||
resultTemp.MacaroonDirectoryPath = kv[1];
|
||||
break;
|
||||
case "certthumbprint":
|
||||
resultTemp.CertificateThumbprint = kv[1];
|
||||
break;
|
||||
case "macaroonfilepath":
|
||||
resultTemp.MacaroonFilePath = kv[1];
|
||||
break;
|
||||
case "api-token":
|
||||
resultTemp.APIToken = kv[1];
|
||||
break;
|
||||
case "access-key":
|
||||
resultTemp.AccessKey = kv[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalServices : List<ExternalService>
|
||||
{
|
||||
public void Load(string cryptoCode, IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (gRPC server)");
|
||||
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (REST server)");
|
||||
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Spark server)");
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
|
||||
}
|
||||
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public ExternalServiceTypes Type { get; set; }
|
||||
public ExternalConnectionString ConnectionString { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServiceName { get; set; }
|
||||
}
|
||||
|
||||
public enum ExternalServiceTypes
|
||||
{
|
||||
LNDRest,
|
||||
LNDGRPC,
|
||||
Spark,
|
||||
RTL,
|
||||
Charge
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class SparkConnectionString
|
||||
{
|
||||
public Uri Server { get; private set; }
|
||||
public string CookeFile { get; private set; }
|
||||
|
||||
public static bool TryParse(string str, out SparkConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new SparkConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
|
||||
if(resultTemp.Server.Scheme == "http")
|
||||
{
|
||||
error = "Insecure transport protocol (http)";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookeFile != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookeFile = kv[1];
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,8 @@ namespace BTCPayServer.Controllers
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new BitpayHttpException(400, "The request body is missing");
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
if (string.IsNullOrEmpty(request.PairingCode))
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
@ -56,7 +57,9 @@ namespace BTCPayServer.Controllers
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
@ -90,6 +93,24 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
var parsedSounds = vm.Sounds.Split(
|
||||
new[] {"\r\n", "\r", "\n"},
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.SoundsEnabled && !parsedSounds.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
|
||||
}
|
||||
|
||||
var parsedAnimationColors = vm.AnimationColors.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -124,7 +145,9 @@ namespace BTCPayServer.Controllers
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity,
|
||||
Sounds = parsedSounds,
|
||||
AnimationColors = parsedAnimationColors
|
||||
};
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
|
@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -104,7 +104,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
|
@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
@ -108,11 +109,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
|
||||
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
|
||||
appInfo.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (settings.Enabled) return View(appInfo);
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(await _AppService.GetAppInfo(appId));
|
||||
return View(appInfo);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -120,7 +123,7 @@ namespace BTCPayServer.Controllers
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
@ -131,14 +134,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
if (!isAdmin)
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
if (!settings.Enabled && !isAdmin) {
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
|
||||
|
||||
info.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (!isAdmin &&
|
||||
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
||||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
|
||||
@ -186,7 +187,7 @@ namespace BTCPayServer.Controllers
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) }, cancellationToken: cancellationToken);
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
||||
@ -216,7 +217,7 @@ namespace BTCPayServer.Controllers
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey,
|
||||
string posData = null)
|
||||
string posData = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
@ -265,7 +266,7 @@ namespace BTCPayServer.Controllers
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -48,7 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
@ -102,11 +103,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
|
@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
@ -32,11 +33,11 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
{
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(400, "Invalid invoice");
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -273,6 +273,7 @@ namespace BTCPayServer.Controllers
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
@ -283,6 +284,7 @@ namespace BTCPayServer.Controllers
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
@ -345,9 +347,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
@ -542,7 +543,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
@ -590,7 +591,7 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
@ -62,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
@ -154,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
|
||||
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
@ -241,6 +242,7 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
paymentMethod.PreferOnion = this.Request.IsOnion();
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ArgumentNullException(nameof(directoryPath));
|
||||
Macaroons macaroons = new Macaroons();
|
||||
if (!Directory.Exists(directoryPath))
|
||||
return macaroons;
|
||||
throw new DirectoryNotFoundException("Macaroons directory not found");
|
||||
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
|
||||
{
|
||||
try
|
||||
@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return macaroons;
|
||||
}
|
||||
|
||||
public Macaroons Clone()
|
||||
{
|
||||
return new Macaroons()
|
||||
{
|
||||
AdminMacaroon = AdminMacaroon,
|
||||
InvoiceMacaroon = InvoiceMacaroon,
|
||||
ReadonlyMacaroon = ReadonlyMacaroon
|
||||
};
|
||||
}
|
||||
|
||||
public Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
@ -23,6 +24,7 @@ using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -97,7 +99,12 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a payment request"
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -175,7 +182,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove Payment Request",
|
||||
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
|
||||
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
@ -197,7 +204,7 @@ namespace BTCPayServer.Controllers
|
||||
new
|
||||
{
|
||||
StatusMessage =
|
||||
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -212,7 +219,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
return View(result);
|
||||
}
|
||||
|
||||
@ -220,14 +227,14 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{id}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||
decimal? amount = null)
|
||||
decimal? amount = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
@ -267,21 +274,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||
{
|
||||
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
|
||||
|
||||
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
|
||||
}
|
||||
amount = Math.Min(result.AmountDue, amount.Value);
|
||||
else
|
||||
amount = result.AmountDue;
|
||||
|
||||
|
||||
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
|
||||
bool redirectToInvoice,
|
||||
ViewPaymentRequestViewModel result,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
var blob = pr.GetBlob();
|
||||
var store = pr.StoreData;
|
||||
@ -291,14 +288,17 @@ namespace BTCPayServer.Controllers
|
||||
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
|
||||
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
Price = amount.GetValueOrDefault(result.AmountDue),
|
||||
FullNotifications = true,
|
||||
BuyerEmail = result.Email,
|
||||
RedirectURL = redirectUrl,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
Price = amount.Value,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = result.Email,
|
||||
RedirectURL = redirectUrl,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
|
||||
cancellationToken: cancellationToken))
|
||||
.Data.Id;
|
||||
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
@ -313,9 +313,28 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/clone")]
|
||||
public async Task<IActionResult> ClonePaymentRequest(string id)
|
||||
{
|
||||
var result = await EditPaymentRequest(id);
|
||||
if (result is ViewResult viewResult)
|
||||
{
|
||||
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
|
||||
model.Id = null;
|
||||
model.Title = $"Clone of {model.Title}";
|
||||
|
||||
return View("EditPaymentRequest", model);
|
||||
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
@ -28,7 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
@ -56,7 +57,7 @@ namespace BTCPayServer.Controllers
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
|
||||
network);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel()
|
||||
|
@ -13,6 +13,7 @@ using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
@ -64,7 +65,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
|
||||
|
||||
var result = await GetRates2(currencypairs, store.Id);
|
||||
var result = await GetRates2(currencypairs, store.Id, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -75,10 +76,10 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates/{baseCurrency}/{currency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -88,10 +89,9 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
@ -139,15 +139,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
|
||||
|
||||
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
@ -168,7 +163,7 @@ namespace BTCPayServer.Controllers
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules, cancellationToken);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
|
@ -26,7 +26,6 @@ using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -40,6 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
private readonly TorServices _torServices;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
@ -49,6 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
TorServices torServices,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_Options = options;
|
||||
@ -59,6 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
_torServices = torServices;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -385,18 +387,17 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (IsAdmin(roles))
|
||||
{
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
||||
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
||||
"Delete"));
|
||||
@ -429,13 +430,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("server/policies")]
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
@ -455,202 +449,147 @@ namespace BTCPayServer.Controllers
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
result.ExternalServices = _Options.ExternalServices;
|
||||
foreach (var externalService in _Options.OtherExternalServices)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = grpcService.Type,
|
||||
Action = nameof(LndServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
i = 0;
|
||||
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Spark server",
|
||||
Action = nameof(SparkService),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Ride the Lightning server (RTL)",
|
||||
Action = nameof(RTLService),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Lightning charge server",
|
||||
Action = nameof(LightningChargeServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
foreach (var externalService in _Options.ExternalServices)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
|
||||
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
if (_Options.SSHSettings != null)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
foreach(var torService in _torServices.Services)
|
||||
{
|
||||
if (torService.VirtualPort == 80)
|
||||
{
|
||||
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"http://{torService.OnionHost}"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
|
||||
});
|
||||
}
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (lightningCharge == null)
|
||||
{
|
||||
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
|
||||
vm.APIToken = lightningCharge.Password;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
|
||||
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
switch (service.Type)
|
||||
{
|
||||
if (lightningCharge.CookieFilePath != "fake")
|
||||
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
|
||||
else
|
||||
vm.APIToken = "fake";
|
||||
case ExternalServiceTypes.Charge:
|
||||
return LightningChargeServices(service, connectionString, showQR);
|
||||
case ExternalServiceTypes.RTL:
|
||||
case ExternalServiceTypes.Spark:
|
||||
if (connectionString.AccessKey == null)
|
||||
{
|
||||
StatusMessage = $"Error: The access key of the service is not set";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = service.DisplayName;
|
||||
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||
return View("LightningWalletServices", vm);
|
||||
case ExternalServiceTypes.LNDGRPC:
|
||||
case ExternalServiceTypes.LNDRest:
|
||||
return LndServices(service, connectionString, nonce);
|
||||
default:
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
}
|
||||
var builder = new UriBuilder(lightningCharge.ToUri(false));
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/services/spark/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
|
||||
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
|
||||
{
|
||||
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
|
||||
}
|
||||
[Route("server/services/rtl/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
|
||||
{
|
||||
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
|
||||
}
|
||||
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
|
||||
if (external == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = walletName;
|
||||
try
|
||||
{
|
||||
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View("LightningWalletServices", vm);
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = connectionString.Server.AbsoluteUri;
|
||||
vm.APIToken = connectionString.APIToken;
|
||||
var builder = new UriBuilder(connectionString.Server);
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
return View(nameof(LightningChargeServices), vm);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, 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();
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
|
||||
model.SSL = connectionString.Server.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
model.Uri = external.BaseUri.AbsoluteUri;
|
||||
model.Uri = connectionString.Server.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
if (external.CertificateThumbprint != null)
|
||||
if (connectionString.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
model.CertificateThumbprint = connectionString.CertificateThumbprint;
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
if (connectionString.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
}
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
|
||||
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
return View(nameof(LndServices), model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
@ -663,68 +602,62 @@ namespace BTCPayServer.Controllers
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
|
||||
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
|
||||
{
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
|
||||
ExternalConnectionString connectionString = null;
|
||||
try
|
||||
{
|
||||
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
LightningConfiguration grpcConf = new LightningConfiguration();
|
||||
grpcConf.Type = "grpc";
|
||||
grpcConf.Host = external.BaseUri.DnsSafeHost;
|
||||
grpcConf.Port = external.BaseUri.Port;
|
||||
grpcConf.SSL = external.BaseUri.Scheme == "https";
|
||||
grpcConf.Host = connectionString.Server.DnsSafeHost;
|
||||
grpcConf.Port = connectionString.Server.Port;
|
||||
grpcConf.SSL = connectionString.Server.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Uri = external.BaseUri.AbsoluteUri;
|
||||
restconf.Uri = connectionString.Server.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(external.ConnectionType.ToString());
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
|
||||
commonConf.ChainType = _Options.NetworkType.ToString();
|
||||
commonConf.CryptoCode = cryptoCode;
|
||||
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
|
||||
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
|
||||
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LndServices), 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;
|
||||
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
@ -739,15 +672,25 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
|
||||
var server = IsLocalNetwork(settings.Server) ? this.Request.Host.Host: settings.Server;
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static bool IsLocalNetwork(string server)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
@ -763,19 +706,28 @@ namespace BTCPayServer.Controllers
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
var client = model.Settings.CreateSmtpClient();
|
||||
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -77,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
vm.StatusMessage = "Test Successful";
|
||||
|
@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -188,24 +189,39 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
public IActionResult Rates(string storeId)
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = storeId;
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[] currencyPairs = null;
|
||||
try
|
||||
{
|
||||
currencyPairs = model.DefaultCurrencyPairs?
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => CurrencyPair.Parse(p))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -219,7 +235,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
blob.DefaultCurrencyPairs = currencyPairs;
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
@ -267,7 +283,7 @@ namespace BTCPayServer.Controllers
|
||||
pairs.Add(currencyPair);
|
||||
}
|
||||
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken);
|
||||
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||
foreach (var fetch in fetchs)
|
||||
{
|
||||
@ -332,13 +348,14 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
SetCryptoCurrencies(vm, StoreData);
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
return View(vm);
|
||||
}
|
||||
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
|
||||
@ -395,13 +412,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
|
@ -181,7 +181,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
|
||||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
|
@ -307,6 +307,25 @@ namespace BTCPayServer.Data
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
CurrencyPair[] _DefaultCurrencyPairs;
|
||||
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
|
||||
public CurrencyPair[] DefaultCurrencyPairs
|
||||
{
|
||||
get
|
||||
{
|
||||
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
|
||||
}
|
||||
set
|
||||
{
|
||||
_DefaultCurrencyPairs = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDefaultCurrencyPairString()
|
||||
{
|
||||
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
|
||||
}
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
@ -330,10 +349,11 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue OnChainMinValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
|
43
BTCPayServer/EndpointParser.cs
Normal file
43
BTCPayServer/EndpointParser.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Tor;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class EndPointParser
|
||||
{
|
||||
public static bool TryParse(string hostPort, out EndPoint endpoint)
|
||||
{
|
||||
if (hostPort == null)
|
||||
throw new ArgumentNullException(nameof(hostPort));
|
||||
endpoint = null;
|
||||
var index = hostPort.LastIndexOf(':');
|
||||
if (index == -1)
|
||||
return false;
|
||||
var portStr = hostPort.Substring(index + 1);
|
||||
if (!ushort.TryParse(portStr, out var port))
|
||||
return false;
|
||||
return TryParse(hostPort.Substring(0, index), port, out endpoint);
|
||||
}
|
||||
public static bool TryParse(string host, int port, out EndPoint endpoint)
|
||||
{
|
||||
if (host == null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
endpoint = null;
|
||||
if (IPAddress.TryParse(host, out var address))
|
||||
endpoint = new IPEndPoint(address, port);
|
||||
else if (host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
endpoint = new OnionEndpoint(host, port);
|
||||
else
|
||||
{
|
||||
if (Uri.CheckHostName(host) != UriHostNameType.Dns)
|
||||
return false;
|
||||
endpoint = new DnsEndPoint(host, port);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -17,7 +18,7 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
public ExplorerClientProvider(IHttpClientFactory httpClientFactory, BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
@ -32,14 +33,15 @@ namespace BTCPayServer
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
|
||||
if (setting.ExplorerUri != null)
|
||||
{
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
private static ExplorerClient CreateExplorerClient(HttpClient httpClient, BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
|
||||
explorer.SetClient(httpClient);
|
||||
if (cookieFile == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
|
||||
|
@ -33,6 +33,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -125,6 +126,12 @@ namespace BTCPayServer
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
public static string WithStartingSlash(this string str)
|
||||
{
|
||||
if (str.StartsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return $"/{str}";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
@ -159,6 +166,13 @@ namespace BTCPayServer
|
||||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
|
||||
}
|
||||
|
||||
public static bool IsOnion(this HttpRequest request)
|
||||
{
|
||||
if (request?.Host.Host == null)
|
||||
return false;
|
||||
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -211,8 +225,10 @@ namespace BTCPayServer
|
||||
/// <returns></returns>
|
||||
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
|
||||
{
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
|
||||
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
|
||||
uri.IsAbsoluteUri)
|
||||
return path;
|
||||
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
@ -228,6 +244,31 @@ namespace BTCPayServer
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will return an absolute URL.
|
||||
/// If `relativeOrAsbolute` is absolute, returns it.
|
||||
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="relativeOrAbsolte"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
|
||||
{
|
||||
if (relativeOrAbsolute == null)
|
||||
{
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()), UriKind.Absolute);
|
||||
}
|
||||
if (relativeOrAbsolute.IsAbsoluteUri)
|
||||
return relativeOrAbsolute;
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -282,6 +323,7 @@ namespace BTCPayServer
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
|
@ -45,10 +45,12 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
public bool DiscourageSearchEngines { get; set; }
|
||||
|
||||
internal void Update(PoliciesSettings data)
|
||||
{
|
||||
ShowRegister = !data.LockSubscription;
|
||||
DiscourageSearchEngines = data.DiscourageSearchEngines;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
|
||||
{
|
||||
return _Summaries.TryGetValue(cryptoCode, out summary) &&
|
||||
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
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 =>
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
||||
{
|
||||
if (t.Result.Exception != null)
|
||||
{
|
||||
|
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class TorServicesHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
private readonly TorServices _torServices;
|
||||
|
||||
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
|
||||
{
|
||||
_options = options;
|
||||
_torServices = torServices;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
|
||||
if (string.IsNullOrEmpty(_options.TorrcFile))
|
||||
return Array.Empty<Task>();
|
||||
return new Task[] { CreateLoopTask(RefreshTorServices) };
|
||||
}
|
||||
|
||||
async Task RefreshTorServices()
|
||||
{
|
||||
await _torServices.Refresh();
|
||||
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
@ -62,6 +62,8 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<TorServices>();
|
||||
services.TryAddSingleton<SocketFactory>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<InvoiceRepository>(o =>
|
||||
@ -188,6 +190,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
|
@ -32,8 +32,6 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
RewriteHostIfNeeded(httpContext);
|
||||
|
||||
try
|
||||
{
|
||||
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||
@ -125,57 +123,6 @@ namespace BTCPayServer.Hosting
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
reverseProxyScheme = scheme;
|
||||
}
|
||||
}
|
||||
|
||||
ushort? reverseProxyPort = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
reverseProxyPort = pp;
|
||||
}
|
||||
}
|
||||
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = ex.StatusCode;
|
||||
|
@ -3,7 +3,6 @@ using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -14,9 +13,9 @@ using BTCPayServer.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
@ -159,14 +158,22 @@ namespace BTCPayServer.Hosting
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
var forwardingOptions = new ForwardedHeadersOptions()
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
};
|
||||
forwardingOptions.KnownNetworks.Clear();
|
||||
forwardingOptions.KnownProxies.Clear();
|
||||
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
|
||||
app.UseForwardedHeaders(forwardingOptions);
|
||||
app.UseCors();
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
route.MapHub<AppHub>("/apps/hub");
|
||||
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
|
||||
AppHub.Register(route);
|
||||
PaymentRequestHub.Register(route);
|
||||
});
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
|
39
BTCPayServer/JsonConverters/CurrencyPairJsonConverter.cs
Normal file
39
BTCPayServer/JsonConverters/CurrencyPairJsonConverter.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class CurrencyPairJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
CurrencyPair.TryParse((string)reader.Value, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid currency pair", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid currency pair", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
99
BTCPayServer/ModelBinders/InvariantDecimalModelBinder.cs
Normal file
99
BTCPayServer/ModelBinders/InvariantDecimalModelBinder.cs
Normal file
@ -0,0 +1,99 @@
|
||||
// Copied and adjusted from https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DecimalModelBinder.cs
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace BTCPayServer.ModelBinders
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
|
||||
/// <see cref="decimal"/>.
|
||||
/// </summary>
|
||||
public class InvariantDecimalModelBinder : IModelBinder
|
||||
{
|
||||
private readonly NumberStyles _supportedStyles;
|
||||
|
||||
public InvariantDecimalModelBinder()
|
||||
{
|
||||
_supportedStyles = NumberStyles.Any;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
}
|
||||
|
||||
var modelName = bindingContext.ModelName;
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
|
||||
if (valueProviderResult == ValueProviderResult.None)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var modelState = bindingContext.ModelState;
|
||||
modelState.SetModelValue(modelName, valueProviderResult);
|
||||
|
||||
var metadata = bindingContext.ModelMetadata;
|
||||
var type = metadata.UnderlyingOrModelType;
|
||||
try
|
||||
{
|
||||
var value = valueProviderResult.FirstValue;
|
||||
var culture = CultureInfo.InvariantCulture;
|
||||
|
||||
object model;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
|
||||
model = null;
|
||||
}
|
||||
else if (type == typeof(decimal))
|
||||
{
|
||||
model = decimal.Parse(value, _supportedStyles, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// unreachable
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// When converting value, a null model may indicate a failed conversion for an otherwise required
|
||||
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
|
||||
// current bindingContext. If not, an error is logged.
|
||||
if (model == null && !metadata.IsReferenceOrNullableType)
|
||||
{
|
||||
modelState.TryAddModelError(
|
||||
modelName,
|
||||
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
|
||||
valueProviderResult.ToString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var isFormatException = exception is FormatException;
|
||||
if (!isFormatException && exception.InnerException != null)
|
||||
{
|
||||
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
|
||||
// this code in case a cursory review of the CoreFx code missed something.
|
||||
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
|
||||
}
|
||||
|
||||
modelState.TryAddModelError(modelName, exception, metadata);
|
||||
|
||||
// Conversion failed.
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string TargetCurrency { get; set; } = "BTC";
|
||||
|
||||
[Display(Name = "Set a Target amount ")]
|
||||
[Range(0, double.PositiveInfinity)]
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
|
||||
@ -79,5 +80,11 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
[Display(Name = "Display contribution ranking")]
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
|
||||
|
||||
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
|
||||
public string Sounds{ get; set; }
|
||||
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
|
||||
public string AnimationColors{ get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
public class ViewCrowdfundViewModel
|
||||
{
|
||||
public string HubPath { get; set; }
|
||||
public string StatusMessage{ get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string AppId { get; set; }
|
||||
@ -30,6 +33,8 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public bool SoundsEnabled { get; set; }
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; }
|
||||
public string[] AnimationColors { get; set; }
|
||||
public string[] Sounds { get; set; }
|
||||
public int ResetEveryAmount { get; set; }
|
||||
public bool NeverReset { get; set; }
|
||||
|
||||
@ -50,11 +55,26 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public DateTime? LastResetDate { get; set; }
|
||||
public DateTime? NextResetDate { get; set; }
|
||||
}
|
||||
public class Contribution
|
||||
{
|
||||
public PaymentMethodId PaymentMehtodId { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public decimal CurrencyValue { get; set; }
|
||||
}
|
||||
public class Contributions : Dictionary<PaymentMethodId, Contribution>
|
||||
{
|
||||
public Contributions(IEnumerable<KeyValuePair<PaymentMethodId, Contribution>> collection) : base(collection)
|
||||
{
|
||||
TotalCurrency = Values.Select(v => v.CurrencyValue).Sum();
|
||||
}
|
||||
public decimal TotalCurrency { get; }
|
||||
}
|
||||
|
||||
public bool Started => !StartDate.HasValue || DateTime.Now.ToUniversalTime() > StartDate;
|
||||
|
||||
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
|
@ -20,6 +20,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string DefaultLang { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public bool IsModal { get; set; }
|
||||
public bool IsLightning { get; set; }
|
||||
@ -64,5 +65,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public bool CoinSwitchEnabled { get; set; }
|
||||
public string CoinSwitchMode { get; set; }
|
||||
public string CoinSwitchMerchantId { get; set; }
|
||||
public string RootPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
|
||||
public string Id { get; set; }
|
||||
[Required] public string StoreId { get; set; }
|
||||
[Required] public decimal Amount { get; set; }
|
||||
[Required][Range(0, double.PositiveInfinity)]public decimal Amount { get; set; }
|
||||
|
||||
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]
|
||||
public string Currency { get; set; }
|
||||
@ -134,12 +134,14 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
public string AmountCollectedFormatted { get; set; }
|
||||
public string AmountFormatted { get; set; }
|
||||
public bool AnyPendingInvoice { get; set; }
|
||||
public string HubPath { get; set; }
|
||||
|
||||
public class PaymentRequestInvoice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTime ExpiryDate { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string AmountFormatted { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
||||
public List<PaymentRequestInvoicePayment> Payments { get; set; }
|
||||
|
@ -18,8 +18,9 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[EmailAddress]
|
||||
[Display(Name = "Test Email")]
|
||||
public string TestEmail
|
||||
{
|
||||
get; set;
|
||||
|
@ -2,27 +2,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class ServicesViewModel
|
||||
{
|
||||
public class LNDServiceViewModel
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string Type { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Action { get; internal set; }
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
public class OtherExternalService
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
|
||||
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
|
||||
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
|
||||
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
|
||||
public List<OtherExternalService> TorHttpServices { get; set; } = new List<OtherExternalService>();
|
||||
public List<OtherExternalService> TorOtherServices { get; set; } = new List<OtherExternalService>();
|
||||
}
|
||||
}
|
||||
|
@ -25,19 +25,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string DefaultPaymentMethod { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Requires a refund email")]
|
||||
public bool RequiresRefundEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
|
||||
[MaxLength(20)]
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
||||
[Display(Name = "Link to a custom CSS stylesheet")]
|
||||
[Uri]
|
||||
@ -49,9 +36,23 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Custom HTML title to display on Checkout page")]
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
[Display(Name = "Requires a refund email")]
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
|
||||
[MaxLength(20)]
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Display lightning payment amounts in Satoshis")]
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
public void SetLanguages(LanguageService langService, string defaultLang)
|
||||
{
|
||||
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";
|
||||
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
|
||||
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
|
||||
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
|
@ -3,11 +3,14 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class PayButtonViewModel
|
||||
{
|
||||
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
|
||||
public decimal Price { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
[Required]
|
||||
|
@ -42,6 +42,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string Script { get; set; }
|
||||
public string DefaultScript { get; set; }
|
||||
public string ScriptTest { get; set; }
|
||||
public string DefaultCurrencyPairs { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Add a spread on exchange rate of ... %")]
|
||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
@ -59,6 +60,15 @@ namespace BTCPayServer.PaymentRequest
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetHubPath(HttpRequest request)
|
||||
{
|
||||
return request.GetRelativePathOrAbsolute("/payment-requests/hub");
|
||||
}
|
||||
public static void Register(HubRouteBuilder route)
|
||||
{
|
||||
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentRequestStreamer : EventHostedServiceBase
|
||||
|
@ -51,10 +51,8 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||
var amountCollected =
|
||||
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||
if (amountCollected >= blob.Amount)
|
||||
var contributions = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
if (contributions.TotalCurrency >= blob.Amount)
|
||||
{
|
||||
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
|
||||
}
|
||||
@ -80,17 +78,14 @@ namespace BTCPayServer.PaymentRequest
|
||||
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||
|
||||
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||
var amountCollected =
|
||||
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||
|
||||
var amountDue = blob.Amount - amountCollected;
|
||||
var paymentStats = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
||||
|
||||
return new ViewPaymentRequestViewModel(pr)
|
||||
{
|
||||
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
|
||||
AmountCollected = amountCollected,
|
||||
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
|
||||
AmountCollected = paymentStats.TotalCurrency,
|
||||
AmountCollectedFormatted = _currencies.FormatCurrency(paymentStats.TotalCurrency, blob.Currency),
|
||||
AmountDue = amountDue,
|
||||
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||
@ -100,6 +95,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
Id = entity.Id,
|
||||
Amount = entity.ProductInformation.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||
Status = entity.GetInvoiceState().ToString(),
|
||||
|
@ -19,11 +19,11 @@ namespace BTCPayServer.Payments.Changelly
|
||||
private readonly bool _showFiat;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
{
|
||||
_apisecret = apiSecret;
|
||||
_showFiat = showFiat;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
_httpClient = httpClient;
|
||||
_httpClient.BaseAddress = new Uri(apiUrl);
|
||||
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ namespace BTCPayServer.Payments.Changelly
|
||||
throw new ChangellyException("Changelly not enabled for this store");
|
||||
}
|
||||
|
||||
var changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl, changellySettings.ShowFiat);
|
||||
_clientCache.AddOrReplace(storeId, changelly);
|
||||
return changelly;
|
||||
|
@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tor;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -17,15 +19,19 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public static int LIGHTNING_TIMEOUT = 5000;
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
private readonly SocketFactory _socketFactory;
|
||||
|
||||
public LightningLikePaymentHandler(
|
||||
NBXplorerDashboard dashboard)
|
||||
NBXplorerDashboard dashboard,
|
||||
SocketFactory socketFactory)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_socketFactory = socketFactory;
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var test = GetNodeInfo(supportedPaymentMethod, network);
|
||||
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
@ -63,7 +69,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
public async Task<NodeInfo> GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
@ -84,8 +90,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
|
||||
if (info.NodeInfo == null)
|
||||
var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault();
|
||||
if (nodeInfo == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
@ -96,7 +102,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
|
||||
return info.NodeInfo;
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,22 +110,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
try
|
||||
{
|
||||
IPAddress address = null;
|
||||
try
|
||||
{
|
||||
address = IPAddress.Parse(nodeInfo.Host);
|
||||
}
|
||||
catch
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
|
||||
}
|
||||
if (!EndPointParser.TryParse(nodeInfo.Host, nodeInfo.Port, out var endpoint))
|
||||
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
|
||||
|
||||
if (address == null)
|
||||
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
|
||||
|
||||
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
using (var tcp = await _socketFactory.ConnectAsync(endpoint, SocketType.Stream, ProtocolType.Tcp, cancellation))
|
||||
{
|
||||
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -192,7 +192,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
BOLT11 = notification.BOLT11,
|
||||
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
|
||||
Amount = notification.Amount
|
||||
Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable
|
||||
}, network, accounted: true);
|
||||
if (payment != null)
|
||||
{
|
||||
|
@ -51,6 +51,7 @@ namespace BTCPayServer
|
||||
.ConfigureLogging(l =>
|
||||
{
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
||||
l.AddProvider(new CustomConsoleLogProvider(processor));
|
||||
|
||||
|
@ -30,8 +30,8 @@
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -40,7 +41,7 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
|
||||
var result =
|
||||
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
|
||||
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted);
|
||||
switch (result)
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
@ -62,5 +63,13 @@ namespace BTCPayServer.Services.Apps
|
||||
|
||||
}
|
||||
|
||||
public static string GetHubPath(HttpRequest request)
|
||||
{
|
||||
return request.GetRelativePathOrAbsolute("/apps/hub");
|
||||
}
|
||||
public static void Register(HubRouteBuilder route)
|
||||
{
|
||||
route.MapHub<AppHub>("/apps/hub");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
using static BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
@ -33,7 +34,6 @@ namespace BTCPayServer.Services.Apps
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
CurrencyNameTable _Currencies;
|
||||
private readonly RateFetcher _RateFetcher;
|
||||
private readonly HtmlSanitizer _HtmlSanitizer;
|
||||
private readonly BTCPayNetworkProvider _Networks;
|
||||
public CurrencyNameTable Currencies => _Currencies;
|
||||
@ -41,13 +41,11 @@ namespace BTCPayServer.Services.Apps
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networks,
|
||||
CurrencyNameTable currencies,
|
||||
RateFetcher rateFetcher,
|
||||
HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_Currencies = currencies;
|
||||
_RateFetcher = rateFetcher;
|
||||
_HtmlSanitizer = htmlSanitizer;
|
||||
_Networks = networks;
|
||||
}
|
||||
@ -94,17 +92,8 @@ namespace BTCPayServer.Services.Apps
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed)).ToArray();
|
||||
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
|
||||
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.EnforceTargetAmount);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.EnforceTargetAmount);
|
||||
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
paymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||
pendingPaymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
||||
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
||||
|
||||
var perkCount = invoices
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
|
||||
@ -140,6 +129,7 @@ namespace BTCPayServer.Services.Apps
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = perks,
|
||||
Enabled = settings.Enabled,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
@ -148,19 +138,21 @@ namespace BTCPayServer.Services.Apps
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
PerkCount = perkCount,
|
||||
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
|
||||
Sounds = settings.Sounds,
|
||||
AnimationColors = settings.AnimationColors,
|
||||
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = invoices.Length,
|
||||
CurrentPendingAmount = currentPendingAmount,
|
||||
CurrentAmount = currentAmount,
|
||||
ProgressPercentage = (currentAmount / settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = (currentPendingAmount / settings.TargetAmount) * 100,
|
||||
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
|
||||
LastUpdated = DateTime.Now,
|
||||
PaymentStats = paymentStats,
|
||||
PendingPaymentStats = pendingPaymentStats,
|
||||
PaymentStats = currentPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
||||
PendingPaymentStats = pendingPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
||||
LastResetDate = lastResetDate,
|
||||
NextResetDate = nextResetDate
|
||||
NextResetDate = nextResetDate,
|
||||
CurrentPendingAmount = pendingPayments.TotalCurrency,
|
||||
CurrentAmount = currentPayments.TotalCurrency
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -288,46 +280,20 @@ namespace BTCPayServer.Services.Apps
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
|
||||
public Contributions GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
var result = new List<decimal>();
|
||||
|
||||
var ratesTask = _RateFetcher.FetchRates(
|
||||
stats.Keys
|
||||
.Select((x) => new CurrencyPair(primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
|
||||
.Distinct()
|
||||
.ToHashSet(),
|
||||
rateRules).Select(async rateTask =>
|
||||
{
|
||||
var (key, value) = rateTask;
|
||||
var tResult = await value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null)
|
||||
return;
|
||||
|
||||
foreach (var stat in stats)
|
||||
{
|
||||
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, key.Right,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result.Add((1m / rate.Value) * stat.Value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(ratesTask);
|
||||
|
||||
return result.Sum();
|
||||
}
|
||||
|
||||
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
return invoices
|
||||
var contributions = invoices
|
||||
.Where(p => p.ProductInformation.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(p =>
|
||||
{
|
||||
var contribution = new Contribution();
|
||||
contribution.PaymentMehtodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike);
|
||||
contribution.CurrencyValue = p.ProductInformation.Price;
|
||||
contribution.Value = contribution.CurrencyValue;
|
||||
|
||||
// For hardcap, we count newly created invoices as part of the contributions
|
||||
if (!softcap && p.Status == InvoiceStatus.New)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
|
||||
return new[] { contribution };
|
||||
|
||||
// If the user get a donation via other mean, he can register an invoice manually for such amount
|
||||
// then mark the invoice as complete
|
||||
@ -335,20 +301,38 @@ namespace BTCPayServer.Services.Apps
|
||||
if (payments.Count == 0 &&
|
||||
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||
p.Status == InvoiceStatus.Complete)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
|
||||
return new[] { contribution };
|
||||
|
||||
contribution.CurrencyValue = 0m;
|
||||
contribution.Value = 0m;
|
||||
|
||||
// If an invoice has been marked invalid, remove the contribution
|
||||
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||
p.Status == InvoiceStatus.Invalid)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: 0m) };
|
||||
return new[] { contribution };
|
||||
|
||||
|
||||
// Else, we just sum the payments
|
||||
return payments
|
||||
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
|
||||
.Select(pay =>
|
||||
{
|
||||
var paymentMethodContribution = new Contribution();
|
||||
paymentMethodContribution.PaymentMehtodId = pay.GetPaymentMethodId();
|
||||
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId, _Networks).Rate;
|
||||
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
|
||||
return paymentMethodContribution;
|
||||
})
|
||||
.ToArray();
|
||||
})
|
||||
.GroupBy(p => p.Key)
|
||||
.ToDictionary(p => p.Key, p => p.Select(v => v.Value).Sum());
|
||||
.GroupBy(p => p.PaymentMehtodId)
|
||||
.ToDictionary(p => p.Key, p => new Contribution()
|
||||
{
|
||||
PaymentMehtodId = p.Key,
|
||||
Value = p.Select(v => v.Value).Sum(),
|
||||
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
|
||||
});
|
||||
return new Contributions(contributions);
|
||||
}
|
||||
|
||||
private class PosHolder
|
||||
|
@ -34,6 +34,54 @@ namespace BTCPayServer.Services.Apps
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
public string[] AnimationColors { get; set; } = new string[]
|
||||
{
|
||||
"#FF6138", "#FFBE53", "#2980B9", "#282741"
|
||||
};
|
||||
|
||||
public string[] Sounds { get; set; } = new string[]
|
||||
{
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/dominating.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/doublekill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/doublekill2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/eagleeye.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/flawless.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/godlike.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/hattrick.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headhunter.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/holyshit.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/killingspree.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ludicrouskill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/megakill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/monsterkill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/multikill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/nade.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ownage.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/payback.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare4.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/rampage.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide4.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/teamkiller.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/triplekill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ultrakill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav"
|
||||
};
|
||||
}
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
|
@ -13,11 +13,11 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
public class BTCPayServerEnvironment
|
||||
{
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
|
||||
IHttpContextAccessor httpContext;
|
||||
TorServices torServices;
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext, TorServices torServices)
|
||||
{
|
||||
ExpectedHost = httpContext.HttpContext.Request.Host.Value;
|
||||
ExpectedDomain = httpContext.HttpContext.Request.Host.Host;
|
||||
ExpectedProtocol = httpContext.HttpContext.Request.Scheme;
|
||||
this.httpContext = httpContext;
|
||||
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
|
||||
#if DEBUG
|
||||
Build = "Debug";
|
||||
@ -26,15 +26,18 @@ namespace BTCPayServer.Services
|
||||
#endif
|
||||
Environment = env;
|
||||
NetworkType = provider.NetworkType;
|
||||
this.torServices = torServices;
|
||||
}
|
||||
public IHostingEnvironment Environment
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ExpectedDomain { get; set; }
|
||||
public string ExpectedHost { get; set; }
|
||||
public string ExpectedProtocol { get; set; }
|
||||
public string ExpectedDomain => httpContext.HttpContext.Request.Host.Host;
|
||||
public string ExpectedHost => httpContext.HttpContext.Request.Host.Value;
|
||||
public string ExpectedProtocol => httpContext.HttpContext.Request.Scheme;
|
||||
public string OnionUrl => this.torServices.Services.Where(s => s.ServiceType == TorServiceType.BTCPayServer)
|
||||
.Select(s => $"http://{s.OnionHost}").FirstOrDefault();
|
||||
|
||||
public NetworkType NetworkType { get; set; }
|
||||
public string Version
|
||||
|
@ -717,6 +717,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
[Obsolete("Use GetId().PaymentType instead")]
|
||||
public string PaymentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// We only use this to pass a singleton asking to the payment handler to prefer payments through TOR, we don't really
|
||||
/// need to save this information
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool PreferOnion { get; set; }
|
||||
|
||||
public PaymentMethodId GetId()
|
||||
{
|
||||
|
@ -35,6 +35,7 @@ namespace BTCPayServer.Services.Mails
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Enable SSL")]
|
||||
public bool EnableSSL
|
||||
{
|
||||
get; set;
|
||||
|
@ -18,5 +18,9 @@ namespace BTCPayServer.Services
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Disable registration")]
|
||||
public bool LockSubscription { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Discourage search engines from indexing this site")]
|
||||
public bool DiscourageSearchEngines { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -93,13 +94,13 @@ namespace BTCPayServer.Services.Rates
|
||||
public bool DoNotAutoFetchIfExpired { get; set; }
|
||||
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
|
||||
|
||||
public async Task<LatestFetch> UpdateIfNecessary()
|
||||
public async Task<LatestFetch> UpdateIfNecessary(CancellationToken cancellationToken)
|
||||
{
|
||||
if (NextUpdate <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Fetch();
|
||||
await Fetch(cancellationToken);
|
||||
}
|
||||
catch { } // Exception is inside _Latest
|
||||
return _Latest;
|
||||
@ -108,7 +109,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
LatestFetch _Latest;
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var latest = _Latest;
|
||||
if (!DoNotAutoFetchIfExpired && latest != null && latest.Expiration <= DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1.0))
|
||||
@ -116,7 +117,7 @@ namespace BTCPayServer.Services.Rates
|
||||
Logs.PayServer.LogWarning($"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this.");
|
||||
latest = null;
|
||||
}
|
||||
return (latest ?? (await Fetch())).GetResult();
|
||||
return (latest ?? (await Fetch(cancellationToken))).GetResult();
|
||||
}
|
||||
|
||||
private string GetExchangeName()
|
||||
@ -126,14 +127,14 @@ namespace BTCPayServer.Services.Rates
|
||||
return "???";
|
||||
}
|
||||
|
||||
private async Task<LatestFetch> Fetch()
|
||||
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken)
|
||||
{
|
||||
var previous = _Latest;
|
||||
var fetch = new LatestFetch();
|
||||
fetch.ExchangeName = GetExchangeName();
|
||||
try
|
||||
{
|
||||
var rates = await _Inner.GetRatesAsync();
|
||||
var rates = await _Inner.GetRatesAsync(cancellationToken);
|
||||
fetch.Latest = rates;
|
||||
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
|
||||
fetch.NextRefresh = DateTimeOffset.UtcNow + RefreshRate;
|
||||
|
@ -6,6 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -22,7 +23,7 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public string ExchangeName => BitpayName;
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
.AllRates
|
||||
|
29
BTCPayServer/Services/Rates/ByllsRateProvider.cs
Normal file
29
BTCPayServer/Services/Rates/ByllsRateProvider.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ByllsRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public ByllsRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bylls";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["public_price"]["to_price"].Value<decimal>();
|
||||
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -40,12 +41,12 @@ namespace BTCPayServer.Services.Rates
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
|
||||
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync();
|
||||
return _Inner.GetRatesAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -65,7 +66,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
set
|
||||
{
|
||||
_LocalClient = null;
|
||||
_LocalClient = value;
|
||||
}
|
||||
}
|
||||
HttpClient _LocalClient;
|
||||
@ -111,7 +112,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await HttpClient.SendAsync(request);
|
||||
var resp = await HttpClient.SendAsync(request, cancellationToken);
|
||||
using (resp)
|
||||
{
|
||||
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -31,7 +32,7 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public string ExchangeName => _ExchangeName;
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = await _ExchangeAPI.GetTickersAsync();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -16,13 +17,17 @@ namespace BTCPayServer.Services.Rates
|
||||
_Providers = providers;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var p in _Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await p.GetRatesAsync().ConfigureAwait(false);
|
||||
return await p.GetRatesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch(Exception ex) { Exceptions.Add(ex); }
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -8,6 +9,6 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface IRateProvider
|
||||
{
|
||||
Task<ExchangeRates> GetRatesAsync();
|
||||
Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -28,7 +29,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
set
|
||||
{
|
||||
_LocalClient = null;
|
||||
_LocalClient = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +56,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{ "ZCAD", "CAD" },
|
||||
};
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new ExchangeRates();
|
||||
var symbols = await GetSymbolsAsync();
|
||||
@ -135,7 +136,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null)
|
||||
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append("https://api.kraken.com");
|
||||
@ -147,7 +148,7 @@ namespace BTCPayServer.Services.Rates
|
||||
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
|
||||
}
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
|
||||
var response = await HttpClient.SendAsync(request);
|
||||
var response = await HttpClient.SendAsync(request, cancellationToken);
|
||||
string stringResult = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<T>(stringResult);
|
||||
if (result is JToken json)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -20,7 +21,7 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Instance;
|
||||
}
|
||||
}
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new ExchangeRates());
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -31,9 +32,9 @@ namespace BTCPayServer.Services.Rates
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all", cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -38,12 +39,12 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public RateProviderFactory RateProviderFactory => _rateProviderFactory;
|
||||
|
||||
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
|
||||
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules, CancellationToken cancellationToken)
|
||||
{
|
||||
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
|
||||
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules, cancellationToken).First().Value;
|
||||
}
|
||||
|
||||
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
|
||||
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules, CancellationToken cancellationToken)
|
||||
{
|
||||
if (rules == null)
|
||||
throw new ArgumentNullException(nameof(rules));
|
||||
@ -59,7 +60,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
|
||||
{
|
||||
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange);
|
||||
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, cancellationToken);
|
||||
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
|
||||
}
|
||||
dependentQueries.Add(fetching);
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -21,12 +22,12 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
try
|
||||
{
|
||||
return await _inner.GetRatesAsync();
|
||||
return await _inner.GetRatesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -109,8 +110,9 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
// Handmade providers
|
||||
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
|
||||
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||
@ -167,18 +169,19 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
// Add other exchanges supported here
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
|
||||
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
|
||||
public async Task<QueryRateResult> QueryRates(string exchangeName)
|
||||
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
|
||||
{
|
||||
Providers.TryGetValue(exchangeName, out var directProvider);
|
||||
directProvider = directProvider ?? NullRateProvider.Instance;
|
||||
|
||||
var wrapper = new WrapperRateProvider(directProvider);
|
||||
var value = await wrapper.GetRatesAsync();
|
||||
var value = await wrapper.GetRatesAsync(cancellationToken);
|
||||
return new QueryRateResult()
|
||||
{
|
||||
Latency = wrapper.Latency,
|
||||
|
76
BTCPayServer/Services/SocketFactory.cs
Normal file
76
BTCPayServer/Services/SocketFactory.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Tor;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class SocketFactory
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
public SocketFactory(BTCPayServerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
public async Task<Socket> ConnectAsync(EndPoint endPoint, SocketType socketType, ProtocolType protocolType, CancellationToken cancellationToken)
|
||||
{
|
||||
Socket socket = null;
|
||||
try
|
||||
{
|
||||
if (endPoint is IPEndPoint ipEndpoint)
|
||||
{
|
||||
socket = new Socket(ipEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(ipEndpoint).WithCancellation(cancellationToken);
|
||||
}
|
||||
else if (endPoint is OnionEndpoint onionEndpoint)
|
||||
{
|
||||
if (_options.SocksEndpoint == null)
|
||||
throw new NotSupportedException("It is impossible to connect to an onion address without btcpay's -socksendpoint configured");
|
||||
socket = await Socks5Connect.ConnectSocksAsync(_options.SocksEndpoint, onionEndpoint, cancellationToken);
|
||||
}
|
||||
else if (endPoint is DnsEndPoint dnsEndPoint)
|
||||
{
|
||||
var address = (await Dns.GetHostAddressesAsync(dnsEndPoint.Host)).FirstOrDefault();
|
||||
socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(dnsEndPoint).WithCancellation(cancellationToken);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException("Endpoint type not supported");
|
||||
}
|
||||
catch
|
||||
{
|
||||
CloseSocket(ref socket);
|
||||
throw;
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
private void CloseSocket(ref Socket s)
|
||||
{
|
||||
if (s == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
s.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
BTCPayServer/Services/TorServices.cs
Normal file
90
BTCPayServer/Services/TorServices.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class TorServices
|
||||
{
|
||||
BTCPayServerOptions _Options;
|
||||
public TorServices(BTCPayServerOptions options)
|
||||
{
|
||||
_Options = options;
|
||||
}
|
||||
|
||||
public TorService[] Services { get; internal set; } = Array.Empty<TorService>();
|
||||
|
||||
|
||||
internal async Task Refresh()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_Options.TorrcFile) || !File.Exists(_Options.TorrcFile))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_Options.TorrcFile))
|
||||
Logs.PayServer.LogWarning("Torrc file is not found");
|
||||
Services = Array.Empty<TorService>();
|
||||
return;
|
||||
}
|
||||
List<TorService> result = new List<TorService>();
|
||||
try
|
||||
{
|
||||
var torrcContent = await File.ReadAllTextAsync(_Options.TorrcFile);
|
||||
if (!Torrc.TryParse(torrcContent, out var torrc))
|
||||
{
|
||||
Logs.PayServer.LogWarning("Torrc file could not be parsed");
|
||||
Services = Array.Empty<TorService>();
|
||||
return;
|
||||
}
|
||||
|
||||
var services = torrc.ServiceDirectories.SelectMany(d => d.ServicePorts.Select(p => (Directory: new DirectoryInfo(d.DirectoryPath), VirtualPort: p.VirtualPort)))
|
||||
.Select(d => (ServiceName: d.Directory.Name,
|
||||
ReadingLines: System.IO.File.ReadAllLinesAsync(Path.Combine(d.Directory.FullName, "hostname")),
|
||||
VirtualPort: d.VirtualPort))
|
||||
.ToArray();
|
||||
foreach (var service in services)
|
||||
{
|
||||
try
|
||||
{
|
||||
var onionHost = (await service.ReadingLines)[0].Trim();
|
||||
var torService = new TorService()
|
||||
{
|
||||
Name = service.ServiceName,
|
||||
OnionHost = onionHost,
|
||||
VirtualPort = service.VirtualPort
|
||||
};
|
||||
if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase))
|
||||
torService.ServiceType = TorServiceType.BTCPayServer;
|
||||
result.Add(torService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Error while reading hidden service {service.ServiceName} configuration");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Error while reading torrc file");
|
||||
}
|
||||
Services = result.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class TorService
|
||||
{
|
||||
public TorServiceType ServiceType { get; set; } = TorServiceType.Other;
|
||||
public string Name { get; set; }
|
||||
public string OnionHost { get; set; }
|
||||
public int VirtualPort { get; set; }
|
||||
}
|
||||
|
||||
public enum TorServiceType
|
||||
{
|
||||
BTCPayServer,
|
||||
Other
|
||||
}
|
||||
}
|
100
BTCPayServer/Services/Torrc.cs
Normal file
100
BTCPayServer/Services/Torrc.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Torrc
|
||||
{
|
||||
public static bool TryParse(string str, out Torrc value)
|
||||
{
|
||||
value = null;
|
||||
List<HiddenServiceDir> serviceDirectories = new List<HiddenServiceDir>();
|
||||
var lines = str.Split(new char[] { '\n' });
|
||||
HiddenServiceDir currentDirectory = null;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (HiddenServiceDir.TryParse(line, out var dir))
|
||||
{
|
||||
serviceDirectories.Add(dir);
|
||||
currentDirectory = dir;
|
||||
}
|
||||
else if (HiddenServicePortDefinition.TryParse(line, out var portDef) && currentDirectory != null)
|
||||
{
|
||||
currentDirectory.ServicePorts.Add(portDef);
|
||||
}
|
||||
}
|
||||
value = new Torrc() { ServiceDirectories = serviceDirectories };
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<HiddenServiceDir> ServiceDirectories { get; set; } = new List<HiddenServiceDir>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach(var serviceDir in ServiceDirectories)
|
||||
{
|
||||
builder.AppendLine(serviceDir.ToString());
|
||||
foreach (var port in serviceDir.ServicePorts)
|
||||
builder.AppendLine(port.ToString());
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class HiddenServiceDir
|
||||
{
|
||||
public static bool TryParse(string str, out HiddenServiceDir serviceDir)
|
||||
{
|
||||
serviceDir = null;
|
||||
if (!str.Trim().StartsWith("HiddenServiceDir ", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
serviceDir = new HiddenServiceDir() { DirectoryPath = parts[1].Trim() };
|
||||
return true;
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; set; }
|
||||
public List<HiddenServicePortDefinition> ServicePorts { get; set; } = new List<HiddenServicePortDefinition>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HiddenServiceDir {DirectoryPath}";
|
||||
}
|
||||
}
|
||||
public class HiddenServicePortDefinition
|
||||
{
|
||||
public static bool TryParse(string str, out HiddenServicePortDefinition portDefinition)
|
||||
{
|
||||
portDefinition = null;
|
||||
if (!str.Trim().StartsWith("HiddenServicePort ", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 3)
|
||||
return false;
|
||||
if (!int.TryParse(parts[1].Trim(), out int virtualPort))
|
||||
return false;
|
||||
var addressPort = parts[2].Trim().Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (addressPort.Length != 2)
|
||||
return false;
|
||||
if (!int.TryParse(addressPort[1].Trim(), out int port))
|
||||
return false;
|
||||
if (!IPAddress.TryParse(addressPort[0].Trim(), out IPAddress address))
|
||||
return false;
|
||||
portDefinition = new HiddenServicePortDefinition() { VirtualPort = virtualPort, Endpoint = new IPEndPoint(address, port) };
|
||||
return true;
|
||||
}
|
||||
public int VirtualPort { get; set; }
|
||||
public IPEndPoint Endpoint { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HiddenServicePort {VirtualPort} {Endpoint}";
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/Tor/OnionEndpoint.cs
Normal file
16
BTCPayServer/Tor/OnionEndpoint.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tor
|
||||
{
|
||||
public class OnionEndpoint : DnsEndPoint
|
||||
{
|
||||
public OnionEndpoint(string host, int port): base(host, port)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
186
BTCPayServer/Tor/Socks5Connect.cs
Normal file
186
BTCPayServer/Tor/Socks5Connect.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tor
|
||||
{
|
||||
public enum SocksErrorCode
|
||||
{
|
||||
Success = 0,
|
||||
GeneralServerFailure = 1,
|
||||
ConnectionNotAllowed = 2,
|
||||
NetworkUnreachable = 3,
|
||||
HostUnreachable = 4,
|
||||
ConnectionRefused = 5,
|
||||
TTLExpired = 6,
|
||||
CommandNotSupported = 7,
|
||||
AddressTypeNotSupported = 8,
|
||||
}
|
||||
public class SocksException : Exception
|
||||
{
|
||||
public SocksException(SocksErrorCode errorCode) : base(GetMessageForCode((int)errorCode))
|
||||
{
|
||||
SocksErrorCode = errorCode;
|
||||
}
|
||||
|
||||
public SocksErrorCode SocksErrorCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
private static string GetMessageForCode(int errorCode)
|
||||
{
|
||||
switch (errorCode)
|
||||
{
|
||||
case 0:
|
||||
return "Success";
|
||||
case 1:
|
||||
return "general SOCKS server failure";
|
||||
case 2:
|
||||
return "connection not allowed by ruleset";
|
||||
case 3:
|
||||
return "Network unreachable";
|
||||
case 4:
|
||||
return "Host unreachable";
|
||||
case 5:
|
||||
return "Connection refused";
|
||||
case 6:
|
||||
return "TTL expired";
|
||||
case 7:
|
||||
return "Command not supported";
|
||||
case 8:
|
||||
return "Address type not supported";
|
||||
default:
|
||||
return "Unknown code";
|
||||
}
|
||||
}
|
||||
|
||||
public SocksException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class Socks5Connect
|
||||
{
|
||||
static readonly byte[] SelectionMessage = new byte[] { 5, 1, 0 };
|
||||
public static async Task<Socket> ConnectSocksAsync(EndPoint socksEndpoint, DnsEndPoint endpoint, CancellationToken cancellation)
|
||||
{
|
||||
Socket s = null;
|
||||
int maxTries = 3;
|
||||
int retry = 0;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await s.ConnectAsync(socksEndpoint).WithCancellation(cancellation).ConfigureAwait(false);
|
||||
NetworkStream stream = new NetworkStream(s, false);
|
||||
|
||||
await stream.WriteAsync(SelectionMessage, 0, SelectionMessage.Length, cancellation).ConfigureAwait(false);
|
||||
await stream.FlushAsync(cancellation).ConfigureAwait(false);
|
||||
|
||||
var selectionResponse = new byte[2];
|
||||
await stream.ReadAsync(selectionResponse, 0, 2, cancellation);
|
||||
if (selectionResponse[0] != 5)
|
||||
throw new SocksException("Invalid version in selection reply");
|
||||
if (selectionResponse[1] != 0)
|
||||
throw new SocksException("Unsupported authentication method in selection reply");
|
||||
|
||||
var connectBytes = CreateConnectMessage(endpoint.Host, endpoint.Port);
|
||||
await stream.WriteAsync(connectBytes, 0, connectBytes.Length, cancellation).ConfigureAwait(false);
|
||||
await stream.FlushAsync(cancellation).ConfigureAwait(false);
|
||||
|
||||
var connectResponse = new byte[10];
|
||||
await stream.ReadAsync(connectResponse, 0, 10, cancellation);
|
||||
if (connectResponse[0] != 5)
|
||||
throw new SocksException("Invalid version in connect reply");
|
||||
if (connectResponse[1] != 0)
|
||||
{
|
||||
var code = (SocksErrorCode)connectResponse[1];
|
||||
if (!IsTransient(code) || retry++ >= maxTries)
|
||||
throw new SocksException(code);
|
||||
CloseSocket(ref s);
|
||||
await Task.Delay(1000, cancellation).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
if (connectResponse[2] != 0)
|
||||
throw new SocksException("Invalid RSV in connect reply");
|
||||
if (connectResponse[3] != 1)
|
||||
throw new SocksException("Invalid ATYP in connect reply");
|
||||
for (int i = 4; i < 4 + 4; i++)
|
||||
{
|
||||
if (connectResponse[i] != 0)
|
||||
throw new SocksException("Invalid BIND address in connect reply");
|
||||
}
|
||||
|
||||
if (connectResponse[8] != 0 || connectResponse[9] != 0)
|
||||
throw new SocksException("Invalid PORT address connect reply");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
CloseSocket(ref s);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseSocket(ref Socket s)
|
||||
{
|
||||
if (s == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
s.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTransient(SocksErrorCode code)
|
||||
{
|
||||
return code == SocksErrorCode.GeneralServerFailure ||
|
||||
code == SocksErrorCode.TTLExpired;
|
||||
}
|
||||
|
||||
internal static byte[] CreateConnectMessage(string host, int port)
|
||||
{
|
||||
byte[] sendBuffer;
|
||||
byte[] nameBytes = Encoding.ASCII.GetBytes(host);
|
||||
|
||||
var addressBytes =
|
||||
Enumerable.Empty<byte>()
|
||||
.Concat(new[] { (byte)nameBytes.Length })
|
||||
.Concat(nameBytes).ToArray();
|
||||
|
||||
sendBuffer =
|
||||
Enumerable.Empty<byte>()
|
||||
.Concat(
|
||||
new byte[]
|
||||
{
|
||||
(byte)5, (byte) 0x01, (byte) 0x00, (byte)0x03
|
||||
})
|
||||
.Concat(addressBytes)
|
||||
.Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)port))).ToArray();
|
||||
return sendBuffer;
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
|
||||
<a href="https://docs.btcpayserver.org/features/apps" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@model UpdateCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Crowdfund";
|
||||
@ -67,8 +67,8 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="StartDate" class="control-label"></label>
|
||||
<div class="input-group ">
|
||||
<input asp-for="StartDate" class="form-control datetime"/>
|
||||
<div class="input-group-append only-for-js">
|
||||
<input asp-for="StartDate" class="form-control datetime" placeholder="No start date has been set for this crowdfund"/>
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||
<span class=" fa fa-times"></span>
|
||||
@ -93,8 +93,8 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="EndDate" class="control-label"></label>
|
||||
<div class="input-group ">
|
||||
<input asp-for="EndDate" class="form-control datetime" />
|
||||
<div class="input-group-append only-for-js">
|
||||
<input asp-for="EndDate" class="form-control datetime" placeholder="No end date has been set for this crowdfund" />
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||
<span class=" fa fa-times"></span>
|
||||
@ -167,11 +167,23 @@
|
||||
<input asp-for="SoundsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Sounds"></label>
|
||||
<textarea asp-for="Sounds" class="form-control"></textarea>
|
||||
<span asp-validation-for="Sounds" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AnimationsEnabled"></label>
|
||||
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="AnimationColors"></label>
|
||||
<textarea asp-for="AnimationColors" class="form-control"></textarea>
|
||||
<span asp-validation-for="AnimationColors" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisqusEnabled"></label>
|
||||
<input asp-for="DisqusEnabled" type="checkbox" class="form-check"/>
|
||||
|
@ -1,4 +1,10 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
@if(!Model.Enabled)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert" style="text-align: center;">
|
||||
This crowdfund page is not publically viewable!
|
||||
</div>
|
||||
}
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
|
||||
|
@ -10,13 +10,13 @@
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
}
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@ -25,13 +25,15 @@
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
|
||||
<script src="~/modal/btcpay.js"></script>
|
||||
}
|
||||
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,13 @@
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
||||
}
|
||||
<style>
|
||||
.card-deck {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<script id="template-cart-item" type="text/template">
|
||||
@ -233,27 +240,39 @@
|
||||
|
||||
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||
<div class="row">
|
||||
@for (int i = 0; i < Model.Items.Length; i++)
|
||||
|
||||
|
||||
@{
|
||||
var rowsJ = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
|
||||
}
|
||||
@for (int x = 0; x < rowsJ; x++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
var startingIndex = x == 0 ? 0 : x * 4;
|
||||
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int i = startingIndex; i < endLoopIndex; i++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
|
||||
<div class="js-add-cart card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="js-add-cart card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -295,14 +314,20 @@
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
<div class="row">
|
||||
@for (int i = 0; i < Model.Items.Length; i++)
|
||||
@{
|
||||
var rows = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
|
||||
}
|
||||
@for (int x = 0; x < rows; x++)
|
||||
{
|
||||
var startingIndex = x == 0 ? 0 : x * 4;
|
||||
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int i = startingIndex; i < endLoopIndex; i++)
|
||||
{
|
||||
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
<div class="@className my-3 px-2">
|
||||
<div class="card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
@ -339,9 +364,10 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-2 mb-4">
|
||||
|
@ -129,31 +129,31 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 ml-auto text-center">
|
||||
<a href="https://chat.btcpayserver.org/">
|
||||
<a href="https://chat.btcpayserver.org/" target="_blank">
|
||||
<img src="~/img/mattermost.png" height="100" />
|
||||
</a>
|
||||
<p><a href="https://chat.btcpayserver.org/">On Mattermost</a></p>
|
||||
<p><a href="https://chat.btcpayserver.org/" target="_blank">On Mattermost</a></p>
|
||||
</div>
|
||||
<div class="col-lg-3 ml-auto text-center">
|
||||
<a href="https://slack.btcpayserver.org/">
|
||||
<a href="https://slack.btcpayserver.org/" target="_blank">
|
||||
<img src="~/img/slack.png" height="100" />
|
||||
</a>
|
||||
<p><a href="http://slack.forkbitpay.ninja/">On Slack</a></p>
|
||||
<p><a href="http://slack.forkbitpay.ninja/" target="_blank">On Slack</a></p>
|
||||
</div>
|
||||
<div class="col-lg-3 mr-auto text-center">
|
||||
<a href="https://twitter.com/BtcpayServer">
|
||||
<a href="https://twitter.com/BtcpayServer" target="_blank">
|
||||
<img src="~/img/twitter.png" height="100" />
|
||||
</a>
|
||||
<p>
|
||||
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
|
||||
<a href="https://twitter.com/BtcpayServer" target="_blank">On Twitter</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg-3 mr-auto text-center">
|
||||
<a href="https://github.com/btcpayserver/btcpayserver">
|
||||
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">
|
||||
<img src="~/img/github.png" height="100" />
|
||||
</a>
|
||||
<p>
|
||||
<a href="https://github.com/btcpayserver/btcpayserver">On Github</a>
|
||||
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">On Github</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user