Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
a7ad71d492 | |||
18977f7265 | |||
8a88b44e98 | |||
c9e5fe42ba | |||
56dffbf514 | |||
0e1fac3773 | |||
e7c06880a8 | |||
39463a3202 | |||
36136f0f0f | |||
22e5b2869a | |||
fc3f32a4e0 | |||
3b0914e89e | |||
76cd9a7b25 | |||
0934bebf7b | |||
cd1a4c4749 | |||
8075273ec8 | |||
97b59be9bf | |||
b87ec4f3d9 | |||
3822358096 | |||
ba7e8cfe78 | |||
41978f1c59 | |||
e75e691404 | |||
a22216fd04 | |||
6900e16aa4 | |||
10c981b2a0 | |||
5f940df1b4 | |||
3f85918a0c | |||
e4299c09ea | |||
e864cf35f7 | |||
3652866660 | |||
0421004616 | |||
6936b034cb | |||
73ed4003a3 | |||
5cb8cdd511 | |||
195b5fdd1a | |||
d19b78b6cc | |||
9bbc05c3a7 | |||
7df3c86649 | |||
c6e0a923bb | |||
637fe1727b | |||
fd087bbeb8 | |||
2f515e1cc0 | |||
84cd9e570f | |||
415cde1629 | |||
18986faca8 | |||
b099f93c78 | |||
81afe397be | |||
f869c06aee |
@ -72,7 +72,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
string chain = ChainType.Regtest.ToNetwork().Name;
|
||||
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Tests
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
|
@ -632,8 +632,8 @@ namespace BTCPayServer.Tests
|
||||
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
|
||||
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
|
||||
rates.Add(bitflyer);
|
||||
|
||||
foreach(var rate in rates)
|
||||
|
||||
foreach (var rate in rates)
|
||||
{
|
||||
Assert.Single(rates.Where(r => r == rate));
|
||||
}
|
||||
@ -873,7 +873,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanParseDerivationScheme()
|
||||
{
|
||||
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
|
||||
var parser = new DerivationSchemeParser(Network.TestNet);
|
||||
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
|
||||
// Passing electrum stuff
|
||||
// Native
|
||||
@ -1185,6 +1185,26 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckQuadrigacxRateProvider()
|
||||
{
|
||||
var quadri = new QuadrigacxRateProvider("BTC");
|
||||
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
||||
|
||||
quadri = new QuadrigacxRateProvider("LTC");
|
||||
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
|
@ -46,7 +46,7 @@ services:
|
||||
- lightning-charged
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.34
|
||||
image: nicolasdorier/nbxplorer:1.0.2.0
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
|
@ -14,34 +14,28 @@ namespace BTCPayServer
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
|
||||
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
|
||||
{
|
||||
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
|
||||
chainType == ChainType.Regtest ? Network.RegTest :
|
||||
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
|
||||
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.ChainType = chainType;
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
|
||||
chainType == ChainType.Regtest ? 23002 :
|
||||
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
||||
chainType == NetworkType.Regtest ? 23002 :
|
||||
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
|
||||
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public ChainType ChainType { get; internal set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
|
@ -20,15 +20,15 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,20 +12,18 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitDogecoin()
|
||||
{
|
||||
NBitcoin.Altcoins.Dogecoin.EnsureRegistered();
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("3'") : new KeyPath("1'")
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,21 +12,19 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitLitecoin()
|
||||
{
|
||||
NBitcoin.Altcoins.Litecoin.EnsureRegistered();
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("1'")
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ namespace BTCPayServer
|
||||
|
||||
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
|
||||
{
|
||||
ChainType = filtered.ChainType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.ChainType);
|
||||
NetworkType = filtered.NetworkType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
|
||||
_Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
|
||||
foreach (var network in filtered._Networks)
|
||||
@ -40,11 +40,11 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
public ChainType ChainType { get; set; }
|
||||
public BTCPayNetworkProvider(ChainType chainType)
|
||||
public NetworkType NetworkType { get; private set; }
|
||||
public BTCPayNetworkProvider(NetworkType networkType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||
ChainType = chainType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitDogecoin();
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.85</Version>
|
||||
<Version>1.0.1.91</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -39,10 +39,10 @@
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.0.11" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.24" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\checkout\js\core.js" />
|
||||
<None Include="wwwroot\main\js\creative.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public ChainType ChainType
|
||||
public NetworkType NetworkType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -51,15 +51,15 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
ChainType = DefaultConfiguration.GetChainType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
|
||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
|
||||
foreach (var chain in supportedChains)
|
||||
{
|
||||
if (NetworkProvider.GetNetwork(chain) == null)
|
||||
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(ChainType.Main);
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
@ -48,12 +48,12 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
@ -69,7 +69,7 @@ namespace BTCPayServer.Configuration
|
||||
return Path.Combine(chainDir, fileName);
|
||||
}
|
||||
|
||||
public static ChainType GetChainType(IConfiguration conf)
|
||||
public static NetworkType GetNetworkType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
@ -79,17 +79,18 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.ToChainType();
|
||||
return n.NetworkType;
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
|
||||
|
||||
return net;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var networkType = GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
@ -102,7 +103,7 @@ namespace BTCPayServer.Configuration
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
|
||||
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
@ -116,7 +117,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,54 +85,66 @@ namespace BTCPayServer.Controllers
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var payments = invoice
|
||||
var onChainPayments = invoice
|
||||
.GetPayments()
|
||||
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
.Select<PaymentEntity, Task<object>>(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
paymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(paymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = paymentData.ConfirmationCount;
|
||||
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
||||
return new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
};
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = paymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
await Task.WhenAll(onChainPayments);
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
|
||||
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
@ -253,7 +265,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
return model;
|
||||
}
|
||||
|
||||
@ -272,17 +284,6 @@ namespace BTCPayServer.Controllers
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
@ -373,7 +374,7 @@ namespace BTCPayServer.Controllers
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = Prettify(invoice.InvoiceTime),
|
||||
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
@ -386,30 +387,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string Prettify(DateTimeOffset invoiceTime)
|
||||
{
|
||||
var ago = DateTime.UtcNow - invoiceTime;
|
||||
|
||||
if (ago.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
|
||||
}
|
||||
if (ago.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalMinutes} minute{Plural((int)ago.TotalMinutes)} ago";
|
||||
}
|
||||
if (ago.Days < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalHours} hour{Plural((int)ago.TotalHours)} ago";
|
||||
}
|
||||
return $"{(int)ago.TotalDays} day{Plural((int)ago.TotalDays)} ago";
|
||||
}
|
||||
|
||||
private string Plural(int totalDays)
|
||||
{
|
||||
return totalDays > 1 ? "s" : string.Empty;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
@ -450,7 +427,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if(StatusMessage != null)
|
||||
if (StatusMessage != null)
|
||||
{
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
@ -24,45 +25,47 @@ namespace BTCPayServer.Controllers
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
private CssThemeManager _CssThemeManager;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
SettingsRepository settingsRepository,
|
||||
CssThemeManager cssThemeManager)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
public async Task<IActionResult> Rates()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
return View(new RatesViewModel()
|
||||
|
||||
var vm = new RatesViewModel()
|
||||
{
|
||||
CacheMinutes = rates.CacheInMinutes,
|
||||
PrivateKey = rates.PrivateKey,
|
||||
PublicKey = rates.PublicKey
|
||||
});
|
||||
|
||||
};
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
class TestCoinAverageAuthenticator : ICoinAverageAuthenticator
|
||||
private static async Task FetchRateLimits(RatesViewModel vm)
|
||||
{
|
||||
private RatesSetting settings;
|
||||
|
||||
public TestCoinAverageAuthenticator(RatesSetting settings)
|
||||
var coinAverage = GetCoinaverageService(vm, false);
|
||||
if (coinAverage != null)
|
||||
{
|
||||
this.settings = settings;
|
||||
}
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var sig = settings.GetCoinAverageSignature();
|
||||
if (sig != null)
|
||||
message.Headers.Add("X-signature", settings.GetCoinAverageSignature());
|
||||
return Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Rates(RatesViewModel vm)
|
||||
@ -73,24 +76,38 @@ namespace BTCPayServer.Controllers
|
||||
rates.CacheInMinutes = vm.CacheMinutes;
|
||||
try
|
||||
{
|
||||
if (rates.GetCoinAverageSignature() != null)
|
||||
{
|
||||
await new CoinAverageRateProvider("BTC")
|
||||
{ Authenticator = new TestCoinAverageAuthenticator(rates) }.TestAuthAsync();
|
||||
}
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if(service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
await _SettingsRepository.UpdateSetting(rates);
|
||||
((BTCPayRateProviderFactory)_RateProviderFactory).CacheSpan = TimeSpan.FromMinutes(vm.CacheMinutes);
|
||||
StatusMessage = "Rate settings successfully updated";
|
||||
return RedirectToAction(nameof(Rates));
|
||||
}
|
||||
|
||||
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
|
||||
{
|
||||
var settings = new CoinAverageSettings()
|
||||
{
|
||||
KeyPair = (vm.PublicKey, vm.PrivateKey)
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider("BTC")
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[Route("server/users")]
|
||||
public IActionResult ListUsers()
|
||||
{
|
||||
@ -115,6 +132,7 @@ namespace BTCPayServer.Controllers
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UserViewModel();
|
||||
userVM.Id = user.Id;
|
||||
userVM.Email = user.Email;
|
||||
userVM.IsAdmin = IsAdmin(roles);
|
||||
return View(userVM);
|
||||
}
|
||||
@ -201,7 +219,25 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies upadated successfully";
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/theme")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Theme(ThemeSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
// TODO: remove controller/class-level property and have only reference to
|
||||
// CssThemeManager here in this method
|
||||
_CssThemeManager.Update(settings);
|
||||
TempData["StatusMessage"] = "Theme settings updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -47,7 +48,8 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env)
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
@ -64,7 +66,9 @@ namespace BTCPayServer.Controllers
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
_CoinAverage = coinAverage;
|
||||
}
|
||||
CoinAverageSettings _CoinAverage;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
@ -237,7 +241,7 @@ namespace BTCPayServer.Controllers
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
|
||||
if(!ModelState.IsValid)
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
@ -273,6 +277,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
@ -283,7 +288,6 @@ namespace BTCPayServer.Controllers
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -325,6 +329,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -371,14 +376,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
||||
{
|
||||
using (HttpClient client = new HttpClient())
|
||||
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var rate = await client.GetAsync(model.RateSource);
|
||||
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,9 +396,14 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private (String DisplayName, String Name)[] GetSupportedExchanges()
|
||||
{
|
||||
return new[] { ("Coin Average", "coinaverage") }.Concat(_CoinAverage.AvailableExchanges).ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
|
@ -13,13 +13,11 @@ namespace BTCPayServer
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public ChainType ChainType { get; set; }
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
|
||||
public DerivationSchemeParser(Network expectedNetwork)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
ChainType = chainType;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
@ -78,7 +76,7 @@ namespace BTCPayServer
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
|
@ -28,11 +28,44 @@ using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Models;
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string Prettify(this TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
|
||||
}
|
||||
if (timeSpan.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
|
||||
}
|
||||
if (timeSpan.Days < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
|
||||
}
|
||||
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
}
|
||||
|
||||
private static string Plural(int totalDays)
|
||||
{
|
||||
return totalDays > 1 ? "s" : string.Empty;
|
||||
}
|
||||
|
||||
public static string PrettyPrint(this TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
public static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
|
62
BTCPayServer/HostedServices/CssThemeManager.cs
Normal file
62
BTCPayServer/HostedServices/CssThemeManager.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CssThemeManager
|
||||
{
|
||||
public CssThemeManager(SettingsRepository settingsRepository)
|
||||
{
|
||||
Update(settingsRepository);
|
||||
}
|
||||
|
||||
private async void Update(SettingsRepository settingsRepository)
|
||||
{
|
||||
var data = (await settingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
Update(data);
|
||||
}
|
||||
|
||||
public void Update(ThemeSettings data)
|
||||
{
|
||||
UpdateBootstrap(data.BootstrapCssUri);
|
||||
UpdateCreativeStart(data.CreativeStartCssUri);
|
||||
}
|
||||
|
||||
private string _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
|
||||
public string BootstrapUri
|
||||
{
|
||||
get { return _bootstrapUri; }
|
||||
}
|
||||
public void UpdateBootstrap(string newUri)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(newUri))
|
||||
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v="+ DateTime.Now.Ticks;
|
||||
else
|
||||
_bootstrapUri = newUri;
|
||||
}
|
||||
|
||||
private string _creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
public string CreativeStartUri
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
public void UpdateCreativeStart(string newUri)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(newUri))
|
||||
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_creativeStartUri = newUri;
|
||||
}
|
||||
}
|
||||
}
|
@ -184,8 +184,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if(status != null && error == null)
|
||||
{
|
||||
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
|
||||
if(status.NetworkType != _Network.NBitcoinNetwork.NetworkType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.NetworkType}, expected: {_Network.NBitcoinNetwork.NetworkType})";
|
||||
}
|
||||
|
||||
if (error != null)
|
||||
|
@ -8,6 +8,9 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -15,41 +18,89 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
CoinAverageSettings coinAverageSettings,
|
||||
IRateProviderFactory rateProviderFactory)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_coinAverageSettings = coinAverageSettings;
|
||||
}
|
||||
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
|
||||
List<Task> _Tasks = new List<Task>();
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
_Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token));
|
||||
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
async void Init()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
|
||||
//string[] availableExchanges = null;
|
||||
//// So we don't run this in testing
|
||||
//if(_RateProviderFactory is BTCPayRateProviderFactory)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync();
|
||||
// }
|
||||
// catch(Exception ex)
|
||||
// {
|
||||
// Logs.PayServer.LogWarning(ex, "Failed to get exchange tickers");
|
||||
// }
|
||||
//}
|
||||
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, caller + " failed");
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
|
||||
{
|
||||
return Timer(async () =>
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
_coinAverageSettings.AvailableExchanges = tickers
|
||||
.Exchanges
|
||||
.Select(c => (c.DisplayName, c.Name))
|
||||
.ToArray();
|
||||
await Task.Delay(TimeSpan.FromHours(5), cancellation);
|
||||
}, cancellation);
|
||||
}
|
||||
|
||||
Task RefreshCoinAverageSettings(CancellationToken cancellation)
|
||||
{
|
||||
return Timer(async () =>
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(cancellation);
|
||||
}, cancellation);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, BTCPayCoinAverageAuthenticator>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -137,6 +138,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<CssThemeManager>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
@ -160,7 +162,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
{
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().NetworkType == NetworkType.Mainnet)
|
||||
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
|
@ -27,7 +27,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
public class Payment
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Crypto { get; set; }
|
||||
public string Confirmations
|
||||
{
|
||||
get; set;
|
||||
@ -72,7 +72,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get; set;
|
||||
} = new List<CryptoPayment>();
|
||||
|
||||
public List<Payment> Payments { get; set; } = new List<Payment>();
|
||||
public List<Payment> OnChainPayments { get; set; } = new List<Payment>();
|
||||
public List<OffChainPayment> OffChainPayments { get; set; } = new List<OffChainPayment>();
|
||||
public class OffChainPayment
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string BOLT11 { get; set; }
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
@ -14,5 +15,6 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
[Display(Name = "Cache the rates for ... minutes")]
|
||||
[Range(0, 60)]
|
||||
public int CacheMinutes { get; set; }
|
||||
public GetRateLimitsResponse RateLimits { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public class DerivationScheme
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
@ -44,6 +49,17 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? "coinaverage";
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
}
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using NBXplorer;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -20,13 +21,13 @@ namespace BTCPayServer.Services
|
||||
Build = "Release";
|
||||
#endif
|
||||
Environment = env;
|
||||
ChainType = provider.NBXplorerNetworkProvider.ChainType;
|
||||
NetworkType = provider.NetworkType;
|
||||
}
|
||||
public IHostingEnvironment Environment
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ChainType ChainType { get; set; }
|
||||
public NetworkType NetworkType { get; set; }
|
||||
public string Version
|
||||
{
|
||||
get; set;
|
||||
@ -40,7 +41,7 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
get
|
||||
{
|
||||
return ChainType == ChainType.Regtest && Environment.IsDevelopment();
|
||||
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
|
||||
}
|
||||
}
|
||||
public override string ToString()
|
||||
|
@ -101,7 +101,7 @@ namespace BTCPayServer.Services
|
||||
var pubKey = await ledger.GetWalletPubKeyAsync(account);
|
||||
if (pubKey.Address.Network != network.NBitcoinNetwork)
|
||||
{
|
||||
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
|
||||
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
|
||||
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
|
||||
}
|
||||
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
|
||||
|
@ -65,9 +65,17 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
|
||||
{
|
||||
List<IRateProvider> providers = new List<IRateProvider>();
|
||||
|
||||
if(exchange == "quadrigacx")
|
||||
{
|
||||
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
|
||||
}
|
||||
|
||||
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
|
||||
coinAverage.Exchange = exchange;
|
||||
return coinAverage;
|
||||
providers.Add(coinAverage);
|
||||
return new FallbackRateProvider(providers.ToArray());
|
||||
}
|
||||
|
||||
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
|
||||
|
@ -59,41 +59,11 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public class RatesSetting
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
[DefaultValue(15)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int CacheInMinutes { get; set; } = 15;
|
||||
|
||||
public string GetCoinAverageSignature()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PublicKey) || string.IsNullOrEmpty(PrivateKey))
|
||||
return null;
|
||||
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
|
||||
var payload = timestamp + "." + PublicKey;
|
||||
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
|
||||
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
|
||||
return payload + "." + digestValueHex;
|
||||
}
|
||||
}
|
||||
public class BTCPayCoinAverageAuthenticator : ICoinAverageAuthenticator
|
||||
{
|
||||
private SettingsRepository settingsRepo;
|
||||
|
||||
public BTCPayCoinAverageAuthenticator(SettingsRepository settingsRepo)
|
||||
{
|
||||
this.settingsRepo = settingsRepo;
|
||||
}
|
||||
public async Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var settings = (await settingsRepo.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
var signature = settings.GetCoinAverageSignature();
|
||||
if (signature != null)
|
||||
{
|
||||
message.Headers.Add("X-signature", signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICoinAverageAuthenticator
|
||||
@ -200,9 +170,45 @@ namespace BTCPayServer.Services.Rates
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<GetRateLimitsResponse> GetRateLimitsAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/info/ratelimits");
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await _Client.SendAsync(request);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
var response = new GetRateLimitsResponse();
|
||||
response.CounterReset = TimeSpan.FromSeconds(jobj["counter_reset"].Value<int>());
|
||||
var totalPeriod = jobj["total_period"].Value<string>();
|
||||
if (totalPeriod == "24h")
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromHours(24);
|
||||
}
|
||||
else if (totalPeriod == "30d")
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromDays(30);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromSeconds(jobj["total_period"].Value<int>());
|
||||
}
|
||||
response.RequestsLeft = jobj["requests_left"].Value<int>();
|
||||
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await _Client.SendAsync(request);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
@ -221,4 +227,12 @@ namespace BTCPayServer.Services.Rates
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public class GetRateLimitsResponse
|
||||
{
|
||||
public TimeSpan CounterReset { get; set; }
|
||||
public int RequestsLeft { get; set; }
|
||||
public int RequestsPerPeriod { get; set; }
|
||||
public TimeSpan TotalPeriod { get; set; }
|
||||
}
|
||||
}
|
||||
|
119
BTCPayServer/Services/Rates/CoinAverageSettings.cs
Normal file
119
BTCPayServer/Services/Rates/CoinAverageSettings.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
|
||||
{
|
||||
CoinAverageSettings _Settings;
|
||||
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
|
||||
{
|
||||
_Settings = settings;
|
||||
}
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
return _Settings.AddHeader(message);
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
|
||||
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
|
||||
|
||||
public CoinAverageSettings()
|
||||
{
|
||||
//GENERATED BY:
|
||||
//StringBuilder b = new StringBuilder();
|
||||
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
|
||||
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
|
||||
//{
|
||||
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
|
||||
//}
|
||||
//b.AppendLine("}.ToArray()");
|
||||
|
||||
AvailableExchanges = new[] {
|
||||
(DisplayName: "BitBargain", Name: "bitbargain"),
|
||||
(DisplayName: "Tidex", Name: "tidex"),
|
||||
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
||||
(DisplayName: "EtherDelta", Name: "etherdelta"),
|
||||
(DisplayName: "Kraken", Name: "kraken"),
|
||||
(DisplayName: "BitBay", Name: "bitbay"),
|
||||
(DisplayName: "Independent Reserve", Name: "independentreserve"),
|
||||
(DisplayName: "Exmoney", Name: "exmoney"),
|
||||
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
|
||||
(DisplayName: "Huobi", Name: "huobi"),
|
||||
(DisplayName: "GDAX", Name: "gdax"),
|
||||
(DisplayName: "Coincheck", Name: "coincheck"),
|
||||
(DisplayName: "Bittylicious", Name: "bittylicious"),
|
||||
(DisplayName: "Gemini", Name: "gemini"),
|
||||
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
|
||||
(DisplayName: "Bit2C", Name: "bit2c"),
|
||||
(DisplayName: "Luno", Name: "luno"),
|
||||
(DisplayName: "Negocie Coins", Name: "negociecoins"),
|
||||
(DisplayName: "FYB-SE", Name: "fybse"),
|
||||
(DisplayName: "Hitbtc", Name: "hitbtc"),
|
||||
(DisplayName: "Bitex.la", Name: "bitex"),
|
||||
(DisplayName: "Korbit", Name: "korbit"),
|
||||
(DisplayName: "itBit", Name: "itbit"),
|
||||
(DisplayName: "Okex", Name: "okex"),
|
||||
(DisplayName: "Bitsquare", Name: "bitsquare"),
|
||||
(DisplayName: "Bitfinex", Name: "bitfinex"),
|
||||
(DisplayName: "CoinMate", Name: "coinmate"),
|
||||
(DisplayName: "Bitstamp", Name: "bitstamp"),
|
||||
(DisplayName: "Cryptonit", Name: "cryptonit"),
|
||||
(DisplayName: "Foxbit", Name: "foxbit"),
|
||||
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
|
||||
(DisplayName: "Poloniex", Name: "poloniex"),
|
||||
(DisplayName: "Bit-Z", Name: "bitz"),
|
||||
(DisplayName: "Liqui", Name: "liqui"),
|
||||
(DisplayName: "BitKonan", Name: "bitkonan"),
|
||||
(DisplayName: "Kucoin", Name: "kucoin"),
|
||||
(DisplayName: "Binance", Name: "binance"),
|
||||
(DisplayName: "Rock Trading", Name: "rocktrading"),
|
||||
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
|
||||
(DisplayName: "Coinsecure", Name: "coinsecure"),
|
||||
(DisplayName: "Coinfloor", Name: "coinfloor"),
|
||||
(DisplayName: "bitFlyer", Name: "bitflyer"),
|
||||
(DisplayName: "BTCTurk", Name: "btcturk"),
|
||||
(DisplayName: "Bittrex", Name: "bittrex"),
|
||||
(DisplayName: "CampBX", Name: "campbx"),
|
||||
(DisplayName: "Zaif", Name: "zaif"),
|
||||
(DisplayName: "FYB-SG", Name: "fybsg"),
|
||||
(DisplayName: "Quoine", Name: "quoine"),
|
||||
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
}.ToArray();
|
||||
}
|
||||
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var signature = GetCoinAverageSignature();
|
||||
if (signature != null)
|
||||
{
|
||||
message.Headers.Add("X-signature", signature);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetCoinAverageSignature()
|
||||
{
|
||||
var keyPair = KeyPair;
|
||||
if (!keyPair.HasValue)
|
||||
return null;
|
||||
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
|
||||
return null;
|
||||
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
|
||||
var payload = timestamp + "." + keyPair.Value.PublicKey;
|
||||
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
|
||||
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
|
||||
return payload + "." + digestValueHex;
|
||||
}
|
||||
}
|
||||
}
|
65
BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs
Normal file
65
BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class QuadrigacxRateProvider : IRateProvider
|
||||
{
|
||||
public QuadrigacxRateProvider(string crypto)
|
||||
{
|
||||
CryptoCode = crypto;
|
||||
}
|
||||
public string CryptoCode { get; set; }
|
||||
static HttpClient _Client = new HttpClient();
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return await GetRatesAsyncCore(CryptoCode, currency);
|
||||
}
|
||||
|
||||
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
if (!TryToDecimal(rates, out var result))
|
||||
throw new RateUnavailableException(currency);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryToDecimal(JObject p, out decimal v)
|
||||
{
|
||||
v = 0.0m;
|
||||
JToken token = p.Property("bid")?.Value;
|
||||
if (token == null)
|
||||
return false;
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
List<Rate> result = new List<Rate>();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
var rate = new Rate();
|
||||
var splitted = prop.Name.Split('_');
|
||||
var crypto = splitted[0].ToUpperInvariant();
|
||||
if (crypto != CryptoCode)
|
||||
continue;
|
||||
rate.Currency = splitted[1].ToUpperInvariant();
|
||||
TryToDecimal((JObject)prop.Value, out var v);
|
||||
rate.Value = v;
|
||||
result.Add(rate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -51,6 +52,22 @@ namespace BTCPayServer.Services
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyCollection<TaskCompletionSource<bool>> value;
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
if(_Subscriptions.TryGetValue(typeof(T), out value))
|
||||
{
|
||||
_Subscriptions.Remove(typeof(T));
|
||||
}
|
||||
}
|
||||
if(value != null)
|
||||
{
|
||||
foreach(var v in value)
|
||||
{
|
||||
v.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T Deserialize<T>(string value)
|
||||
@ -62,5 +79,35 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj);
|
||||
}
|
||||
|
||||
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
|
||||
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (cancellation.Register(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
catch { }
|
||||
}))
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
_Subscriptions.Add(typeof(T), tcs);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
tcs.Task.ContinueWith(_ =>
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
_Subscriptions.Remove(typeof(T), tcs);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
BTCPayServer/Services/ThemesSettings.cs
Normal file
17
BTCPayServer/Services/ThemesSettings.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class ThemeSettings
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public string BootstrapCssUri { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public string CreativeStartCssUri { get; set; }
|
||||
}
|
||||
}
|
37
BTCPayServer/SynchronizationContextRemover.cs
Normal file
37
BTCPayServer/SynchronizationContextRemover.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public struct SynchronizationContextRemover : INotifyCompletion
|
||||
{
|
||||
public bool IsCompleted => SynchronizationContext.Current == null;
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
var prev = SynchronizationContext.Current;
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(null);
|
||||
continuation();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(prev);
|
||||
}
|
||||
}
|
||||
|
||||
public SynchronizationContextRemover GetAwaiter()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public void GetResult()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
</head>
|
||||
<body class="h-100">
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
|
@ -6,8 +6,9 @@
|
||||
|
||||
<header class="masthead">
|
||||
<div class="header-content">
|
||||
<div class="header-content-inner">
|
||||
<h1 id="homeHeading" style="padding-bottom:30px;">Welcome to BTCPay Server</h1>
|
||||
<div class="header-content-inner text-white">
|
||||
<h1>Welcome to BTCPay Server</h1>
|
||||
<hr />
|
||||
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business. The API is compatible with Bitpay service to allow seamless migration.</p>
|
||||
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://github.com/btcpayserver/btcpayserver-doc">Getting started</a>
|
||||
</div>
|
||||
@ -28,21 +29,21 @@
|
||||
<div class="col-lg-4 col-md-6 text-center">
|
||||
<div class="service-box">
|
||||
<img src="~/img/lock-logo.png" />
|
||||
<h3>Secure</h3>
|
||||
<h3 class="text-dark">Secure</h3>
|
||||
<p class="text-muted">The payment server does not need to know your private keys, so your money can't be stolen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 text-center">
|
||||
<div class="service-box">
|
||||
<img src="~/img/qr-logo.png" />
|
||||
<h3>Easy</h3>
|
||||
<h3 class="text-dark">Easy</h3>
|
||||
<p class="text-muted">A user-friendly Bitcoin checkout page for your customers.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 text-center">
|
||||
<div class="service-box">
|
||||
<img src="~/img/money-logo.png" />
|
||||
<h3>Visibility</h3>
|
||||
<h3 class="text-dark">Visibility</h3>
|
||||
<p class="text-muted">Manage, generate reports, and search for your invoices easily.</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,7 +53,7 @@
|
||||
|
||||
|
||||
|
||||
<div class="call-to-action bg-dark">
|
||||
<div class="call-to-action bg-dark text-white">
|
||||
<div class="container text-center">
|
||||
<h2>Video tutorials</h2>
|
||||
<div class="row">
|
||||
@ -102,12 +103,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="call-to-action bg-dark">
|
||||
<div class="call-to-action bg-dark text-white">
|
||||
<div class="container text-center">
|
||||
<h2>Donate</h2>
|
||||
<p>Donation to this address will be reinvested into the development of this tool</p>
|
||||
<img src="~/img/donation.jpg">
|
||||
<p style="font-size:10px">3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
|
||||
<p><img src="~/img/donation.jpg"></p>
|
||||
<p>3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -117,7 +118,10 @@
|
||||
<div class="col-lg-8 mx-auto text-center">
|
||||
<h2 class="section-heading">Let's Get In Touch!</h2>
|
||||
<hr class="primary">
|
||||
<p>An open source project is nothing without its community, come and get in touch with us!</p>
|
||||
<p>
|
||||
An open source project is nothing without its community<br />
|
||||
Come and get in touch with us!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@ -4,14 +4,8 @@
|
||||
}
|
||||
|
||||
<style type="text/css">
|
||||
.overflowbox {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.money {
|
||||
text-align: right;
|
||||
.linethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -28,14 +22,13 @@
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
<p>Invoice details</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Information</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tr>
|
||||
<th>Store</th>
|
||||
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
|
||||
@ -93,10 +86,9 @@
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>Buyer information</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tr>
|
||||
<th>Name
|
||||
<th>
|
||||
<th>Name</th>
|
||||
<td>@Model.BuyerInformation.BuyerName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -134,7 +126,7 @@
|
||||
</table>
|
||||
|
||||
<h3>Product information</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@Model.ProductInformation.ItemCode</td>
|
||||
@ -153,77 +145,105 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Paid summary</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Payment method</th>
|
||||
<th>Address</th>
|
||||
<th class="money">Rate</th>
|
||||
<th class="money">Paid</th>
|
||||
<th class="money">Due</th>
|
||||
<th class="text-right">Rate</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
<td>@payment.Address</td>
|
||||
<td class="money">@payment.Rate</td>
|
||||
<td class="money">@payment.Paid</td>
|
||||
<td class="money">@payment.Due</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
<td>@payment.Address</td>
|
||||
<td class="text-right">@payment.Rate</td>
|
||||
<td class="text-right">@payment.Paid</td>
|
||||
<td class="text-right">@payment.Due</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Payments</h3>
|
||||
<table class="table table-sm">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th style="text-align:right;">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.Payments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "text-decoration: line-through;" : "";
|
||||
<tr>
|
||||
<td style="@replaced">@payment.PaymentMethod</td>
|
||||
<td style="@replaced">@payment.DepositAddress</td>
|
||||
<td style="@replaced"><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td style="text-align:right;">@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@if(Model.OnChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>On-Chain payments</h3>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.OnChainPayments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "class='linethrough'" : "";
|
||||
<tr>
|
||||
<td @replaced>@payment.Crypto</td>
|
||||
<td @replaced>@payment.DepositAddress</td>
|
||||
<td @replaced><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(Model.OffChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Off-Chain payments</h3>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>BOLT11</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.OffChainPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.BOLT11</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Addresses</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Payment method</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var address in Model.Addresses)
|
||||
@foreach (var address in Model.Addresses)
|
||||
{
|
||||
var current = address.Current ? "font-weight: bold;" : "";
|
||||
<tr>
|
||||
<td style="width:100px;@current">@address.PaymentMethod</td>
|
||||
<td style="max-width:100px;overflow:hidden;@current">@address.Destination</td>
|
||||
</tr>
|
||||
}
|
||||
var current = address.Current ? "class='font-weight-bold'" : "";
|
||||
<tr>
|
||||
<td>@address.PaymentMethod</td>
|
||||
<td @current>@address.Destination</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -232,7 +252,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Events</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
@ -240,7 +260,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var evt in Model.Events)
|
||||
@foreach (var evt in Model.Events)
|
||||
{
|
||||
<tr>
|
||||
<td>@evt.Timestamp</td>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model ChangePasswordViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Change password";
|
||||
ViewData.AddActivePage(ManageNavPages.ChangePassword);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Change password");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,6 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "Disable two-factor authentication (2FA)";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Disable two-factor authentication (2FA)");
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model EnableAuthenticatorViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Enable authenticator";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Enable authenticator");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model ExternalLoginsViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage your external logins";
|
||||
ViewData.AddActivePage(ManageNavPages.ExternalLogins);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.ExternalLogins, "Manage your external logins");
|
||||
}
|
||||
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model GenerateRecoveryCodesViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Recovery codes";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Recovery codes");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model IndexViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Profile";
|
||||
ViewData.AddActivePage(ManageNavPages.Index);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.Index, "Profile");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -2,41 +2,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Views.Manage
|
||||
{
|
||||
public static class ManageNavPages
|
||||
public enum ManageNavPages
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
|
||||
public static string Index => "Index";
|
||||
|
||||
public static string ChangePassword => "ChangePassword";
|
||||
|
||||
public static string ExternalLogins => "ExternalLogins";
|
||||
|
||||
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
|
||||
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
|
||||
|
||||
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
|
||||
|
||||
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
var activePage = viewContext.ViewData["ActivePage"] as string;
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
|
||||
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
|
||||
Index, ChangePassword, ExternalLogins, TwoFactorAuthentication, Tokens
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "Reset authenticator key";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Reset authenticator key");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model SetPasswordViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Set password";
|
||||
ViewData.AddActivePage(ManageNavPages.ChangePassword);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Set password");
|
||||
}
|
||||
|
||||
<h4>Set your password</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model TwoFactorAuthenticationViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Two-factor authentication");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,16 +1,15 @@
|
||||
@using BTCPayServer.Views.Manage
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@{
|
||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||
}
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" asp-action="ChangePassword">Password</a>
|
||||
@if(hasExternalLogins)
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
||||
@if (hasExternalLogins)
|
||||
{
|
||||
<a class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-action="ExternalLogins">External logins</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ExternalLogins)" asp-action="ExternalLogins">External logins</a>
|
||||
}
|
||||
<a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
</div>
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model EmailsViewModel
|
||||
@{
|
||||
ViewData["Title"] = ServerNavPages.Emails;
|
||||
ViewData.AddActivePage(ServerNavPages.Emails);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Emails);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model UsersViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Users";
|
||||
ViewData.AddActivePage(ServerNavPages.Users);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Users);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,19 +1,18 @@
|
||||
@model BTCPayServer.Services.PoliciesSettings
|
||||
@{
|
||||
ViewData["Title"] = ServerNavPages.Policies;
|
||||
ViewData.AddActivePage(ServerNavPages.Policies);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Policies);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-lg-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-lg-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresConfirmedEmail"></label>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model RatesViewModel
|
||||
@{
|
||||
ViewData["Title"] = ServerNavPages.Rates;
|
||||
ViewData.AddActivePage(ServerNavPages.Rates);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Rates);
|
||||
}
|
||||
|
||||
|
||||
@ -28,14 +27,31 @@
|
||||
<input asp-for="PublicKey" style="width:50%;" class="form-control" placeholder="Public key" />
|
||||
<label class="sr-only" asp-for="PrivateKey"></label>
|
||||
<input asp-for="PrivateKey" style="width:50%;" class="form-control" placeholder="Private key" />
|
||||
<span asp-validation-for="PrivateKey" class="text-danger"></span>
|
||||
<p class="form-text text-muted">You can find the information on <a href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p>
|
||||
<p class="form-text text-muted">You can find the information on <a target="_blank" href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CacheMinutes"></label>
|
||||
<input asp-for="CacheMinutes" class="form-control" />
|
||||
<span asp-validation-for="CacheMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
@if(Model.RateLimits != null)
|
||||
{
|
||||
<h5>Current Bitcoin Average Quotas:</h5>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Quota period</th>
|
||||
<td>@Model.RateLimits.TotalPeriod.Prettify()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Requests quota</th>
|
||||
<td>@Model.RateLimits.RequestsLeft/@Model.RateLimits.RequestsPerPeriod</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Quota reset in</th>
|
||||
<td>@Model.RateLimits.CounterReset.Prettify()</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -2,37 +2,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Views.Server
|
||||
{
|
||||
public static class ServerNavPages
|
||||
public enum ServerNavPages
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
public static string Index => "Index";
|
||||
|
||||
|
||||
public static string Users => "Users";
|
||||
public static string Rates => "Rates";
|
||||
public static string Emails => "Email server";
|
||||
public static string Policies => "Policies";
|
||||
public static string Hangfire => "Hangfire";
|
||||
|
||||
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
|
||||
public static string EmailsNavClass(ViewContext viewContext) => PageNavClass(viewContext, Emails);
|
||||
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
|
||||
public static string PoliciesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Policies);
|
||||
public static string HangfireNavClass(ViewContext viewContext) => PageNavClass(viewContext, Hangfire);
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
var activePage = viewContext.ViewData["ActivePage"] as string;
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
|
||||
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
|
||||
Index, Users, Rates, Emails, Policies, Theme, Hangfire
|
||||
}
|
||||
}
|
||||
|
43
BTCPayServer/Views/Server/Theme.cshtml
Normal file
43
BTCPayServer/Views/Server/Theme.cshtml
Normal file
@ -0,0 +1,43 @@
|
||||
@model BTCPayServer.Services.ThemeSettings
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Theme);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="BootstrapCssUri"></label>
|
||||
<input asp-for="BootstrapCssUri" class="form-control" />
|
||||
<span asp-validation-for="BootstrapCssUri" class="text-danger"></span>
|
||||
<p class="form-text text-muted">
|
||||
<a href="https://bootstrap.build/app/v4.0/" target="_blank">Build your own theme</a>
|
||||
or <a href="https://bootswatch.com/" target="_blank">pick one already made</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CreativeStartCssUri"></label>
|
||||
<input asp-for="CreativeStartCssUri" class="form-control" />
|
||||
<span asp-validation-for="CreativeStartCssUri" class="text-danger"></span>
|
||||
<p class="form-text text-muted">
|
||||
<a href="https://startbootstrap.com/template-overviews/creative/" target="_blank">Creative Start theme</a>
|
||||
is used on top of Bootstrap
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
@model UserViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Email;
|
||||
ViewData.AddActivePage(ServerNavPages.Users);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Users);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<h4>Modify User - @Model.Email</h4>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
@using BTCPayServer.Views.Server
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ServerNavPages.UsersNavClass(ViewContext)" asp-action="Users">Users</a>
|
||||
<a class="nav-link @ServerNavPages.RatesNavClass(ViewContext)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @ServerNavPages.EmailsNavClass(ViewContext)" asp-action="Emails">Email server</a>
|
||||
<a class="nav-link @ServerNavPages.PoliciesNavClass(ViewContext)" asp-action="Policies">Policies</a>
|
||||
<a class="nav-link @ServerNavPages.HangfireNavClass(ViewContext)" href="~/hangfire" target="_blank">Hangfire</a>
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="Users">Users</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
|
||||
</div>
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
@inject RoleManager<IdentityRole> RoleManager
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -17,6 +18,9 @@
|
||||
<title>BTCPay Server</title>
|
||||
|
||||
@* CSS *@
|
||||
<link href="@themeManager.BootstrapUri" rel="stylesheet" />
|
||||
<link href="@themeManager.CreativeStartUri" rel="stylesheet" />
|
||||
|
||||
<bundle name="wwwroot/bundles/main-bundle.min.css" />
|
||||
|
||||
@* JS *@
|
||||
@ -38,9 +42,9 @@
|
||||
<div class="container">
|
||||
<a class="navbar-brand js-scroll-trigger" href="~/">
|
||||
<img src="~/img/logo.png" height="45">
|
||||
@if (env.ChainType != NBXplorer.ChainType.Main)
|
||||
@if (env.NetworkType != NBitcoin.NetworkType.Mainnet)
|
||||
{
|
||||
<span class="badge badge-warning" style="font-size:10px;">@env.ChainType.ToString()</span>
|
||||
<span class="badge badge-warning" style="font-size:10px;">@env.NetworkType.ToString()</span>
|
||||
}
|
||||
</a>
|
||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||
@ -48,25 +52,25 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@if(SignInManager.IsSignedIn(User))
|
||||
{
|
||||
@if(User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@ -75,8 +79,8 @@ else
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<footer class="bg-dark">
|
||||
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
|
||||
<footer class="siteFooter bg-dark text-white">
|
||||
<div class="container text-right">@env.ToString()</div>
|
||||
</footer>
|
||||
|
||||
@if (!dashboard.IsFullySynched())
|
||||
|
@ -36,10 +36,10 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<input asp-for="PreferredExchange" class="form-control" />
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -81,13 +81,13 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var scheme in Model.DerivationSchemes)
|
||||
@foreach(var scheme in Model.DerivationSchemes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.Crypto</td>
|
||||
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
|
||||
<td style="text-align:right">
|
||||
@if (!string.IsNullOrWhiteSpace(scheme.Value))
|
||||
@if(!string.IsNullOrWhiteSpace(scheme.Value))
|
||||
{
|
||||
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
|
||||
}
|
||||
@ -117,7 +117,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var scheme in Model.LightningNodes)
|
||||
@foreach(var scheme in Model.LightningNodes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.CryptoCode</td>
|
||||
|
26
BTCPayServer/Views/ViewsRazor.cs
Normal file
26
BTCPayServer/Views/ViewsRazor.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Views
|
||||
{
|
||||
public static class ViewsRazor
|
||||
{
|
||||
public const string ACTIVE_PAGE_KEY = "ActivePage";
|
||||
public static void SetActivePageAndTitle<T>(this ViewDataDictionary viewData, T activePage, string title = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
viewData["Title"] = title ?? activePage.ToString();
|
||||
viewData[ACTIVE_PAGE_KEY] = activePage;
|
||||
}
|
||||
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page)
|
||||
where T : IConvertible
|
||||
{
|
||||
var activePage = (T)viewData[ACTIVE_PAGE_KEY];
|
||||
return page.Equals(activePage) ? "active" : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Views
|
||||
@using BTCPayServer.Models
|
||||
@using BTCPayServer.Models.AccountViewModels
|
||||
@using BTCPayServer.Models.InvoicingModels
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/main-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/bootstrap4/css/bootstrap.css",
|
||||
"wwwroot/vendor/bootstrap4-creativestart/Open-Sans.css",
|
||||
"wwwroot/vendor/magnific-popup/magnific-popup.css",
|
||||
"wwwroot/vendor/font-awesome/css/font-awesome.css",
|
||||
"wwwroot/main/**/*.css",
|
||||
@ -18,7 +18,7 @@
|
||||
"wwwroot/vendor/jquery-easing/jquery.easing.js",
|
||||
"wwwroot/vendor/scrollreveal/scrollreveal.min.js",
|
||||
"wwwroot/vendor/magnific-popup/jquery.magnific-popup.js",
|
||||
"wwwroot/main/**/js/*.js"
|
||||
"wwwroot/vendor/bootstrap4-creativestart/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -47,5 +47,8 @@ Můžete se vrátit do {{storeName}}, pokud chcete svojí objednávku založit z
|
||||
"Archived_Body": "Prosíme kontaktujte prodejce pro informace o objednávce a případnou pomoc",
|
||||
// Lightning
|
||||
"BOLT 11 Invoice": "BOLT 11 Faktura",
|
||||
"Node Info": "Info o uzlu"
|
||||
"Node Info": "Info o uzlu",
|
||||
//
|
||||
"txCount": "{{count}} transakce",
|
||||
"txCount_plural": "{{count}} transakcí"
|
||||
};
|
||||
|
@ -1,44 +1,16 @@
|
||||
/* Wrapping element */
|
||||
/* Set some basic padding to keep content from hitting the edges */
|
||||
.body-content {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/* Carousel */
|
||||
.carousel-caption p {
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Make .svg files in the carousel display properly in older browsers */
|
||||
.carousel-inner .item img[src$=".svg"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide/rearrange for smaller screens */
|
||||
@media screen and (max-width: 767px) {
|
||||
/* Hide captions */
|
||||
.carousel-caption {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 0 18px !important;
|
||||
}
|
||||
|
||||
footer {
|
||||
.siteFooter {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 18px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
vertical-align: middle;
|
||||
font-size: 8px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
560
BTCPayServer/wwwroot/vendor/bootstrap4-creativestart/Open-Sans.css
vendored
Normal file
560
BTCPayServer/wwwroot/vendor/bootstrap4-creativestart/Open-Sans.css
vendored
Normal file
@ -0,0 +1,560 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(https://fonts.gstatic.com/s/opensans/v15/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhmIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhvIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhnIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhoIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhkIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhlIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKWiUNhrIqM.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hmIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hvIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hnIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hoIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hkIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hlIqOjjg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), url(https://fonts.gstatic.com/s/opensans/v15/memnYaGs126MiZpBA-UFUKW-U9hrIqM.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UNirkOUuhp.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOX-hpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOVuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOXuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOUehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOXehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOXOhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN7rgOUuhp.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOX-hpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOVuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOXuhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOUehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOXehpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOXOhpOqc.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), url(https://fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN8rsOUuhp.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Start Bootstrap - Createive v4.0.0-beta (http://startbootstrap.com/template-overviews/creative)
|
||||
* Start Bootstrap - Creative v4.0.0-beta (http://startbootstrap.com/template-overviews/creative)
|
||||
* Copyright 2013-2017 Start Bootstrap
|
||||
* Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-creative/blob/master/LICENSE)
|
||||
*/
|
||||
@ -9,7 +9,7 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Merriweather', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
hr {
|
||||
@ -22,17 +22,6 @@ hr {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #329F80;
|
||||
-webkit-transition: all 0.2s;
|
||||
-moz-transition: all 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
color: #0F3723;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
@ -48,19 +37,6 @@ p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #0F3B21 !important;
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
color: white;
|
||||
background-color: #0F3723 !important;
|
||||
}
|
||||
|
||||
.text-faded {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 100px 0;
|
||||
}
|
||||
@ -195,7 +171,6 @@ header.masthead {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background-image: url("../../img/bg.png");
|
||||
background-position: center;
|
||||
-webkit-background-size: cover;
|
||||
@ -227,7 +202,6 @@ header.masthead {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
margin-bottom: 50px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@ -268,10 +242,6 @@ header.masthead {
|
||||
margin: 50px auto 0;
|
||||
}
|
||||
|
||||
.service-box h3 {
|
||||
color: #0F3B21;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.service-box {
|
||||
margin: 20px auto 0;
|
||||
@ -297,7 +267,6 @@ header.masthead {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
color: white;
|
||||
background: rgba(240, 95, 64, 0.9);
|
||||
-webkit-transition: all 0.2s;
|
||||
-moz-transition: all 0.2s;
|
||||
@ -354,100 +323,7 @@ header.masthead {
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #0F3B21 !important;
|
||||
}
|
||||
|
||||
.no-gutter > [class*='col-'] {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus {
|
||||
color: #fff;
|
||||
background-color: #0F3723;
|
||||
}
|
||||
|
||||
/*.btn-default {
|
||||
color: #222222;
|
||||
border-color: white;
|
||||
background-color: white; }
|
||||
.btn-default.focus, .btn-default:focus {
|
||||
color: #222222;
|
||||
border-color: #bfbfbf;
|
||||
background-color: #e6e6e6; }
|
||||
.btn-default:hover {
|
||||
color: #222222;
|
||||
border-color: #e0e0e0;
|
||||
background-color: #e6e6e6; }
|
||||
.btn-default.active, .btn-default:active,
|
||||
.open > .btn-default.dropdown-toggle {
|
||||
color: #222222;
|
||||
border-color: #e0e0e0;
|
||||
background-color: #e6e6e6; }
|
||||
.btn-default.active.focus, .btn-default.active:focus, .btn-default.active:hover, .btn-default:active.focus, .btn-default:active:focus, .btn-default:active:hover,
|
||||
.open > .btn-default.dropdown-toggle.focus,
|
||||
.open > .btn-default.dropdown-toggle:focus,
|
||||
.open > .btn-default.dropdown-toggle:hover {
|
||||
color: #222222;
|
||||
border-color: #bfbfbf;
|
||||
background-color: #d4d4d4; }
|
||||
.btn-default.active, .btn-default:active,
|
||||
.open > .btn-default.dropdown-toggle {
|
||||
background-image: none; }
|
||||
.btn-default.disabled.focus, .btn-default.disabled:focus, .btn-default.disabled:hover, .btn-default[disabled].focus, .btn-default[disabled]:focus, .btn-default[disabled]:hover,
|
||||
fieldset[disabled] .btn-default.focus,
|
||||
fieldset[disabled] .btn-default:focus,
|
||||
fieldset[disabled] .btn-default:hover {
|
||||
border-color: white;
|
||||
background-color: white; }
|
||||
.btn-default .badge {
|
||||
color: white;
|
||||
background-color: #222222; }
|
||||
|
||||
.btn-primary {
|
||||
color: white;
|
||||
border-color: #0F3B21;
|
||||
background-color: #0F3B21; }
|
||||
.btn-primary.focus, .btn-primary:focus {
|
||||
color: white;
|
||||
border-color: #a4270d;
|
||||
background-color: #eb3812; }
|
||||
.btn-primary:hover {
|
||||
color: white;
|
||||
border-color: #e13612;
|
||||
background-color: #eb3812; }
|
||||
.btn-primary.active, .btn-primary:active,
|
||||
.open > .btn-primary.dropdown-toggle {
|
||||
color: white;
|
||||
border-color: #e13612;
|
||||
background-color: #eb3812; }
|
||||
.btn-primary.active.focus, .btn-primary.active:focus, .btn-primary.active:hover, .btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover,
|
||||
.open > .btn-primary.dropdown-toggle.focus,
|
||||
.open > .btn-primary.dropdown-toggle:focus,
|
||||
.open > .btn-primary.dropdown-toggle:hover {
|
||||
color: white;
|
||||
border-color: #a4270d;
|
||||
background-color: #c93110; }
|
||||
.btn-primary.active, .btn-primary:active,
|
||||
.open > .btn-primary.dropdown-toggle {
|
||||
background-image: none; }
|
||||
.btn-primary.disabled.focus, .btn-primary.disabled:focus, .btn-primary.disabled:hover, .btn-primary[disabled].focus, .btn-primary[disabled]:focus, .btn-primary[disabled]:hover,
|
||||
fieldset[disabled] .btn-primary.focus,
|
||||
fieldset[disabled] .btn-primary:focus,
|
||||
fieldset[disabled] .btn-primary:hover {
|
||||
border-color: #0F3B21;
|
||||
background-color: #0F3B21; }
|
||||
.btn-primary .badge {
|
||||
color: #0F3B21;
|
||||
background-color: white; }
|
||||
|
||||
.btn {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
border-radius: 300px;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif; }
|
||||
|
||||
.btn-xl {
|
||||
padding: 15px 30px; }*/
|
@ -1,3 +1,4 @@
|
||||
$primary: #23735c;
|
||||
$primary: #329f80;
|
||||
$secondary: #2284A6;
|
||||
$dark: #0f3b21;
|
||||
$font-size-base: 0.9rem;
|
||||
|
@ -17,15 +17,15 @@
|
||||
--cyan: #17a2b8;
|
||||
--white: #fff;
|
||||
--gray: #6c757d;
|
||||
--gray-dark: #343a40;
|
||||
--primary: #23735c;
|
||||
--gray-dark: #0f3b21;
|
||||
--primary: #329f80;
|
||||
--secondary: #2284A6;
|
||||
--success: #28a745;
|
||||
--info: #17a2b8;
|
||||
--warning: #ffc107;
|
||||
--danger: #dc3545;
|
||||
--light: #f8f9fa;
|
||||
--dark: #343a40;
|
||||
--dark: #0f3b21;
|
||||
--breakpoint-xs: 0;
|
||||
--breakpoint-sm: 576px;
|
||||
--breakpoint-md: 768px;
|
||||
@ -137,7 +137,7 @@ sup {
|
||||
top: -.5em; }
|
||||
|
||||
a {
|
||||
color: #23735c;
|
||||
color: #329f80;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects; }
|
||||
@ -1635,8 +1635,8 @@ fieldset:disabled a.btn {
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #23735c;
|
||||
border-color: #23735c; }
|
||||
background-color: #329f80;
|
||||
border-color: #329f80; }
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #1a5645;
|
||||
@ -1645,8 +1645,8 @@ fieldset:disabled a.btn {
|
||||
box-shadow: 0 0 0 0.2rem rgba(35, 115, 92, 0.5); }
|
||||
.btn-primary.disabled, .btn-primary:disabled {
|
||||
color: #fff;
|
||||
background-color: #23735c;
|
||||
border-color: #23735c; }
|
||||
background-color: #329f80;
|
||||
border-color: #329f80; }
|
||||
.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-primary.dropdown-toggle {
|
||||
color: #fff;
|
||||
@ -1796,8 +1796,8 @@ fieldset:disabled a.btn {
|
||||
|
||||
.btn-dark {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40; }
|
||||
background-color: #0f3b21;
|
||||
border-color: #0f3b21; }
|
||||
.btn-dark:hover {
|
||||
color: #fff;
|
||||
background-color: #23272b;
|
||||
@ -1806,8 +1806,8 @@ fieldset:disabled a.btn {
|
||||
box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); }
|
||||
.btn-dark.disabled, .btn-dark:disabled {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40; }
|
||||
background-color: #0f3b21;
|
||||
border-color: #0f3b21; }
|
||||
.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-dark.dropdown-toggle {
|
||||
color: #fff;
|
||||
@ -1818,24 +1818,24 @@ fieldset:disabled a.btn {
|
||||
box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); }
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #23735c;
|
||||
color: #329f80;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border-color: #23735c; }
|
||||
border-color: #329f80; }
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #23735c;
|
||||
border-color: #23735c; }
|
||||
background-color: #329f80;
|
||||
border-color: #329f80; }
|
||||
.btn-outline-primary:focus, .btn-outline-primary.focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(35, 115, 92, 0.5); }
|
||||
.btn-outline-primary.disabled, .btn-outline-primary:disabled {
|
||||
color: #23735c;
|
||||
color: #329f80;
|
||||
background-color: transparent; }
|
||||
.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-outline-primary.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #23735c;
|
||||
border-color: #23735c; }
|
||||
background-color: #329f80;
|
||||
border-color: #329f80; }
|
||||
.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,
|
||||
.show > .btn-outline-primary.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(35, 115, 92, 0.5); }
|
||||
@ -1979,31 +1979,31 @@ fieldset:disabled a.btn {
|
||||
box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); }
|
||||
|
||||
.btn-outline-dark {
|
||||
color: #343a40;
|
||||
color: #0f3b21;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border-color: #343a40; }
|
||||
border-color: #0f3b21; }
|
||||
.btn-outline-dark:hover {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40; }
|
||||
background-color: #0f3b21;
|
||||
border-color: #0f3b21; }
|
||||
.btn-outline-dark:focus, .btn-outline-dark.focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); }
|
||||
.btn-outline-dark.disabled, .btn-outline-dark:disabled {
|
||||
color: #343a40;
|
||||
color: #0f3b21;
|
||||
background-color: transparent; }
|
||||
.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-outline-dark.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40; }
|
||||
background-color: #0f3b21;
|
||||
border-color: #0f3b21; }
|
||||
.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,
|
||||
.show > .btn-outline-dark.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); }
|
||||
|
||||
.btn-link {
|
||||
font-weight: 400;
|
||||
color: #23735c;
|
||||
color: #329f80;
|
||||
background-color: transparent; }
|
||||
.btn-link:hover {
|
||||
color: #11382d;
|
||||
@ -2197,7 +2197,7 @@ tbody.collapse.show {
|
||||
.dropdown-item.active, .dropdown-item:active {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
.dropdown-item.disabled, .dropdown-item:disabled {
|
||||
color: #6c757d;
|
||||
background-color: transparent; }
|
||||
@ -2429,7 +2429,7 @@ tbody.collapse.show {
|
||||
opacity: 0; }
|
||||
.custom-control-input:checked ~ .custom-control-label::before {
|
||||
color: #fff;
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
.custom-control-input:focus ~ .custom-control-label::before {
|
||||
box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(35, 115, 92, 0.25); }
|
||||
.custom-control-input:active ~ .custom-control-label::before {
|
||||
@ -2469,13 +2469,13 @@ tbody.collapse.show {
|
||||
border-radius: 0.25rem; }
|
||||
|
||||
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
|
||||
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); }
|
||||
|
||||
.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
|
||||
.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E"); }
|
||||
@ -2490,7 +2490,7 @@ tbody.collapse.show {
|
||||
border-radius: 50%; }
|
||||
|
||||
.custom-radio .custom-control-input:checked ~ .custom-control-label::before {
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
|
||||
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); }
|
||||
@ -2636,7 +2636,7 @@ tbody.collapse.show {
|
||||
.nav-pills .nav-link.active,
|
||||
.nav-pills .show > .nav-link {
|
||||
color: #fff;
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
|
||||
.nav-fill .nav-item {
|
||||
flex: 1 1 auto;
|
||||
@ -3148,7 +3148,7 @@ tbody.collapse.show {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-left: -1px;
|
||||
line-height: 1.25;
|
||||
color: #23735c;
|
||||
color: #329f80;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6; }
|
||||
.page-link:hover {
|
||||
@ -3175,8 +3175,8 @@ tbody.collapse.show {
|
||||
.page-item.active .page-link {
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
background-color: #23735c;
|
||||
border-color: #23735c; }
|
||||
background-color: #329f80;
|
||||
border-color: #329f80; }
|
||||
|
||||
.page-item.disabled .page-link {
|
||||
color: #6c757d;
|
||||
@ -3235,7 +3235,7 @@ tbody.collapse.show {
|
||||
|
||||
.badge-primary {
|
||||
color: #fff;
|
||||
background-color: #23735c; }
|
||||
background-color: #329f80; }
|
||||
.badge-primary[href]:hover, .badge-primary[href]:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
@ -3291,7 +3291,7 @@ tbody.collapse.show {
|
||||
|
||||
.badge-dark {
|
||||
color: #fff;
|
||||
background-color: #343a40; }
|
||||
background-color: #0f3b21; }
|
||||
.badge-dark[href]:hover, .badge-dark[href]:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
@ -3425,7 +3425,7 @@ tbody.collapse.show {
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #23735c;
|
||||
background-color: #329f80;
|
||||
transition: width 0.6s ease; }
|
||||
|
||||
.progress-bar-striped {
|
||||
@ -3483,8 +3483,8 @@ tbody.collapse.show {
|
||||
.list-group-item.active {
|
||||
z-index: 2;
|
||||
color: #fff;
|
||||
background-color: #23735c;
|
||||
border-color: #23735c; }
|
||||
background-color: #329f80;
|
||||
border-color: #329f80; }
|
||||
|
||||
.list-group-flush .list-group-item {
|
||||
border-right: 0;
|
||||
@ -4099,7 +4099,7 @@ button.close {
|
||||
vertical-align: text-top !important; }
|
||||
|
||||
.bg-primary {
|
||||
background-color: #23735c !important; }
|
||||
background-color: #329f80 !important; }
|
||||
|
||||
a.bg-primary:hover, a.bg-primary:focus,
|
||||
button.bg-primary:hover,
|
||||
@ -4155,7 +4155,7 @@ button.bg-light:focus {
|
||||
background-color: #dae0e5 !important; }
|
||||
|
||||
.bg-dark {
|
||||
background-color: #343a40 !important; }
|
||||
background-color: #0f3b21 !important; }
|
||||
|
||||
a.bg-dark:hover, a.bg-dark:focus,
|
||||
button.bg-dark:hover,
|
||||
@ -4199,7 +4199,7 @@ button.bg-dark:focus {
|
||||
border-left: 0 !important; }
|
||||
|
||||
.border-primary {
|
||||
border-color: #23735c !important; }
|
||||
border-color: #329f80 !important; }
|
||||
|
||||
.border-secondary {
|
||||
border-color: #2284A6 !important; }
|
||||
@ -4220,7 +4220,7 @@ button.bg-dark:focus {
|
||||
border-color: #f8f9fa !important; }
|
||||
|
||||
.border-dark {
|
||||
border-color: #343a40 !important; }
|
||||
border-color: #0f3b21 !important; }
|
||||
|
||||
.border-white {
|
||||
border-color: #fff !important; }
|
||||
@ -5929,7 +5929,7 @@ button.bg-dark:focus {
|
||||
color: #fff !important; }
|
||||
|
||||
.text-primary {
|
||||
color: #23735c !important; }
|
||||
color: #329f80 !important; }
|
||||
|
||||
a.text-primary:hover, a.text-primary:focus {
|
||||
color: #174c3d !important; }
|
||||
@ -5971,7 +5971,7 @@ a.text-light:hover, a.text-light:focus {
|
||||
color: #dae0e5 !important; }
|
||||
|
||||
.text-dark {
|
||||
color: #343a40 !important; }
|
||||
color: #0f3b21 !important; }
|
||||
|
||||
a.text-dark:hover, a.text-dark:focus {
|
||||
color: #1d2124 !important; }
|
||||
|
Reference in New Issue
Block a user