Compare commits

..

52 Commits

Author SHA1 Message Date
a82b971ce7 bump 2019-03-07 20:51:51 +09:00
b58c8ef2f0 Bump libraries 2019-03-07 20:33:05 +09:00
274533bfdf Fix spot not using HttpClient created by the httpclientfactory 2019-03-07 19:41:20 +09:00
cd6ce401e1 Fix logs getting spammed by HTTP requests 2019-03-07 19:41:19 +09:00
0c0809101d Datetime picker and small edit UI changes (#647)
* do not allow negative amounts for crowdfund and payment requests

* remove currency placeholder in payment requests

* Improve date picker ui 

Clear button only appears when a value is set. If no value is set, display a placeholder indicating it. closes #625
2019-03-07 14:29:29 +09:00
4b342376a8 Pos experimental card deck (#651)
* Try out experimental card deck

* apply card deck to shopping cart version
2019-03-07 14:28:42 +09:00
fd963b9ad0 fix no store error message for payment request (#646)
* fix no store error message for payment request 

closes #628

* Update PaymentRequestController.cs
2019-03-07 14:28:14 +09:00
06406c0695 Clone Payment Requests (#648)
* Clone Payment Requests

closes #615

* Do not save clone instantly
2019-03-07 14:27:16 +09:00
2b567de5c1 Allow sounds and animation colors in crowdfund to be configured (#653)
closes #652
2019-03-07 14:25:09 +09:00
ef46d03760 fix lightning typo (#655)
closes #622
2019-03-07 13:14:47 +09:00
b174f299fa Fix LND QR code (Fix #656) 2019-03-07 10:26:27 +09:00
09837966b9 bump 2019-03-06 17:53:03 +09:00
465dce1d02 Running check of submitted SMTP data on both Test and Save 2019-03-05 13:00:14 -06:00
067dbad546 Fix build 2019-03-05 17:29:52 +09:00
522970fdb9 Fix build 2019-03-05 17:21:44 +09:00
e67aa499a6 Fix build 2019-03-05 17:20:26 +09:00
3b68d81507 Small refactoring 2019-03-05 17:13:34 +09:00
051248f2fc Add CancellationToken to GetRatesAsync and propagate it from the controllers to the rate fetcher 2019-03-05 17:09:17 +09:00
9a239f99f4 Add bylls as supported exchange 2019-03-05 16:07:23 +09:00
86c431d66e Only get contributions from invoices matching the currency of the Payment request or crowdfund 2019-03-05 14:26:27 +09:00
b35fe0e8e3 Fix CurrentPendingAmount/CurrentAmount not being set 2019-03-05 14:06:40 +09:00
54905f5ceb Fix currencyValue 2019-03-05 13:58:13 +09:00
1c9b05d992 Fix: Payment requests and crowdfund were estimating current contributions based on the current rate 2019-03-05 13:54:34 +09:00
a89c71df38 Fix bundleconfig 2019-03-04 22:38:06 +09:00
3db1f2af12 Fix warning when compiling bundles 2019-03-04 22:35:56 +09:00
5399ff2751 Fix forwarding options 2019-03-04 22:34:14 +09:00
bcea6027e9 Replace Forwarded Headers via ASP.NET Core middleware 2019-03-04 20:48:19 +09:00
a9722df7e4 bump 2019-03-04 18:39:16 +09:00
474be6f7be French update 2019-03-04 18:39:03 +09:00
ada9a7264b Fix typo in AppsController.cs (#630)
Fixing a tiny typo in the AppsController.cs
2019-03-04 18:36:47 +09:00
e991b302d0 Fix bug: "BTCPay is expecting you to access this website" being cached 2019-03-04 18:33:57 +09:00
eef301c6ec Fix error message if X-Forwarded-Proto not set correctly 2019-03-04 17:26:08 +09:00
3fdfd0adfd Removed exponentiation on invoice request amount
Fixes #620
2019-03-03 17:44:28 -06:00
358f1ffc43 Removed border when there is only one currency 2019-03-03 17:44:28 -06:00
349c3409df Preserve password when sending test email 2019-03-03 17:40:40 -06:00
0263a2950c added enabled to ViewCrowdfundViewModel, added warning on preview page 2019-03-03 17:39:53 -06:00
0364a57cae Added a space between Enable and SSL. 2019-03-03 17:39:22 -06:00
e232dd7d7e Add a space between "Test" and "Email" in the UI. 2019-03-03 17:38:57 -06:00
fee936b569 Only checking for last admin if user being deleted is admin
Bugfixing issue #632
2019-03-03 17:38:38 -06:00
420115c54d bump nbx 2019-03-03 01:37:34 +09:00
312e961098 Update c-lightning, nbxplorer and lightning libs 2019-03-02 23:38:26 +09:00
223213857f Do not expose internal IP on SSH connection settings 2019-03-01 16:41:36 +09:00
c0da81557b Fix missing authenticated URI for charge 2019-03-01 15:38:11 +09:00
81945c0737 Fix bug on spark external config parsing 2019-03-01 15:34:30 +09:00
9664e3d6a1 bump version and deps 2019-03-01 14:55:53 +09:00
0a8bd38e76 Danish support 2019-03-01 14:52:45 +09:00
013054fb82 Small refactor 2019-03-01 14:46:32 +09:00
d898f716d1 Add some tests on externalConnectionString 2019-03-01 14:33:32 +09:00
1d3f144d21 Support relative path for external services, simplify the code in Services 2019-03-01 13:20:21 +09:00
de29d87487 Fix QR code not showing full uri if using relative path 2019-02-28 23:01:25 +09:00
ebef085a9c Support relative path for Spark and RTL external url, check in server settings if we are using a secure protocol 2019-02-28 22:20:14 +09:00
2c1f159d72 Document error of reverse proxy configuration 2019-02-28 21:43:44 +09:00
85 changed files with 1081 additions and 877 deletions

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}

View File

@ -52,6 +52,9 @@ using NBitpayClient.Extensions;
using BTCPayServer.Services;
using System.Text.RegularExpressions;
using BTCPayServer.Events;
using BTCPayServer.Configuration;
using System.Security;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Tests
{
@ -586,7 +589,7 @@ namespace BTCPayServer.Tests
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule).GetAwaiter().GetResult();
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult();
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
@ -715,20 +718,20 @@ 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[]>>();
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);
@ -2236,7 +2239,7 @@ donation:
foreach (var result in factory
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
@ -2253,12 +2256,13 @@ donation:
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
}
[Fact]
@ -2274,7 +2278,7 @@ donation:
.ToHashSet();
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = fetcher.FetchRates(pairs, rules);
var result = fetcher.FetchRates(pairs, rules, default);
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
@ -2296,7 +2300,7 @@ donation:
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync()
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
Hit = true;
var rates = new ExchangeRates();
@ -2332,6 +2336,62 @@ donation:
}
}
[Fact]
[Trait("Fast", "Fast")]
public async Task CanExpandExternalConnectionString()
{
var unusedUri = new Uri("https://toto.com");
Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error));
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("https://toto.com/test"), expanded.Server);
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server);
await Assert.ThrowsAsync<SecurityException>(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge));
// Make sure absolute paths are not expanded
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
// Error if directory not exists
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest));
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
var macaroonDirectory = CreateDirectory();
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error));
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
Assert.NotNull(expanded.Macaroons);
Assert.Null(expanded.MacaroonFilePath);
Assert.Null(expanded.Macaroons.AdminMacaroon);
Assert.Null(expanded.Macaroons.InvoiceMacaroon);
Assert.Null(expanded.Macaroons.ReadonlyMacaroon);
File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa });
File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab });
File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac });
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
Assert.NotNull(expanded.Macaroons.AdminMacaroon);
Assert.NotNull(expanded.Macaroons.InvoiceMacaroon);
Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex);
Assert.Equal(0xab, expanded.Macaroons.InvoiceMacaroon.Bytes[0]);
Assert.NotNull(expanded.Macaroons.ReadonlyMacaroon);
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error));
File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken");
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
Assert.Equal("apitoken", expanded.APIToken);
}
private string CreateDirectory([CallerMemberName] string caller = null)
{
var name = $"{caller}-{NBitcoin.RandomUtils.GetUInt32()}";
Directory.CreateDirectory(name);
return name;
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
@ -2347,47 +2407,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]

View File

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.8
image: nicolasdorier/nbxplorer:2.0.0.15
restart: unless-stopped
ports:
- "32838:32838"
@ -119,7 +119,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.6.2-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -165,7 +165,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.6.2-dev
image: btcpayserver/lightning:v0.7.0-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.73</Version>
<Version>1.0.3.81</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.11" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
@ -45,10 +45,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
<PackageReference Include="NBitcoin" Version="4.1.1.86" />
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.5" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />

View File

@ -15,7 +15,6 @@ using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using Serilog.Events;
namespace BTCPayServer.Configuration
@ -128,85 +127,7 @@ namespace BTCPayServer.Configuration
}
}
void externalLnd<T>(string code, string lndType)
{
var lightning = conf.GetOrDefault<string>(code, string.Empty);
if (lightning.Length != 0)
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
{
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
}
}
{
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
if (rtl.Length != 0)
{
if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
}
}
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
if (charge.Length != 0)
{
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
$"Error: {chargeError ?? string.Empty}" + Environment.NewLine +
$"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
}
ExternalServices.Load(net.CryptoCode, conf);
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
@ -220,7 +141,8 @@ namespace BTCPayServer.Configuration
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
OtherExternalServices.AddOrReplace(service.Name, uri);
}
}
@ -325,9 +247,9 @@ namespace BTCPayServer.Configuration
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalCharge : ExternalService
{
public ExternalCharge(LightningConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public LightningConnectionString ConnectionString { get; }
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public abstract class ExternalLnd : ExternalService
{
public ExternalLnd(LightningConnectionString connectionString, string type)
{
ConnectionString = connectionString;
Type = type;
}
public string Type { get; set; }
public LightningConnectionString ConnectionString { get; set; }
}
public class ExternalLndGrpc : ExternalLnd
{
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
}
public class ExternalLndRest : ExternalLnd
{
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
}
}

View File

@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalRTL : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalRTL(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalServices : MultiValueDictionary<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
}

View File

@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public interface IAccessKeyService
{
SparkConnectionString ConnectionString { get; }
Task<string> ExtractAccessKey();
}
public class ExternalSpark : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalSpark(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
var cookie = (ConnectionString.CookeFile == "fake"
? "fake:fake:fake" // Hacks for testing
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
return cookie[2];
}
throw new FormatException("Invalid cookiefile format");
}
}
}

View File

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
namespace BTCPayServer.Configuration
{
public class ExternalConnectionString
{
public Uri Server { get; set; }
public byte[] Macaroon { get; set; }
public Macaroons Macaroons { get; set; }
public string MacaroonFilePath { get; set; }
public string CertificateThumbprint { get; set; }
public string MacaroonDirectoryPath { get; set; }
public string APIToken { get; set; }
public string CookieFilePath { get; set; }
public string AccessKey { get; set; }
/// <summary>
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
{
var connectionString = this.Clone();
// Transform relative URI into absolute URI
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
{
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
}
connectionString.Server = serviceUri;
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
{
// Read the MacaroonDirectory
if (connectionString.MacaroonDirectoryPath != null)
{
try
{
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
connectionString.MacaroonDirectoryPath = null;
}
catch (Exception ex)
{
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
}
}
// Read the MacaroonFilePath
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
}
}
}
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
{
// Read access key from cookie file
if (connectionString.CookieFilePath != null)
{
string cookieFileContent = null;
bool isFake = false;
try
{
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
isFake = connectionString.CookieFilePath == "fake";
connectionString.CookieFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
}
if (serviceType == ExternalServiceTypes.RTL)
{
connectionString.AccessKey = cookieFileContent;
}
else if (serviceType == ExternalServiceTypes.Spark)
{
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
: cookieFileContent).Split(':');
if (cookie.Length >= 3)
{
connectionString.AccessKey = cookie[2];
}
else
{
throw new FormatException("Invalid cookiefile format");
}
}
else if (serviceType == ExternalServiceTypes.Charge)
{
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
}
}
}
return connectionString;
}
private Uri ToRelative(Uri absoluteUrlBase, string path)
{
if (path.StartsWith('/'))
path = path.Substring(1);
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
}
public ExternalConnectionString Clone()
{
return new ExternalConnectionString()
{
MacaroonFilePath = MacaroonFilePath,
CertificateThumbprint = CertificateThumbprint,
Macaroon = Macaroon,
MacaroonDirectoryPath = MacaroonDirectoryPath,
Server = Server,
APIToken = APIToken,
CookieFilePath = CookieFilePath,
AccessKey = AccessKey,
Macaroons = Macaroons?.Clone()
};
}
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new ExternalConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookieFilePath != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookieFilePath = kv[1];
break;
case "macaroondirectorypath":
resultTemp.MacaroonDirectoryPath = kv[1];
break;
case "certthumbprint":
resultTemp.CertificateThumbprint = kv[1];
break;
case "macaroonfilepath":
resultTemp.MacaroonFilePath = kv[1];
break;
case "api-token":
resultTemp.APIToken = kv[1];
break;
case "access-key":
resultTemp.AccessKey = kv[1];
break;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using Microsoft.Extensions.Configuration;
namespace BTCPayServer.Configuration
{
public class ExternalServices : List<ExternalService>
{
public void Load(string cryptoCode, IConfiguration configuration)
{
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (gRPC server)");
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (REST server)");
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Spark server)");
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"LND (Ride the Lightning server)");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
}
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
{
var setting = $"{cryptoCode}.external.{serviceName}";
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
if (connStr.Length != 0)
{
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
{
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
}
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
}
}
public ExternalService GetService(string serviceName, string cryptoCode)
{
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
}
public class ExternalService
{
public string DisplayName { get; set; }
public ExternalServiceTypes Type { get; set; }
public ExternalConnectionString ConnectionString { get; set; }
public string CryptoCode { get; set; }
public string ServiceName { get; set; }
}
public enum ExternalServiceTypes
{
LNDRest,
LNDGRPC,
Spark,
RTL,
Charge
}
}

View File

@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class SparkConnectionString
{
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
if(resultTemp.Server.Scheme == "http")
{
error = "Insecure transport protocol (http)";
return false;
}
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookeFile != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookeFile = kv[1];
break;
default:
return false;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -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;

View File

@ -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));

View File

@ -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;
@ -120,7 +121,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,10 +132,8 @@ 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);
@ -186,7 +185,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 +215,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 +264,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 });
}

View File

@ -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);

View File

@ -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]

View File

@ -542,7 +542,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 +590,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));

View File

@ -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)

View File

@ -25,7 +25,7 @@ namespace BTCPayServer.Controllers
throw new ArgumentNullException(nameof(directoryPath));
Macaroons macaroons = new Macaroons();
if (!Directory.Exists(directoryPath))
return macaroons;
throw new DirectoryNotFoundException("Macaroons directory not found");
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
{
try
@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers
}
return macaroons;
}
public Macaroons Clone()
{
return new Macaroons()
{
AdminMacaroon = AdminMacaroon,
InvoiceMacaroon = InvoiceMacaroon,
ReadonlyMacaroon = ReadonlyMacaroon
};
}
public Macaroon ReadonlyMacaroon { get; set; }
public Macaroon InvoiceMacaroon { get; set; }

View File

@ -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
}
});
}
@ -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."
});
}
}
@ -220,7 +227,7 @@ 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;
if (result == null)
@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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,10 @@ 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 +119,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)
@ -168,7 +169,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))

View File

@ -26,7 +26,6 @@ using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers
@ -385,18 +384,17 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
"Delete"));
@ -429,13 +427,6 @@ namespace BTCPayServer.Controllers
}
public IHttpClientFactory HttpClientFactory { get; }
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
@ -455,62 +446,18 @@ namespace BTCPayServer.Controllers
public IActionResult Services()
{
var result = new ServicesViewModel();
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
result.ExternalServices = _Options.ExternalServices;
foreach (var externalService in _Options.OtherExternalServices)
{
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = grpcService.Type,
Action = nameof(LndServices),
Index = i++,
});
}
i = 0;
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Ride the Lightning server (RTL)",
Action = nameof(RTLService),
Index = i++,
});
}
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Lightning charge server",
Action = nameof(LightningChargeServices),
Index = i++,
});
}
}
foreach (var externalService in _Options.ExternalServices)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = externalService.Key,
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
});
}
if (_Options.SSHSettings != null)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = "SSH",
Link = this.Url.Action(nameof(SSHService))
@ -519,138 +466,108 @@ namespace BTCPayServer.Controllers
return View(result);
}
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
[Route("server/services/{serviceName}/{cryptoCode}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if (lightningCharge == null)
{
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (service == null)
return NotFound();
}
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
vm.APIToken = lightningCharge.Password;
try
{
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
switch (service.Type)
{
if (lightningCharge.CookieFilePath != "fake")
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
else
vm.APIToken = "fake";
case ExternalServiceTypes.Charge:
return LightningChargeServices(service, connectionString, showQR);
case ExternalServiceTypes.RTL:
case ExternalServiceTypes.Spark:
if (connectionString.AccessKey == null)
{
StatusMessage = $"Error: The access key of the service is not set";
return RedirectToAction(nameof(Services));
}
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = service.DisplayName;
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
return View("LightningWalletServices", vm);
case ExternalServiceTypes.LNDGRPC:
case ExternalServiceTypes.LNDRest:
return LndServices(service, connectionString, nonce);
default:
throw new NotSupportedException(service.Type.ToString());
}
var builder = new UriBuilder(lightningCharge.ToUri(false));
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
if (external == null)
{
return NotFound();
}
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = walletName;
try
{
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View("LightningWalletServices", vm);
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = connectionString.Server.AbsoluteUri;
vm.APIToken = connectionString.APIToken;
var builder = new UriBuilder(connectionString.Server);
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
return View(nameof(LightningChargeServices), vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LndGrpcServicesViewModel();
if (external.ConnectionType == LightningConnectionType.LndGRPC)
if (service.Type == ExternalServiceTypes.LNDGRPC)
{
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
model.SSL = connectionString.Server.Scheme == "https";
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if (external.ConnectionType == LightningConnectionType.LndREST)
else if (service.Type == ExternalServiceTypes.LNDRest)
{
model.Uri = external.BaseUri.AbsoluteUri;
model.Uri = connectionString.Server.AbsoluteUri;
model.ConnectionType = "REST";
}
if (external.CertificateThumbprint != null)
if (connectionString.CertificateThumbprint != null)
{
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
model.CertificateThumbprint = connectionString.CertificateThumbprint;
}
if (external.Macaroon != null)
if (connectionString.Macaroon != null)
{
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
}
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
if (nonce != null)
{
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
model.QRCode = $"config={model.QRCodeLink}";
}
}
return View(model);
return View(nameof(LndServices), model);
}
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
{
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
}
[Route("lnd-config/{configKey}/lnd.config")]
@ -663,68 +580,62 @@ namespace BTCPayServer.Controllers
return Json(conf);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
[Route("server/services/{serviceName}/{cryptoCode}")]
[HttpPost]
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
{
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (service == null)
return NotFound();
ExternalConnectionString connectionString = null;
try
{
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
LightningConfigurations confs = new LightningConfigurations();
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
if (external.ConnectionType == LightningConnectionType.LndGRPC)
if (service.Type == ExternalServiceTypes.LNDGRPC)
{
LightningConfiguration grpcConf = new LightningConfiguration();
grpcConf.Type = "grpc";
grpcConf.Host = external.BaseUri.DnsSafeHost;
grpcConf.Port = external.BaseUri.Port;
grpcConf.SSL = external.BaseUri.Scheme == "https";
grpcConf.Host = connectionString.Server.DnsSafeHost;
grpcConf.Port = connectionString.Server.Port;
grpcConf.SSL = connectionString.Server.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (external.ConnectionType == LightningConnectionType.LndREST)
else if (service.Type == ExternalServiceTypes.LNDRest)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.Uri = external.BaseUri.AbsoluteUri;
restconf.Uri = connectionString.Server.AbsoluteUri;
confs.Configurations.Add(restconf);
}
else
throw new NotSupportedException(external.ConnectionType.ToString());
throw new NotSupportedException(service.Type.ToString());
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
commonConf.ChainType = _Options.NetworkType.ToString();
commonConf.CryptoCode = cryptoCode;
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if (connectionString == null)
return null;
connectionString = connectionString.Clone();
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch
{
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
return null;
}
}
return connectionString;
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
}
[Route("server/services/ssh")]
@ -739,15 +650,24 @@ namespace BTCPayServer.Controllers
return NotFound();
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
}
var server = IsLocalNetwork(settings.Server) ? this.Request.Host.Host: settings.Server;
SSHServiceViewModel vm = new SSHServiceViewModel();
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
vm.Password = settings.Password;
vm.KeyFilePassword = settings.KeyFilePassword;
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
return View(vm);
}
private static bool IsLocalNetwork(string server)
{
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{
@ -763,19 +683,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";

View File

@ -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";

View File

@ -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;
@ -203,7 +204,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, CancellationToken cancellationToken = default)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
if (!ModelState.IsValid)
@ -267,7 +268,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)
{

View File

@ -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;

View File

@ -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");

View File

@ -125,6 +125,12 @@ namespace BTCPayServer
return str;
return str + "/";
}
public static string WithStartingSlash(this string str)
{
if (str.StartsWith("/", StringComparison.InvariantCulture))
return str;
return $"/{str}";
}
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
{
@ -228,6 +234,31 @@ namespace BTCPayServer
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
}
/// <summary>
/// Will return an absolute URL.
/// If `relativeOrAsbolute` is absolute, returns it.
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
/// </summary>
/// <param name="request"></param>
/// <param name="relativeOrAbsolte"></param>
/// <returns></returns>
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
{
if (relativeOrAbsolute == null)
{
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()), UriKind.Absolute);
}
if (relativeOrAbsolute.IsAbsoluteUri)
return relativeOrAbsolute;
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
}
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{
services.Configure<BTCPayServerOptions>(o =>

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.TryGetValue(cryptoCode, out summary) &&
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
}

View File

@ -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)
{

View File

@ -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;

View File

@ -17,6 +17,7 @@ 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,6 +160,14 @@ 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();

View File

@ -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; }
}
}

View File

@ -1,7 +1,9 @@
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
@ -30,6 +32,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 +54,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

View File

@ -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; }
@ -140,6 +140,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
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; }

View File

@ -18,8 +18,9 @@ namespace BTCPayServer.Models.ServerViewModels
{
get; set;
}
[EmailAddress]
[Display(Name = "Test Email")]
public string TestEmail
{
get; set;

View File

@ -2,27 +2,20 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration.External;
using BTCPayServer.Configuration;
namespace BTCPayServer.Models.ServerViewModels
{
public class ServicesViewModel
{
public class LNDServiceViewModel
{
public string Crypto { get; set; }
public string Type { get; set; }
public int Index { get; set; }
public string Action { get; internal set; }
}
public class ExternalService
public class OtherExternalService
{
public string Name { get; set; }
public string Link { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
}
}

View File

@ -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(),

View File

@ -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);
}

View File

@ -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;

View File

@ -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));

View File

@ -30,8 +30,8 @@
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",

View File

@ -40,7 +40,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:

View File

@ -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

View File

@ -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
{

View File

@ -13,11 +13,10 @@ namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
IHttpContextAccessor httpContext;
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
{
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";
@ -32,9 +31,9 @@ namespace BTCPayServer.Services
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 NetworkType NetworkType { get; set; }
public string Version

View File

@ -35,6 +35,7 @@ namespace BTCPayServer.Services.Mails
get; set;
}
[Display(Name = "Enable SSL")]
public bool EnableSSL
{
get; set;

View File

@ -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;

View File

@ -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

View 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)) });
}
}
}

View File

@ -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);
});
}
}

View File

@ -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)
{

View File

@ -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();

View File

@ -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); }
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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());
}

View File

@ -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());

View File

@ -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);

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
@ -21,12 +22,12 @@ namespace BTCPayServer.Services.Rates
{
_inner = inner;
}
public async Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
{
return await _inner.GetRatesAsync();
return await _inner.GetRatesAsync(cancellationToken);
}
catch (Exception ex)
{
@ -109,8 +110,9 @@ namespace BTCPayServer.Services.Rates
// Handmade providers
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
@ -167,18 +169,19 @@ namespace BTCPayServer.Services.Rates
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
public async Task<QueryRateResult> QueryRates(string exchangeName)
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
{
Providers.TryGetValue(exchangeName, out var directProvider);
directProvider = directProvider ?? NullRateProvider.Instance;
var wrapper = new WrapperRateProvider(directProvider);
var value = await wrapper.GetRatesAsync();
var value = await wrapper.GetRatesAsync(cancellationToken);
return new QueryRateResult()
{
Latency = wrapper.Latency,

View File

@ -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"/>

View File

@ -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">

View File

@ -35,6 +35,13 @@
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
}
<style>
.card-deck {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: .5rem;
}
</style>
</head>
<script id="template-cart-item" type="text/template">
@ -233,27 +240,39 @@
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
@{
var rowsJ = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
}
@for (int x = 0; x < rowsJ; x++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
var startingIndex = x == 0 ? 0 : x * 4;
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
<div class="row card-deck my-3 mx-auto">
@for (int i = startingIndex; i < endLoopIndex; i++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
<div class="js-add-cart card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
<div class="js-add-cart card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
}
</div>
}
</div>
@ -295,14 +314,20 @@
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
@{
var rows = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
}
@for (int x = 0; x < rows; x++)
{
var startingIndex = x == 0 ? 0 : x * 4;
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
<div class="row card-deck my-3 mx-auto">
@for (int i = startingIndex; i < endLoopIndex; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="@className my-3 px-2">
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
@ -339,9 +364,10 @@
}
</div>
</div>
</div>
}
</div>
}
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">

View File

@ -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 v-show="srvModel.isLightning">&#9889;</span>
</div>
}
<div class="payment__spinner">
<partial name="Checkout-Spinner" />

View File

@ -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>

View File

@ -54,6 +54,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>

View File

@ -107,7 +107,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>

View File

@ -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>

View File

@ -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">

View File

@ -17,7 +17,7 @@
<div class="col-md-8">
<h4>Crypto services</h4>
<div class="form-group">
<span>You can get access here to LND (gRPC, Rest) services exposed by your server</span>
<span>You can get access here to services exposed by your server</span>
</div>
<div class="form-group">
@ -30,13 +30,13 @@
</tr>
</thead>
<tbody>
@foreach (var lnd in Model.LNDServices)
@foreach (var s in Model.ExternalServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td>@s.CryptoCode</td>
<td>@s.DisplayName</td>
<td style="text-align:right">
<a asp-action="@lnd.Action" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
<a asp-action="Service" asp-route-serviceName="@s.ServiceName" asp-route-cryptoCode="@s.CryptoCode">See information</a>
</td>
</tr>
}
@ -46,7 +46,7 @@
</div>
</div>
@if (Model.ExternalServices.Count != 0)
@if (Model.OtherExternalServices.Count != 0)
{
<div class="row">
<div class="col-md-8">
@ -63,7 +63,7 @@
</tr>
</thead>
<tbody>
@foreach (var s in Model.ExternalServices)
@foreach (var s in Model.OtherExternalServices)
{
<tr>
<td>@s.Name</td>

View File

@ -85,7 +85,7 @@
</div>
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>BTCPay is expecting you to access this website from <b>@(env.ExpectedProtocol)://@(env.ExpectedHost)/</b>. If you use a reverse proxy, please set the <b>X-Forwarded-Proto</b> header to <b>@(env.ExpectedProtocol)</b>:</span>
<span>BTCPay is expecting you to access this website from <b>@(env.ExpectedProtocol)://@(env.ExpectedHost)/</b>. If you use a reverse proxy, please set the <b>X-Forwarded-Proto</b> header to <b id="browserScheme">@(env.ExpectedProtocol)</b> (<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-deployment#btcpay-is-expecting-you-to-access-this-website-from" target="_blank">More information</a>)</span>
</div>
</div>
</nav>
@ -112,6 +112,7 @@
var expectedProtocol = @Html.Raw(Json.Serialize(env.ExpectedProtocol));
if (window.location.host != expectedDomain || window.location.protocol != expectedProtocol + ":") {
document.getElementById("badUrl").style.display = "block";
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
}
</script>
</body>

View File

@ -103,7 +103,7 @@
"wwwroot/vendor/highlightjs/highlight.min.js",
"wwwroot/vendor/summernote/summernote-bs4.js",
"wwwroot/vendor/flatpickr/flatpickr.js",
"wwwroot/crowdfund-admin/**/*.js",
"wwwroot/crowdfund-admin/main.js",
"wwwroot/products/js/products.js",
"wwwroot/products/js/products.jquery.js"
]
@ -113,8 +113,7 @@
"inputFiles": [
"wwwroot/vendor/highlightjs/default.min.css",
"wwwroot/vendor/summernote/summernote-bs4.css",
"wwwroot/vendor/flatpickr/flatpickr.min.css",
"wwwroot/crowdfund-admin/*.js"
"wwwroot/vendor/flatpickr/flatpickr.min.css"
]
},
{

View File

@ -55,10 +55,16 @@
padding: 6px;
}
.payment__currencies img {
margin-top: -3px;
height: 24px;
}
.payment__currencies_noborder {
font-size: 13px;
border-radius: 5px;
padding: 6px;
}
.payment__currencies img , .payment__currencies_noborder img{
margin-top: -3px;
height: 24px;
}
.payment__currencies:hover .clickable_underline {
border-bottom: 1px dotted black;

View File

@ -256,7 +256,7 @@ addLoadEvent(function (ev) {
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
var onChain = type.toLowerCase() === "btclike";
if(self.sound) {
playRandomQuakeSound();
playRandomSound();
}
if(self.animation) {
fireworks();

View File

@ -1,5 +1,3 @@
function playSound(path) {
// audio supported?
if (typeof window.Audio === 'function') {
@ -11,55 +9,7 @@ function playSound(path) {
}
}
function playQuakeSound (name){
// var path = window.location.protocol +"://github.com/ClaudiuHKS/AdvancedQuakeSounds/blob/master/sound/QuakeSounds/"+name+"?raw=true"
var path = window.location.protocol + "//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/" + name;
playSound(path);
function playRandomSound(){
var sound = srvModel.sounds[Math.floor((Math.random() * srvModel.sounds.length) )];
playSound(sound);
}
function playRandomQuakeSound(){
playQuakeSound(quake[Math.floor((Math.random() * quake.length) )]);
}
var quake = [
"dominating.wav"
,"doublekill.wav"
,"doublekill2.wav"
,"eagleeye.wav"
,"firstblood.wav"
,"firstblood2.wav"
,"firstblood3.wav"
,"flawless.wav"
,"godlike.wav"
,"hattrick.wav"
,"headhunter.wav"
,"headshot.wav"
,"headshot2.wav"
,"headshot3.wav"
,"holyshit.wav"
,"killingspree.wav"
,"knife.wav"
,"knife2.wav"
,"knife3.wav"
,"ludicrouskill.wav"
,"megakill.wav"
,"monsterkill.wav"
,"multikill.wav"
,"nade.wav"
,"ownage.wav"
,"payback.wav"
,"prepare.wav"
,"prepare2.wav"
,"prepare3.wav"
,"prepare4.wav"
,"rampage.wav"
,"suicide.wav"
,"suicide2.wav"
,"suicide3.wav"
,"suicide4.wav"
,"teamkiller.wav"
,"triplekill.wav"
,"ultrakill.wav"
,"unstoppable.wav"
,"whickedsick.wav"];

View File

@ -1,14 +1,15 @@
addLoadEvent(function(){
var c = document.getElementById("fireworks");
var ctx = c.getContext("2d");
var cH;
var cW;
var bgColor = "#FF6138";
var bgColor = srvModel.animationColors[0];
var animations = [];
var circles = [];
var colorPicker = (function() {
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#282741"];
var colors = srvModel.animationColors;
var index = 0;
function next() {
index = index++ < colors.length-1 ? index : 0;

View File

@ -0,0 +1,50 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "da-DK",
"currentLanguage": "Dansk",
"lang": "Sprog",
"Awaiting Payment...": "Afventer betaling...",
"Pay with": "Betal med",
"Contact and Refund Email": "Kontakt og Tilbagebetalings Email",
"Contact_Body": "Angiv venligst en email adresse herunder. Vi kontakter dig på denne email, hvis der er et problem med din betaling.",
"Your email": "Din email",
"Continue": "Forsæt",
"Please enter a valid email address": "Indtast venglist en gyldig email adresse",
"Order Amount": "Bestillingsbeløb",
"Network Cost": "Netværks gebyr",
"Already Paid": "Allerede betalt",
"Due": "Manglende betaling",
"Scan": "Skan",
"Copy": "Kopier",
"Conversion": "Konvertering",
"Open in wallet": "Åben i wallet",
"CompletePay_Body": "For at færdiggøre din betaling, send venglist {{btcDue}} {{cryptoCode}} til addressen herunder.",
"Amount": "Beløb",
"Address": "Adresse",
"Copied": "Kopieret",
"ConversionTab_BodyTop": "Du kan betale {{btcDue}} {{cryptoCode}} med andre altcoins end dem sælgeren understøtter. ",
"ConversionTab_BodyDesc": "Denne service er stillet til rådighed af 3. Partnere. Vær venligst opmærksom på, at vi ikke har kontrol over, hvordan udbydere vil videresende dine midler. Fakturaen vil kun blive markeret betalt, når der er modtaget midler på {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "Genprøv",
"ConversionTab_LoadCurrencies_Error": "Genprøv",
"ConversionTab_Lightning": "Ingen konversions partnere tilgængelig for Lightning Network betalinger.",
"ConversionTab_CurrencyList_Select_Option": "Vælg venligst en valuta at konvertere fra",
"Invoice expiring soon...": "Fakturaen udløber snart...",
"Invoice expired": "Fakturaen er udløbet",
"What happened?": "Hvad skete der?",
"InvoiceExpired_Body_1": "Fakturaen er udløbet. En faktura er kun gyldig i {{maxTime Minutes}} minutter. \nDu kan vende tilbage til {{butiknavn}}, hvis du gerne vil sende din betaling igen.",
"InvoiceExpired_Body_2": "Hvis du har forsøgt at sende en betaling, er den endnu ikke accepteret af netværket. Vi har endnu ikke modtaget dine midler.",
"InvoiceExpired_Body_3": "Hvis vi modtager den på et senere tidspunkt, vil vi enten behandle din ordre eller kontakte dig for at aftale refundering...",
"Invoice ID": "Faktura ID",
"Order ID": "Ordre ID",
"Return to StoreName": "Returner til {{storeName}}",
"This invoice has been paid": "Denne faktura er blevet betalt",
"This invoice has been archived": "Denne faktura er blevet arkiveret",
"Archived_Body": "Kontakt venligt butikken for ordre information eller assistance",
"BOLT 11 Invoice": "BOLT 11 Faktura",
"Node Info": "Node Information",
"txCount": "{{count}} transaktion",
"txCount_plural": "{{count}} transaktioner",
"Pay with CoinSwitch": "Betal med CoinSwitch",
"Pay with Changelly": "Betal med Changelly",
"Close": "Luk"
}

View File

@ -44,7 +44,7 @@
"Node Info": "Informations sur le nœud",
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
"Pay with CoinSwitch": "Payer avec CoinSwitch",
"Pay with Changelly": "Payer avec Changelly",
"Close": "Fermer"
}

View File

@ -16,6 +16,6 @@
}
.only-for-js{
.only-for-js, .input-group-clear{
display: none;
}

View File

@ -8,9 +8,28 @@
});
$(".input-group-clear").on("click", function(){
$(".input-group-clear").on("click", function () {
$(this).parents(".input-group").find("input").val(null);
handleInputGroupClearButtonDisplay(this);
});
$(".input-group-clear").each(function () {
var inputGroupClearBtn = this;
handleInputGroupClearButtonDisplay(inputGroupClearBtn);
$(this).parents(".input-group").find("input").on("change input", function () {
handleInputGroupClearButtonDisplay(inputGroupClearBtn);
})
});
$(".only-for-js").show();
function handleInputGroupClearButtonDisplay(element) {
var value = $(element).parents(".input-group").find("input").val();
if (!value) {
$(element).hide();
} else {
$(element).show();
}
}
});