Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
a82b971ce7 | |||
b58c8ef2f0 | |||
274533bfdf | |||
cd6ce401e1 | |||
0c0809101d | |||
4b342376a8 | |||
fd963b9ad0 | |||
06406c0695 | |||
2b567de5c1 | |||
ef46d03760 | |||
b174f299fa | |||
09837966b9 | |||
465dce1d02 | |||
067dbad546 | |||
522970fdb9 | |||
e67aa499a6 | |||
3b68d81507 | |||
051248f2fc | |||
9a239f99f4 | |||
86c431d66e | |||
b35fe0e8e3 | |||
54905f5ceb | |||
1c9b05d992 | |||
a89c71df38 | |||
3db1f2af12 | |||
5399ff2751 | |||
bcea6027e9 | |||
a9722df7e4 | |||
474be6f7be | |||
ada9a7264b | |||
e991b302d0 | |||
eef301c6ec | |||
3fdfd0adfd | |||
358f1ffc43 | |||
349c3409df | |||
0263a2950c | |||
0364a57cae | |||
e232dd7d7e | |||
fee936b569 | |||
420115c54d | |||
312e961098 | |||
223213857f | |||
c0da81557b | |||
81945c0737 | |||
9664e3d6a1 | |||
0a8bd38e76 | |||
013054fb82 | |||
d898f716d1 | |||
1d3f144d21 | |||
de29d87487 | |||
ebef085a9c | |||
2c1f159d72 |
@ -129,6 +129,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
|
@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
@ -118,7 +118,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
@ -130,7 +130,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
//Scenario 4: Enabled But End Date < Now - Not Allowed
|
||||
|
||||
@ -142,7 +142,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
|
||||
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
|
||||
@ -156,13 +156,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -10,7 +11,7 @@ namespace BTCPayServer.Tests.Mocks
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
|
@ -52,6 +52,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]
|
||||
|
@ -69,7 +69,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
image: nicolasdorier/nbxplorer:2.0.0.15
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -119,7 +119,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -165,7 +165,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.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" />
|
||||
|
@ -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
|
||||
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalCharge : ExternalService
|
||||
{
|
||||
public ExternalCharge(LightningConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
public LightningConnectionString ConnectionString { get; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public abstract class ExternalLnd : ExternalService
|
||||
{
|
||||
public ExternalLnd(LightningConnectionString connectionString, string type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalRTL : ExternalService, IAccessKeyService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalRTL(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<string> ExtractAccessKey()
|
||||
{
|
||||
if (ConnectionString?.CookeFile == null)
|
||||
throw new FormatException("Invalid connection string");
|
||||
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalServices : MultiValueDictionary<string, ExternalService>
|
||||
{
|
||||
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
|
||||
{
|
||||
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
|
||||
return Array.Empty<T>();
|
||||
return services.OfType<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public interface IAccessKeyService
|
||||
{
|
||||
SparkConnectionString ConnectionString { get; }
|
||||
Task<string> ExtractAccessKey();
|
||||
}
|
||||
public class ExternalSpark : ExternalService, IAccessKeyService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalSpark(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<string> ExtractAccessKey()
|
||||
{
|
||||
if (ConnectionString?.CookeFile == null)
|
||||
throw new FormatException("Invalid connection string");
|
||||
var cookie = (ConnectionString.CookeFile == "fake"
|
||||
? "fake:fake:fake" // Hacks for testing
|
||||
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
return cookie[2];
|
||||
}
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
}
|
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalConnectionString
|
||||
{
|
||||
public Uri Server { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public Macaroons Macaroons { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string MacaroonDirectoryPath { get; set; }
|
||||
public string APIToken { get; set; }
|
||||
public string CookieFilePath { get; set; }
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a connectionString which does not depends on external resources or information like relative path or file path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
|
||||
{
|
||||
var connectionString = this.Clone();
|
||||
// Transform relative URI into absolute URI
|
||||
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
|
||||
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
||||
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
|
||||
connectionString.MacaroonDirectoryPath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the MacaroonFilePath
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
isFake = connectionString.CookieFilePath == "fake";
|
||||
connectionString.CookieFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Charge)
|
||||
{
|
||||
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private Uri ToRelative(Uri absoluteUrlBase, string path)
|
||||
{
|
||||
if (path.StartsWith('/'))
|
||||
path = path.Substring(1);
|
||||
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
public ExternalConnectionString Clone()
|
||||
{
|
||||
return new ExternalConnectionString()
|
||||
{
|
||||
MacaroonFilePath = MacaroonFilePath,
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
Macaroon = Macaroon,
|
||||
MacaroonDirectoryPath = MacaroonDirectoryPath,
|
||||
Server = Server,
|
||||
APIToken = APIToken,
|
||||
CookieFilePath = CookieFilePath,
|
||||
AccessKey = AccessKey,
|
||||
Macaroons = Macaroons?.Clone()
|
||||
};
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new ExternalConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
|
||||
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
|
||||
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookieFilePath != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookieFilePath = kv[1];
|
||||
break;
|
||||
case "macaroondirectorypath":
|
||||
resultTemp.MacaroonDirectoryPath = kv[1];
|
||||
break;
|
||||
case "certthumbprint":
|
||||
resultTemp.CertificateThumbprint = kv[1];
|
||||
break;
|
||||
case "macaroonfilepath":
|
||||
resultTemp.MacaroonFilePath = kv[1];
|
||||
break;
|
||||
case "api-token":
|
||||
resultTemp.APIToken = kv[1];
|
||||
break;
|
||||
case "access-key":
|
||||
resultTemp.AccessKey = kv[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalServices : List<ExternalService>
|
||||
{
|
||||
public void Load(string cryptoCode, IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (gRPC server)");
|
||||
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (REST server)");
|
||||
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Spark server)");
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
|
||||
}
|
||||
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public ExternalServiceTypes Type { get; set; }
|
||||
public ExternalConnectionString ConnectionString { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServiceName { get; set; }
|
||||
}
|
||||
|
||||
public enum ExternalServiceTypes
|
||||
{
|
||||
LNDRest,
|
||||
LNDGRPC,
|
||||
Spark,
|
||||
RTL,
|
||||
Charge
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class SparkConnectionString
|
||||
{
|
||||
public Uri Server { get; private set; }
|
||||
public string CookeFile { get; private set; }
|
||||
|
||||
public static bool TryParse(string str, out SparkConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new SparkConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
|
||||
if(resultTemp.Server.Scheme == "http")
|
||||
{
|
||||
error = "Insecure transport protocol (http)";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookeFile != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookeFile = kv[1];
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
@ -56,7 +57,9 @@ namespace BTCPayServer.Controllers
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
@ -90,6 +93,24 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
var parsedSounds = vm.Sounds.Split(
|
||||
new[] {"\r\n", "\r", "\n"},
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.SoundsEnabled && !parsedSounds.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
|
||||
}
|
||||
|
||||
var parsedAnimationColors = vm.AnimationColors.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -124,7 +145,9 @@ namespace BTCPayServer.Controllers
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity,
|
||||
Sounds = parsedSounds,
|
||||
AnimationColors = parsedAnimationColors
|
||||
};
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
|
@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -104,7 +104,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
|
@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -48,7 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
@ -102,11 +103,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
|
@ -1,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]
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ArgumentNullException(nameof(directoryPath));
|
||||
Macaroons macaroons = new Macaroons();
|
||||
if (!Directory.Exists(directoryPath))
|
||||
return macaroons;
|
||||
throw new DirectoryNotFoundException("Macaroons directory not found");
|
||||
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
|
||||
{
|
||||
try
|
||||
@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return macaroons;
|
||||
}
|
||||
|
||||
public Macaroons Clone()
|
||||
{
|
||||
return new Macaroons()
|
||||
{
|
||||
AdminMacaroon = AdminMacaroon,
|
||||
InvoiceMacaroon = InvoiceMacaroon,
|
||||
ReadonlyMacaroon = ReadonlyMacaroon
|
||||
};
|
||||
}
|
||||
|
||||
public Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
@ -23,6 +24,7 @@ using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -97,7 +99,12 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a payment request"
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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 =>
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
|
||||
{
|
||||
return _Summaries.TryGetValue(cryptoCode, out summary) &&
|
||||
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll(_RateProviderFactory.Providers
|
||||
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
||||
{
|
||||
if (t.Result.Exception != null)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -46,6 +46,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string TargetCurrency { get; set; } = "BTC";
|
||||
|
||||
[Display(Name = "Set a Target amount ")]
|
||||
[Range(0, double.PositiveInfinity)]
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
|
||||
@ -79,5 +80,11 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
[Display(Name = "Display contribution ranking")]
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
|
||||
|
||||
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
|
||||
public string Sounds{ get; set; }
|
||||
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
|
||||
public string AnimationColors{ get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,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
|
||||
|
@ -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; }
|
||||
|
@ -18,8 +18,9 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[EmailAddress]
|
||||
[Display(Name = "Test Email")]
|
||||
public string TestEmail
|
||||
{
|
||||
get; set;
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
@ -51,10 +51,8 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||
var amountCollected =
|
||||
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||
if (amountCollected >= blob.Amount)
|
||||
var contributions = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
if (contributions.TotalCurrency >= blob.Amount)
|
||||
{
|
||||
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
|
||||
}
|
||||
@ -80,17 +78,14 @@ namespace BTCPayServer.PaymentRequest
|
||||
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||
|
||||
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||
var amountCollected =
|
||||
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||
|
||||
var amountDue = blob.Amount - amountCollected;
|
||||
var paymentStats = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
||||
|
||||
return new ViewPaymentRequestViewModel(pr)
|
||||
{
|
||||
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
|
||||
AmountCollected = amountCollected,
|
||||
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
|
||||
AmountCollected = paymentStats.TotalCurrency,
|
||||
AmountCollectedFormatted = _currencies.FormatCurrency(paymentStats.TotalCurrency, blob.Currency),
|
||||
AmountDue = amountDue,
|
||||
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||
@ -100,6 +95,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
Id = entity.Id,
|
||||
Amount = entity.ProductInformation.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||
Status = entity.GetInvoiceState().ToString(),
|
||||
|
@ -19,11 +19,11 @@ namespace BTCPayServer.Payments.Changelly
|
||||
private readonly bool _showFiat;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Changelly(IHttpClientFactory httpClientFactory, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
{
|
||||
_apisecret = apiSecret;
|
||||
_showFiat = showFiat;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
_httpClient = httpClient;
|
||||
_httpClient.BaseAddress = new Uri(apiUrl);
|
||||
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ namespace BTCPayServer.Payments.Changelly
|
||||
throw new ChangellyException("Changelly not enabled for this store");
|
||||
}
|
||||
|
||||
var changelly = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl, changellySettings.ShowFiat);
|
||||
_clientCache.AddOrReplace(storeId, changelly);
|
||||
return changelly;
|
||||
|
@ -51,6 +51,7 @@ namespace BTCPayServer
|
||||
.ConfigureLogging(l =>
|
||||
{
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
||||
l.AddProvider(new CustomConsoleLogProvider(processor));
|
||||
|
||||
|
@ -30,8 +30,8 @@
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
|
@ -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:
|
||||
|
@ -25,6 +25,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
using static BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
@ -33,7 +34,6 @@ namespace BTCPayServer.Services.Apps
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
CurrencyNameTable _Currencies;
|
||||
private readonly RateFetcher _RateFetcher;
|
||||
private readonly HtmlSanitizer _HtmlSanitizer;
|
||||
private readonly BTCPayNetworkProvider _Networks;
|
||||
public CurrencyNameTable Currencies => _Currencies;
|
||||
@ -41,13 +41,11 @@ namespace BTCPayServer.Services.Apps
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networks,
|
||||
CurrencyNameTable currencies,
|
||||
RateFetcher rateFetcher,
|
||||
HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_Currencies = currencies;
|
||||
_RateFetcher = rateFetcher;
|
||||
_HtmlSanitizer = htmlSanitizer;
|
||||
_Networks = networks;
|
||||
}
|
||||
@ -94,17 +92,8 @@ namespace BTCPayServer.Services.Apps
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed)).ToArray();
|
||||
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
|
||||
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.EnforceTargetAmount);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.EnforceTargetAmount);
|
||||
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
paymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||
pendingPaymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
||||
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
||||
|
||||
var perkCount = invoices
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
|
||||
@ -140,6 +129,7 @@ namespace BTCPayServer.Services.Apps
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = perks,
|
||||
Enabled = settings.Enabled,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
@ -148,19 +138,21 @@ namespace BTCPayServer.Services.Apps
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
PerkCount = perkCount,
|
||||
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
|
||||
Sounds = settings.Sounds,
|
||||
AnimationColors = settings.AnimationColors,
|
||||
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = invoices.Length,
|
||||
CurrentPendingAmount = currentPendingAmount,
|
||||
CurrentAmount = currentAmount,
|
||||
ProgressPercentage = (currentAmount / settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = (currentPendingAmount / settings.TargetAmount) * 100,
|
||||
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
|
||||
LastUpdated = DateTime.Now,
|
||||
PaymentStats = paymentStats,
|
||||
PendingPaymentStats = pendingPaymentStats,
|
||||
PaymentStats = currentPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
||||
PendingPaymentStats = pendingPayments.ToDictionary(c => c.Key.ToString(), c => c.Value.Value),
|
||||
LastResetDate = lastResetDate,
|
||||
NextResetDate = nextResetDate
|
||||
NextResetDate = nextResetDate,
|
||||
CurrentPendingAmount = pendingPayments.TotalCurrency,
|
||||
CurrentAmount = currentPayments.TotalCurrency
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -288,46 +280,20 @@ namespace BTCPayServer.Services.Apps
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
|
||||
public Contributions GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
var result = new List<decimal>();
|
||||
|
||||
var ratesTask = _RateFetcher.FetchRates(
|
||||
stats.Keys
|
||||
.Select((x) => new CurrencyPair(primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
|
||||
.Distinct()
|
||||
.ToHashSet(),
|
||||
rateRules).Select(async rateTask =>
|
||||
{
|
||||
var (key, value) = rateTask;
|
||||
var tResult = await value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null)
|
||||
return;
|
||||
|
||||
foreach (var stat in stats)
|
||||
{
|
||||
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, key.Right,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result.Add((1m / rate.Value) * stat.Value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(ratesTask);
|
||||
|
||||
return result.Sum();
|
||||
}
|
||||
|
||||
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
return invoices
|
||||
var contributions = invoices
|
||||
.Where(p => p.ProductInformation.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(p =>
|
||||
{
|
||||
var contribution = new Contribution();
|
||||
contribution.PaymentMehtodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike);
|
||||
contribution.CurrencyValue = p.ProductInformation.Price;
|
||||
contribution.Value = contribution.CurrencyValue;
|
||||
|
||||
// For hardcap, we count newly created invoices as part of the contributions
|
||||
if (!softcap && p.Status == InvoiceStatus.New)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
|
||||
return new[] { contribution };
|
||||
|
||||
// If the user get a donation via other mean, he can register an invoice manually for such amount
|
||||
// then mark the invoice as complete
|
||||
@ -335,20 +301,38 @@ namespace BTCPayServer.Services.Apps
|
||||
if (payments.Count == 0 &&
|
||||
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||
p.Status == InvoiceStatus.Complete)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
|
||||
return new[] { contribution };
|
||||
|
||||
contribution.CurrencyValue = 0m;
|
||||
contribution.Value = 0m;
|
||||
|
||||
// If an invoice has been marked invalid, remove the contribution
|
||||
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||
p.Status == InvoiceStatus.Invalid)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: 0m) };
|
||||
return new[] { contribution };
|
||||
|
||||
|
||||
// Else, we just sum the payments
|
||||
return payments
|
||||
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
|
||||
.Select(pay =>
|
||||
{
|
||||
var paymentMethodContribution = new Contribution();
|
||||
paymentMethodContribution.PaymentMehtodId = pay.GetPaymentMethodId();
|
||||
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId, _Networks).Rate;
|
||||
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
|
||||
return paymentMethodContribution;
|
||||
})
|
||||
.ToArray();
|
||||
})
|
||||
.GroupBy(p => p.Key)
|
||||
.ToDictionary(p => p.Key, p => p.Select(v => v.Value).Sum());
|
||||
.GroupBy(p => p.PaymentMehtodId)
|
||||
.ToDictionary(p => p.Key, p => new Contribution()
|
||||
{
|
||||
PaymentMehtodId = p.Key,
|
||||
Value = p.Select(v => v.Value).Sum(),
|
||||
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
|
||||
});
|
||||
return new Contributions(contributions);
|
||||
}
|
||||
|
||||
private class PosHolder
|
||||
|
@ -34,6 +34,54 @@ namespace BTCPayServer.Services.Apps
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
public string[] AnimationColors { get; set; } = new string[]
|
||||
{
|
||||
"#FF6138", "#FFBE53", "#2980B9", "#282741"
|
||||
};
|
||||
|
||||
public string[] Sounds { get; set; } = new string[]
|
||||
{
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/dominating.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/doublekill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/doublekill2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/eagleeye.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/firstblood3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/flawless.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/godlike.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/hattrick.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headhunter.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/headshot3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/holyshit.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/killingspree.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/knife3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ludicrouskill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/megakill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/monsterkill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/multikill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/nade.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ownage.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/payback.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/prepare4.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/rampage.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide2.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide3.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/suicide4.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/teamkiller.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/triplekill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/ultrakill.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav"
|
||||
};
|
||||
}
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
|
@ -13,11 +13,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
|
||||
|
@ -35,6 +35,7 @@ namespace BTCPayServer.Services.Mails
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Enable SSL")]
|
||||
public bool EnableSSL
|
||||
{
|
||||
get; set;
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -93,13 +94,13 @@ namespace BTCPayServer.Services.Rates
|
||||
public bool DoNotAutoFetchIfExpired { get; set; }
|
||||
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
|
||||
|
||||
public async Task<LatestFetch> UpdateIfNecessary()
|
||||
public async Task<LatestFetch> UpdateIfNecessary(CancellationToken cancellationToken)
|
||||
{
|
||||
if (NextUpdate <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Fetch();
|
||||
await Fetch(cancellationToken);
|
||||
}
|
||||
catch { } // Exception is inside _Latest
|
||||
return _Latest;
|
||||
@ -108,7 +109,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
LatestFetch _Latest;
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var latest = _Latest;
|
||||
if (!DoNotAutoFetchIfExpired && latest != null && latest.Expiration <= DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1.0))
|
||||
@ -116,7 +117,7 @@ namespace BTCPayServer.Services.Rates
|
||||
Logs.PayServer.LogWarning($"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this.");
|
||||
latest = null;
|
||||
}
|
||||
return (latest ?? (await Fetch())).GetResult();
|
||||
return (latest ?? (await Fetch(cancellationToken))).GetResult();
|
||||
}
|
||||
|
||||
private string GetExchangeName()
|
||||
@ -126,14 +127,14 @@ namespace BTCPayServer.Services.Rates
|
||||
return "???";
|
||||
}
|
||||
|
||||
private async Task<LatestFetch> Fetch()
|
||||
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken)
|
||||
{
|
||||
var previous = _Latest;
|
||||
var fetch = new LatestFetch();
|
||||
fetch.ExchangeName = GetExchangeName();
|
||||
try
|
||||
{
|
||||
var rates = await _Inner.GetRatesAsync();
|
||||
var rates = await _Inner.GetRatesAsync(cancellationToken);
|
||||
fetch.Latest = rates;
|
||||
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
|
||||
fetch.NextRefresh = DateTimeOffset.UtcNow + RefreshRate;
|
||||
|
@ -6,6 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -22,7 +23,7 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public string ExchangeName => BitpayName;
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
.AllRates
|
||||
|
29
BTCPayServer/Services/Rates/ByllsRateProvider.cs
Normal file
29
BTCPayServer/Services/Rates/ByllsRateProvider.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ByllsRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public ByllsRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bylls";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["public_price"]["to_price"].Value<decimal>();
|
||||
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -40,12 +41,12 @@ namespace BTCPayServer.Services.Rates
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
|
||||
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync();
|
||||
return _Inner.GetRatesAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
@ -65,7 +66,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
set
|
||||
{
|
||||
_LocalClient = null;
|
||||
_LocalClient = value;
|
||||
}
|
||||
}
|
||||
HttpClient _LocalClient;
|
||||
@ -111,7 +112,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await HttpClient.SendAsync(request);
|
||||
var resp = await HttpClient.SendAsync(request, cancellationToken);
|
||||
using (resp)
|
||||
{
|
||||
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -31,7 +32,7 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public string ExchangeName => _ExchangeName;
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = await _ExchangeAPI.GetTickersAsync();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -16,13 +17,17 @@ namespace BTCPayServer.Services.Rates
|
||||
_Providers = providers;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var p in _Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await p.GetRatesAsync().ConfigureAwait(false);
|
||||
return await p.GetRatesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch(Exception ex) { Exceptions.Add(ex); }
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -8,6 +9,6 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface IRateProvider
|
||||
{
|
||||
Task<ExchangeRates> GetRatesAsync();
|
||||
Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -28,7 +29,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
set
|
||||
{
|
||||
_LocalClient = null;
|
||||
_LocalClient = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +56,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{ "ZCAD", "CAD" },
|
||||
};
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new ExchangeRates();
|
||||
var symbols = await GetSymbolsAsync();
|
||||
@ -135,7 +136,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null)
|
||||
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append("https://api.kraken.com");
|
||||
@ -147,7 +148,7 @@ namespace BTCPayServer.Services.Rates
|
||||
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
|
||||
}
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
|
||||
var response = await HttpClient.SendAsync(request);
|
||||
var response = await HttpClient.SendAsync(request, cancellationToken);
|
||||
string stringResult = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<T>(stringResult);
|
||||
if (result is JToken json)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -20,7 +21,7 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Instance;
|
||||
}
|
||||
}
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new ExchangeRates());
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -31,9 +32,9 @@ namespace BTCPayServer.Services.Rates
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all", cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -38,12 +39,12 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public RateProviderFactory RateProviderFactory => _rateProviderFactory;
|
||||
|
||||
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
|
||||
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules, CancellationToken cancellationToken)
|
||||
{
|
||||
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
|
||||
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules, cancellationToken).First().Value;
|
||||
}
|
||||
|
||||
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
|
||||
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules, CancellationToken cancellationToken)
|
||||
{
|
||||
if (rules == null)
|
||||
throw new ArgumentNullException(nameof(rules));
|
||||
@ -59,7 +60,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
|
||||
{
|
||||
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange);
|
||||
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, cancellationToken);
|
||||
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
|
||||
}
|
||||
dependentQueries.Add(fetching);
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -21,12 +22,12 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
try
|
||||
{
|
||||
return await _inner.GetRatesAsync();
|
||||
return await _inner.GetRatesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -109,8 +110,9 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
// Handmade providers
|
||||
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
|
||||
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
|
||||
@ -167,18 +169,19 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
// Add other exchanges supported here
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
|
||||
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
|
||||
public async Task<QueryRateResult> QueryRates(string exchangeName)
|
||||
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
|
||||
{
|
||||
Providers.TryGetValue(exchangeName, out var directProvider);
|
||||
directProvider = directProvider ?? NullRateProvider.Instance;
|
||||
|
||||
var wrapper = new WrapperRateProvider(directProvider);
|
||||
var value = await wrapper.GetRatesAsync();
|
||||
var value = await wrapper.GetRatesAsync(cancellationToken);
|
||||
return new QueryRateResult()
|
||||
{
|
||||
Latency = wrapper.Latency,
|
||||
|
@ -1,4 +1,4 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@model UpdateCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Crowdfund";
|
||||
@ -67,8 +67,8 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="StartDate" class="control-label"></label>
|
||||
<div class="input-group ">
|
||||
<input asp-for="StartDate" class="form-control datetime"/>
|
||||
<div class="input-group-append only-for-js">
|
||||
<input asp-for="StartDate" class="form-control datetime" placeholder="No start date has been set for this crowdfund"/>
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||
<span class=" fa fa-times"></span>
|
||||
@ -93,8 +93,8 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="EndDate" class="control-label"></label>
|
||||
<div class="input-group ">
|
||||
<input asp-for="EndDate" class="form-control datetime" />
|
||||
<div class="input-group-append only-for-js">
|
||||
<input asp-for="EndDate" class="form-control datetime" placeholder="No end date has been set for this crowdfund" />
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||
<span class=" fa fa-times"></span>
|
||||
@ -167,11 +167,23 @@
|
||||
<input asp-for="SoundsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Sounds"></label>
|
||||
<textarea asp-for="Sounds" class="form-control"></textarea>
|
||||
<span asp-validation-for="Sounds" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AnimationsEnabled"></label>
|
||||
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="AnimationColors"></label>
|
||||
<textarea asp-for="AnimationColors" class="form-control"></textarea>
|
||||
<span asp-validation-for="AnimationColors" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisqusEnabled"></label>
|
||||
<input asp-for="DisqusEnabled" type="checkbox" class="form-check"/>
|
||||
|
@ -1,4 +1,10 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
@if(!Model.Enabled)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert" style="text-align: center;">
|
||||
This crowdfund page is not publically viewable!
|
||||
</div>
|
||||
}
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
|
||||
|
@ -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">
|
||||
|
@ -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">⚡</span>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="payment__spinner">
|
||||
<partial name="Checkout-Spinner" />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Password"></label>
|
||||
<input asp-for="Settings.Password" type="password" class="form-control" />
|
||||
<input asp-for="Settings.Password" type="password" class="form-control" value="@Model.Settings.Password" />
|
||||
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
<div class="form-group">
|
||||
<h5>QR Code connection</h5>
|
||||
<p>
|
||||
<span>You can use QR Code to connect to your clightning from your mobile.<br /></span>
|
||||
<span>You can use QR Code to connect to your @Model.WalletName from your mobile.<br /></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -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>
|
||||
|
@ -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">×</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>
|
||||
|
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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"];
|
||||
|
@ -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;
|
||||
|
50
BTCPayServer/wwwroot/locales/da-DK.json
Normal file
50
BTCPayServer/wwwroot/locales/da-DK.json
Normal 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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -16,6 +16,6 @@
|
||||
}
|
||||
|
||||
|
||||
.only-for-js{
|
||||
.only-for-js, .input-group-clear{
|
||||
display: none;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user