Compare commits
107 Commits
Author | SHA1 | Date | |
---|---|---|---|
be90172840 | |||
b85ee895f5 | |||
93de408e07 | |||
d3662ae734 | |||
132d7795ea | |||
bf5a624209 | |||
abbdbda03a | |||
e203cada54 | |||
00c11c7ee9 | |||
82126b85d2 | |||
4f428c8ed1 | |||
9868af4db8 | |||
0a5d7c5efa | |||
c2754b324d | |||
5c618233cb | |||
9e91259b9e | |||
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 |
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,12 +1,9 @@
|
||||
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -55,6 +55,8 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Security;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net;
|
||||
using BTCPayServer.Tor;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -146,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()
|
||||
@ -589,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);
|
||||
}
|
||||
@ -718,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);
|
||||
@ -869,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",
|
||||
@ -1006,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()
|
||||
@ -1044,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();
|
||||
@ -1143,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);
|
||||
@ -1151,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";
|
||||
@ -1168,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";
|
||||
@ -1185,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);
|
||||
@ -2239,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}");
|
||||
@ -2251,17 +2327,25 @@ donation:
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
if (result.ExpectedName == "bitbank")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
|
||||
}
|
||||
else
|
||||
{
|
||||
// This check if the currency pair is using right currency pair
|
||||
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]
|
||||
@ -2277,7 +2361,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();
|
||||
@ -2299,7 +2383,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();
|
||||
@ -2351,7 +2435,7 @@ donation:
|
||||
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));
|
||||
@ -2406,47 +2490,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.21
|
||||
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.77</Version>
|
||||
<Version>1.0.3.88</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.84" />
|
||||
<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.4" />
|
||||
<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" />
|
||||
@ -67,7 +67,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -48,11 +48,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
@ -149,6 +145,10 @@ namespace BTCPayServer.Configuration
|
||||
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))
|
||||
@ -273,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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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))
|
||||
|
@ -39,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,
|
||||
@ -48,6 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
TorServices torServices,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_Options = options;
|
||||
@ -58,6 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
_torServices = torServices;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -384,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"));
|
||||
@ -428,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()
|
||||
{
|
||||
@ -471,6 +466,25 @@ namespace BTCPayServer.Controllers
|
||||
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);
|
||||
}
|
||||
|
||||
@ -565,7 +579,7 @@ namespace BTCPayServer.Controllers
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = Url.Action(nameof(GetLNDConfig), new { configKey = configKey });
|
||||
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
@ -672,6 +686,7 @@ namespace BTCPayServer.Controllers
|
||||
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);
|
||||
}
|
||||
@ -691,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
|
||||
{
|
||||
@ -165,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(
|
||||
@ -217,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(
|
||||
@ -313,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
@ -68,7 +68,7 @@ namespace BTCPayServer.HostedServices
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
|
||||
{
|
||||
exchanges.Add(item);
|
||||
}
|
||||
|
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;
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
@ -17,5 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
|
||||
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]
|
||||
|
@ -21,14 +21,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
RateSource = chosen.Url;
|
||||
}
|
||||
|
||||
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||
@ -42,6 +44,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 ... %")]
|
||||
@ -57,10 +61,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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;
|
||||
|
32
BTCPayServer/Services/Rates/BitbankRateProvider.cs
Normal file
32
BTCPayServer/Services/Rates/BitbankRateProvider.cs
Normal file
@ -0,0 +1,32 @@
|
||||
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 BitbankRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitbankRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bitbank";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
|
||||
.Properties()
|
||||
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), new BidAsk(p.Value["buy"].Value<decimal>(), p.Value["sell"].Value<decimal>())))
|
||||
.ToArray());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
||||
|
@ -23,20 +23,18 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public class CoinAverageExchange
|
||||
{
|
||||
public CoinAverageExchange(string name, string display)
|
||||
public CoinAverageExchange(string name, string display, string url)
|
||||
{
|
||||
Name = name;
|
||||
Display = display;
|
||||
Url = url;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Display { get; set; }
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
|
||||
}
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
|
||||
@ -47,7 +45,11 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public void Add(CoinAverageExchange exchange)
|
||||
{
|
||||
TryAdd(exchange.Name, exchange);
|
||||
if (!TryAdd(exchange.Name, exchange))
|
||||
{
|
||||
this.Remove(exchange.Name);
|
||||
this.Add(exchange.Name, exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
@ -84,7 +86,6 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Coincheck", Name: "coincheck"),
|
||||
(DisplayName: "Bittylicious", Name: "bittylicious"),
|
||||
(DisplayName: "Gemini", Name: "gemini"),
|
||||
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
|
||||
(DisplayName: "Bit2C", Name: "bit2c"),
|
||||
(DisplayName: "Luno", Name: "luno"),
|
||||
(DisplayName: "Negocie Coins", Name: "negociecoins"),
|
||||
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
})
|
||||
{
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
|
||||
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,10 @@ 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")));
|
||||
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||
@ -166,19 +169,20 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
// Add other exchanges supported here
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
|
||||
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
|
||||
@ -35,6 +35,17 @@
|
||||
</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;
|
||||
}
|
||||
.card-deck .card:only-of-type {
|
||||
max-width: 320px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<script id="template-cart-item" type="text/template">
|
||||
@ -232,18 +243,18 @@
|
||||
</div>
|
||||
|
||||
<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 item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
|
||||
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
|
||||
<div class="js-add-cart card" data-id="@i">
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[index];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="js-add-cart card my-2" data-id="@index">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
@:<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>
|
||||
@ -254,8 +265,7 @@
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -295,75 +305,68 @@
|
||||
<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++)
|
||||
|
||||
<div class="row card-deck my-3 mx-auto">
|
||||
@for (int x = 0; x < Model.Items.Length; x++)
|
||||
{
|
||||
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))
|
||||
var item = Model.Items[x];
|
||||
|
||||
<div class="card my-2" data-id="@x">
|
||||
@if (!String.IsNullOrWhiteSpace(item.Image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(item.Description))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
<p class="card-text">@item.Description</p>
|
||||
}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
@if (item.Custom && !Model.EnableShoppingCart)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id" />
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
@if (item.Custom && !Model.EnableShoppingCart)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id"/>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="card h-100">
|
||||
<div class="card-body my-auto">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -48,7 +48,7 @@
|
||||
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
|
||||
<span class="payment__currencies ">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
<span class="clickable_indicator fa fa-angle-right"></span>
|
||||
</span>
|
||||
@ -71,12 +71,11 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="payment__currencies">
|
||||
<div class="payment__currencies_noborder">
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
|
||||
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
|
||||
<span v-show="srvModel.isLightning">⚡</span>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="payment__spinner">
|
||||
<partial name="Checkout-Spinner" />
|
||||
@ -102,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat">
|
||||
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
|
||||
1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -193,7 +192,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null }">
|
||||
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null || !scanDisplayQr }">
|
||||
<div class="btnGroupLnd">
|
||||
<button onclick="lndToggleBolt11()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 0 }"
|
||||
v-bind:title="$t('BOLT 11 Invoice')">
|
||||
@ -206,11 +205,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment__scan">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" />
|
||||
<qrcode v-bind:val="scanDisplayQr" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon"
|
||||
v-if="scanDisplayQr" />
|
||||
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"
|
||||
v-if="scanDisplayQr">
|
||||
</qrcode>
|
||||
|
||||
|
||||
<div class="payment__spinner qr_currency_icon" style="padding-right: 20px;">
|
||||
<partial name="Checkout-Spinner" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
<div class="payment__details__instruction__open-wallet" v-if="scanDisplayQr">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||
<span>{{$t("Open in wallet")}}</span>
|
||||
</a>
|
||||
|
@ -82,15 +82,15 @@
|
||||
@* Not working because of nsSeparator: false, keySeparator: false,
|
||||
{{$t("nested.lang")}} >>
|
||||
*@
|
||||
|
||||
<select asp-for="DefaultLang"
|
||||
|
||||
<select asp-for="DefaultLang"
|
||||
class="cmblang reverse invisible"
|
||||
onchange="changeLanguage($(this).val())"
|
||||
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
|
||||
|
||||
onchange="changeLanguage($(this).val())"
|
||||
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
|
||||
|
||||
<script>
|
||||
var languageSelectorPrettyDropdown;
|
||||
$(function() {
|
||||
$(function () {
|
||||
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
|
||||
$("#DefaultLang").val(startingLanguage);
|
||||
languageSelectorPrettyDropdown = initDropdown("#DefaultLang");
|
||||
@ -113,11 +113,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</invoice>
|
||||
<script type="text/javascript">
|
||||
var availableLanguages = @Html.Raw(Json.Serialize(
|
||||
langService
|
||||
.GetLanguages()
|
||||
.Select((language) => language.Code)));;
|
||||
<script type="text/javascript">
|
||||
var availableLanguages = @Html.Raw(Json.Serialize(langService.GetLanguages().Select((language) => language.Code)));;
|
||||
var storeDefaultLang = "@Model.DefaultLang";
|
||||
var fallbackLanguage = "en";
|
||||
startingLanguage = computeStartingLanguage();
|
||||
@ -126,14 +123,14 @@
|
||||
.use(window.i18nextXHRBackend)
|
||||
.init({
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}.json'
|
||||
loadPath: '@(Model.RootPath)locales/{{lng}}.json'
|
||||
},
|
||||
lng: startingLanguage,
|
||||
fallbackLng: fallbackLanguage,
|
||||
nsSeparator: false,
|
||||
keySeparator: false
|
||||
});
|
||||
|
||||
|
||||
function computeStartingLanguage() {
|
||||
if (urlParams.lang && isLanguageAvailable(urlParams.lang)) {
|
||||
return urlParams.lang;
|
||||
@ -150,11 +147,11 @@
|
||||
i18next.changeLanguage(lang);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isLanguageAvailable(languageCode) {
|
||||
return availableLanguages.indexOf(languageCode) >= 0;
|
||||
}
|
||||
|
||||
|
||||
const i18n = new VueI18next(i18next);
|
||||
|
||||
// TODO: Move all logic from core.js to Vue controller
|
||||
@ -169,7 +166,7 @@
|
||||
i18n: i18n,
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr,
|
||||
qrcode: VueQrcode,
|
||||
changelly: ChangellyComponent,
|
||||
coinswitch: CoinSwitchComponent
|
||||
},
|
||||
@ -179,9 +176,10 @@
|
||||
scanDisplayQr: "",
|
||||
expiringSoon: false,
|
||||
isModal: srvModel.isModal,
|
||||
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
|
||||
selectedThirdPartyProcessor: ""
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -17,8 +17,8 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="Id"/>
|
||||
<form method="post" action="@Url.Action("EditPaymentRequest", "PaymentRequest", new { id = Model.Id}, Context.Request.Scheme)">
|
||||
<input type="hidden" name="Id" value="@Model.Id"/>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Title" class="control-label"></label>*
|
||||
@ -34,7 +34,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Currency" class="control-label"></label>*
|
||||
<input placeholder="BTC" asp-for="Currency" class="form-control"/>
|
||||
<input asp-for="Currency" class="form-control"/>
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -66,8 +66,8 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="ExpiryDate" class="control-label"></label>
|
||||
<div class="input-group ">
|
||||
<input asp-for="ExpiryDate" class="form-control datetime" min="today"/>
|
||||
<div class="input-group-append only-for-js">
|
||||
<input asp-for="ExpiryDate" class="form-control datetime" min="today" placeholder="No expiry date has been set for this payment request"/>
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||
<span class=" fa fa-times"></span>
|
||||
@ -105,7 +105,7 @@
|
||||
asp-action="ListInvoices"
|
||||
asp-controller="Invoice"
|
||||
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
|
||||
|
||||
<a class="btn btn-secondary" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
|
||||
}
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
|
||||
<a href="https://docs.btcpayserver.org/features/paymentrequests" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -54,6 +55,8 @@
|
||||
<span> - </span>
|
||||
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
|
||||
<span> - </span>
|
||||
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a>
|
||||
<span> - </span>
|
||||
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -11,13 +11,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"))
|
||||
{
|
||||
@ -26,13 +26,15 @@
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/payment-request-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/payment-request-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
</head>
|
||||
@ -107,7 +109,7 @@ else
|
||||
<template v-else v-for="invoice of srvModel.invoices" :key="invoice.id">
|
||||
<tr class="bg-light">
|
||||
<td scope="row">{{invoice.id}}</td>
|
||||
<td>{{invoice.amount}} {{invoice.currency}}</td>
|
||||
<td>{{invoice.amountFormatted}}</td>
|
||||
<td>{{moment(invoice.expiryDate).format('L HH:mm')}}</td>
|
||||
<td>{{invoice.status}}</td>
|
||||
</tr>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>@Model.CryptoCode LN Node Info</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 rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||
@ -18,10 +18,10 @@
|
||||
|
||||
<link rel="manifest" href="~/manifest.json">
|
||||
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||
|
||||
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
|
||||
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
qrcode: VueQrcode
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
@ -74,70 +74,76 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy { cursor: copy; }
|
||||
|
||||
.qr-container svg {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
cursor: copy;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<noscript>
|
||||
<div class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
|
||||
<small class="@(Model.Available ? "text-success" : "text-danger")">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
@if (Model.Available)
|
||||
{
|
||||
<div class="card-body m-sm-0 p-sm-0">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
|
||||
<small class="@(Model.Available ? "text-success" : "text-danger")">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
@if (Model.Available)
|
||||
{
|
||||
<div class="card-body m-sm-0 p-sm-0">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app" class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
{{srvModel.cryptoCode}} Lightning Node
|
||||
- {{srvModel.available? "Online" : "Unavailable"}}
|
||||
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
|
||||
<div class="qr-container mb-2">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon"/>
|
||||
<qrcode v-bind:val="srvModel.nodeInfo" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</noscript>
|
||||
<div id="app" class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
{{srvModel.cryptoCode}} Lightning Node
|
||||
- {{srvModel.available? "Online" : "Unavailable"}}
|
||||
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
|
||||
<div class="qr-container mb-2">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
|
||||
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Password"></label>
|
||||
<input asp-for="Settings.Password" type="password" class="form-control" />
|
||||
<input asp-for="Settings.Password" type="password" class="form-control" value="@Model.Settings.Password" />
|
||||
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
<div class="form-group">
|
||||
<h5>QR Code connection</h5>
|
||||
<p>
|
||||
<span>You can use QR Code to connect to your clightning from your mobile.<br /></span>
|
||||
<span>You can use QR Code to connect to your @Model.WalletName from your mobile.<br /></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user