diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index b42107839..ccd7aec85 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -47,7 +47,7 @@ namespace BTCPayServer.Tests } public Uri LTCNBXplorerUri { get; set; } - + public Uri ServerUri { get; @@ -65,6 +65,9 @@ namespace BTCPayServer.Tests get; set; } + + public bool MockRates { get; set; } = true; + public void Start() { if (!Directory.Exists(_Directory)) @@ -101,12 +104,15 @@ namespace BTCPayServer.Tests .UseConfiguration(conf) .ConfigureServices(s => { - var mockRates = new MockRateProviderFactory(); - var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m)); - var ltc = new MockRateProvider("LTC", new Rate("USD", 500m)); - mockRates.AddMock(btc); - mockRates.AddMock(ltc); - s.AddSingleton(mockRates); + if (MockRates) + { + var mockRates = new MockRateProviderFactory(); + var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m)); + var ltc = new MockRateProvider("LTC", new Rate("USD", 500m)); + mockRates.AddMock(btc); + mockRates.AddMock(ltc); + s.AddSingleton(mockRates); + } s.AddLogging(l => { l.SetMinimumLevel(LogLevel.Information) @@ -121,7 +127,7 @@ namespace BTCPayServer.Tests _Host.Start(); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); } - + public string HostName { get; diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 73fc2e425..2e3c4f651 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -34,21 +34,11 @@ namespace BTCPayServer.Tests public ServerTester(string scope) { _Directory = scope; - } - - public bool Dockerized - { - get; set; - } - - public void Start() - { if (Directory.Exists(_Directory)) Utils.DeleteDirectory(_Directory); if (!Directory.Exists(_Directory)) Directory.CreateDirectory(_Directory); - NetworkProvider = new BTCPayNetworkProvider(ChainType.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); @@ -72,6 +62,15 @@ namespace BTCPayServer.Tests PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture); PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false")); + } + + public bool Dockerized + { + get; set; + } + + public void Start() + { PayTester.Start(); } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6825ff335..6c1bfa5cf 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -616,12 +616,54 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanUseExchangeSpecificRate() + { + using (var tester = ServerTester.Create()) + { + tester.PayTester.MockRates = false; + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + List rates = new List(); + rates.Add(CreateInvoice(tester, user, "coinaverage")); + var bitflyer = CreateInvoice(tester, user, "bitflyer"); + var bitflyer2 = CreateInvoice(tester, user, "bitflyer"); + Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache + rates.Add(bitflyer); + + foreach(var rate in rates) + { + Assert.Single(rates.Where(r => r == rate)); + } + } + } + + private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) + { + var storeController = tester.PayTester.GetController(user.UserId); + var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; + vm.PreferredExchange = exchange; + storeController.UpdateStore(user.StoreId, vm).Wait(); + var invoice2 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + return invoice2.CryptoInfo[0].Rate; + } [Fact] public void CanTweakRate() { using (var tester = ServerTester.Create()) { + tester.PayTester.MockRates = false; tester.Start(); var user = tester.NewAccount(); user.GrantAccess(); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 5c70d6d37..ca372fdbb 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.1.82 + 1.0.1.86 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -34,12 +34,12 @@ - + - + diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 91c39408d..369165e01 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -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>(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 { 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 { 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().ToList(); + model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType().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")] @@ -450,7 +451,7 @@ namespace BTCPayServer.Controllers return View(model); } - if(StatusMessage != null) + if (StatusMessage != null) { return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new { diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index af3496b9e..03a89cc84 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers { var btc = _NetworkProvider.BTC; var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc); - var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc)); + var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules()); if (feeProvider != null && rateProvider != null) { var gettingFee = feeProvider.GetFeeRateAsync(); @@ -186,7 +186,7 @@ namespace BTCPayServer.Controllers private async Task CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) { var storeBlob = store.GetStoreBlob(); - var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network)).GetRateAsync(entity.ProductInformation.Currency); + var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency); PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; @@ -221,7 +221,7 @@ namespace BTCPayServer.Controllers if (limitValue.Currency == entity.ProductInformation.Currency) limitValueRate = paymentMethod.Rate; else - limitValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network)).GetRateAsync(limitValue.Currency); + limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency); var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate); if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 3a2170ce6..63336ed0b 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -49,18 +49,20 @@ namespace BTCPayServer.Controllers var network = _NetworkProvider.GetNetwork(cryptoCode); if (network == null) return NotFound(); - var rateProvider = _RateProviderFactory.GetRateProvider(network); - if (rateProvider == null) - return NotFound(); + RateRules rules = null; if (storeId != null) { var store = await _StoreRepo.FindStore(storeId); if (store == null) return NotFound(); - rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider); + rules = store.GetStoreBlob().GetRateRules(); } + var rateProvider = _RateProviderFactory.GetRateProvider(network, rules); + if (rateProvider == null) + return NotFound(); + var allRates = (await rateProvider.GetRatesAsync()); return Json(allRates.Select(r => new NBitpayClient.Rate() diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 2b732c233..8a3214f05 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -42,31 +42,30 @@ namespace BTCPayServer.Controllers public async Task Rates() { var rates = (await _SettingsRepository.GetSettingAsync()) ?? 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 Rates(RatesViewModel vm) @@ -77,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() { diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 1c1ae5e90..d7c93dccd 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -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 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,6 +396,11 @@ 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); diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index d953fabf1..6ac02a0d1 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -284,30 +284,12 @@ namespace BTCPayServer.Data } } - public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider) + public RateRules GetRateRules() { - if (!PreferredExchange.IsCoinAverage()) + return new RateRules(RateRules) { - // If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all - if (rateProvider is CachedRateProvider cachedRateProvider) - { - rateProvider = new FallbackRateProvider(new IRateProvider[] { - new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange }, - cachedRateProvider.Inner - }); - rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange }; - } - else - { - rateProvider = new FallbackRateProvider(new IRateProvider[] { - new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange }, - rateProvider - }); - } - } - if (RateRules == null || RateRules.Count == 0) - return rateProvider; - return new TweakRateProvider(network, rateProvider, RateRules.ToList()); + PreferredExchange = PreferredExchange + }; } } } diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 5f9df05c9..40fe847e5 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -28,11 +28,22 @@ 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 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++) diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 781b87818..5f98d24b2 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -6,29 +7,92 @@ using System.Threading.Tasks; using BTCPayServer.Services; using BTCPayServer.Services.Rates; using Microsoft.Extensions.Hosting; +using BTCPayServer.Logging; +using System.Runtime.CompilerServices; namespace BTCPayServer.HostedServices { public class RatesHostedService : IHostedService { private SettingsRepository _SettingsRepository; - private BTCPayRateProviderFactory _RateProviderFactory; - public RatesHostedService(SettingsRepository repo, IRateProviderFactory rateProviderFactory) + private IRateProviderFactory _RateProviderFactory; + private CoinAverageSettings _coinAverageSettings; + public RatesHostedService(SettingsRepository repo, + CoinAverageSettings coinAverageSettings, + IRateProviderFactory rateProviderFactory) { this._SettingsRepository = repo; - _RateProviderFactory = rateProviderFactory as BTCPayRateProviderFactory; + _RateProviderFactory = rateProviderFactory; + _coinAverageSettings = coinAverageSettings; } - public async Task StartAsync(CancellationToken cancellationToken) + + + CancellationTokenSource _Cts = new CancellationTokenSource(); + + List _Tasks = new List(); + + public Task StartAsync(CancellationToken cancellationToken) { - if (_RateProviderFactory == null) - return; - var rates = (await _SettingsRepository.GetSettingAsync()) ?? new RatesSetting(); - _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); + _Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token)); + _Tasks.Add(RefreshCoinAverageSettings(_Cts.Token)); + return Task.CompletedTask; + } + + + async Task Timer(Func act, CancellationToken cancellation, [CallerMemberName]string caller = null) + { + 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 () => + { + var tickers = await new CoinAverageRateProvider("BTC").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 () => + { + var rates = (await _SettingsRepository.GetSettingAsync()) ?? new RatesSetting(); + _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); + if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey)) + { + _coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey); + } + await _SettingsRepository.WaitSettingsChanged(cancellation); + }, cancellation); } public Task StopAsync(CancellationToken cancellationToken) { - return Task.CompletedTask; + _Cts.Cancel(); + return Task.WhenAll(_Tasks.ToArray()); } } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 62bb69807..5401126d2 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -109,7 +109,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index e889200b9..36b270c2b 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -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(); - public List Payments { get; set; } = new List(); + public List OnChainPayments { get; set; } = new List(); + public List OffChainPayments { get; set; } = new List(); + public class OffChainPayment + { + public string Crypto { get; set; } + public string BOLT11 { get; set; } + } public string Status { diff --git a/BTCPayServer/Models/ServerViewModels/RatesViewModel.cs b/BTCPayServer/Models/ServerViewModels/RatesViewModel.cs index b0e1c3cba..b72f1ba49 100644 --- a/BTCPayServer/Models/ServerViewModels/RatesViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/RatesViewModel.cs @@ -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; } } } diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index dc4a71e80..1ed1dce96 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -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 DerivationSchemes { get; set; } = new List(); + 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; } diff --git a/BTCPayServer/Services/LanguageService.cs b/BTCPayServer/Services/LanguageService.cs index 2a14ffebd..c738be363 100644 --- a/BTCPayServer/Services/LanguageService.cs +++ b/BTCPayServer/Services/LanguageService.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Services new Language("ja-JP", "日本語"), new Language("fr-FR", "Français"), new Language("es-ES", "Spanish"), + new Language("pt-PT", "Portuguese"), new Language("pt-BR", "Portuguese (Brazil)"), new Language("nl-NL", "Dutch"), new Language("cs-CZ", "Česky"), diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index d911f13b8..f62aa1166 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -51,9 +51,28 @@ namespace BTCPayServer.Services.Rates _Cache = new MemoryCache(_CacheOptions); } - public IRateProvider GetRateProvider(BTCPayNetwork network) + public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) { - return new CachedRateProvider(network.CryptoCode, GetDefaultRateProvider(network), _Cache) { CacheSpan = CacheSpan }; + rules = rules ?? new RateRules(); + var rateProvider = GetDefaultRateProvider(network); + if (!rules.PreferredExchange.IsCoinAverage()) + { + rateProvider = CreateExchangeRateProvider(network, rules.PreferredExchange); + } + rateProvider = CreateCachedRateProvider(network, rateProvider, rules.PreferredExchange); + return new TweakRateProvider(network, rateProvider, rules); + } + + private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange) + { + var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider); + coinAverage.Exchange = exchange; + return coinAverage; + } + + private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope) + { + return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope }; } private IRateProvider GetDefaultRateProvider(BTCPayNetwork network) diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs index e26356e6c..ebaa5bde0 100644 --- a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs +++ b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs @@ -30,52 +30,40 @@ namespace BTCPayServer.Services.Rates public string CryptoCode { get; set; } - public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) + public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider) { return new CoinAverageRateProvider(CryptoCode) { Authenticator = serviceProvider.GetService() }; } + + IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider) + { + return CreateRateProvider(serviceProvider); + } + } + + public class GetExchangeTickersResponse + { + public class Exchange + { + public string Name { get; set; } + [JsonProperty("display_name")] + public string DisplayName { get; set; } + public string[] Symbols { get; set; } + } + public bool Success { get; set; } + public Exchange[] Exchanges { get; set; } } 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()) ?? new RatesSetting(); - var signature = settings.GetCoinAverageSignature(); - if (signature != null) - { - message.Headers.Add("X-signature", signature); - } - } } public interface ICoinAverageAuthenticator @@ -181,5 +169,49 @@ namespace BTCPayServer.Services.Rates var resp = await _Client.SendAsync(request); resp.EnsureSuccessStatusCode(); } + + public async Task 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()); + response.RequestsLeft = jobj["requests_left"].Value(); + return response; + } + + public async Task GetExchangeTickersAsync() + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker"); + var resp = await _Client.SendAsync(request); + resp.EnsureSuccessStatusCode(); + var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync()); + var response = new GetExchangeTickersResponse(); + response.Success = jobj["success"].Value(); + var exchanges = (JObject)jobj["exchanges"]; + response.Exchanges = exchanges + .Properties() + .Select(p => + { + var exchange = JsonConvert.DeserializeObject(p.Value.ToString()); + exchange.Name = p.Name; + return exchange; + }) + .ToArray(); + return response; + } + } + + public class GetRateLimitsResponse + { + public TimeSpan CounterReset { get; set; } + public int RequestsLeft { get; set; } } } diff --git a/BTCPayServer/Services/Rates/CoinAverageSettings.cs b/BTCPayServer/Services/Rates/CoinAverageSettings.cs new file mode 100644 index 000000000..0d666fb45 --- /dev/null +++ b/BTCPayServer/Services/Rates/CoinAverageSettings.cs @@ -0,0 +1,42 @@ +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 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 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; + } + } +} diff --git a/BTCPayServer/Services/Rates/IRateProviderFactory.cs b/BTCPayServer/Services/Rates/IRateProviderFactory.cs index 55a364668..5c3b76a77 100644 --- a/BTCPayServer/Services/Rates/IRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/IRateProviderFactory.cs @@ -1,12 +1,40 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Data; namespace BTCPayServer.Services.Rates { + public class RateRules : IEnumerable + { + private List rateRules; + + public RateRules() + { + rateRules = new List(); + } + public RateRules(List rateRules) + { + this.rateRules = rateRules?.ToList() ?? new List(); + } + public string PreferredExchange { get; set; } + + public IEnumerator GetEnumerator() + { + return rateRules.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } public interface IRateProviderFactory { - IRateProvider GetRateProvider(BTCPayNetwork network); + IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules); + TimeSpan CacheSpan { get; set; } + void InvalidateCache(); } } diff --git a/BTCPayServer/Services/Rates/MockRateProvider.cs b/BTCPayServer/Services/Rates/MockRateProvider.cs index c735c980a..28d8298d1 100644 --- a/BTCPayServer/Services/Rates/MockRateProvider.cs +++ b/BTCPayServer/Services/Rates/MockRateProvider.cs @@ -14,14 +14,21 @@ namespace BTCPayServer.Services.Rates } + public TimeSpan CacheSpan { get; set; } + public void AddMock(MockRateProvider mock) { _Mocks.Add(mock); } - public IRateProvider GetRateProvider(BTCPayNetwork network) + public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) { return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode); } + + public void InvalidateCache() + { + + } } public class MockRateProvider : IRateProvider { diff --git a/BTCPayServer/Services/Rates/TweakRateProvider.cs b/BTCPayServer/Services/Rates/TweakRateProvider.cs index 292f9787f..dcca887cd 100644 --- a/BTCPayServer/Services/Rates/TweakRateProvider.cs +++ b/BTCPayServer/Services/Rates/TweakRateProvider.cs @@ -10,9 +10,9 @@ namespace BTCPayServer.Services.Rates { private BTCPayNetwork network; private IRateProvider rateProvider; - private List rateRules; + private RateRules rateRules; - public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List rateRules) + public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules) { if (network == null) throw new ArgumentNullException(nameof(network)); diff --git a/BTCPayServer/Services/SettingsRepository.cs b/BTCPayServer/Services/SettingsRepository.cs index 51e80bfa7..a6911abc2 100644 --- a/BTCPayServer/Services/SettingsRepository.cs +++ b/BTCPayServer/Services/SettingsRepository.cs @@ -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> 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(string value) @@ -62,5 +79,35 @@ namespace BTCPayServer.Services { return JsonConvert.SerializeObject(obj); } + + MultiValueDictionary> _Subscriptions = new MultiValueDictionary>(); + public async Task WaitSettingsChanged(CancellationToken cancellation) + { + var tcs = new TaskCompletionSource(); + 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; + } + } } } diff --git a/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml b/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml index 2fd64e279..807cfc080 100644 --- a/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml @@ -15,7 +15,7 @@
-
+

@Model.Title

diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index 9c79b9585..8a4484ba1 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -110,6 +110,7 @@ 'es-ES': { translation: locales_es }, 'ja-JP': { translation: locales_ja }, 'fr-FR': { translation: locales_fr }, + 'pt': { translation: locales_pt }, 'pt-BR': { translation: locales_pt_br }, 'nl': { translation: locales_nl }, 'cs-CZ': { translation: locales_cs }, diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 8c4096897..b0ad873db 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -171,33 +171,61 @@
-
-
-

Payments

- - - - - - - - - - - @foreach (var payment in Model.Payments) - { - var linethrough = payment.Replaced ? "class='linethrough'" : ""; + @if(Model.OnChainPayments.Count > 0) + { +
+
+

On-Chain payments

+
Payment methodDeposit addressTransaction IdConfirmations
+ - - - - + + + + - } - -
@payment.PaymentMethod@payment.DepositAddress@payment.TransactionId@payment.ConfirmationsCryptoDeposit addressTransaction IdConfirmations
+ + + @foreach(var payment in Model.OnChainPayments) + { + var replaced = payment.Replaced ? "text-decoration: line-through;" : ""; + + @payment.Crypto + @payment.DepositAddress + @payment.TransactionId + @payment.Confirmations + + } + + +
-
+ } + @if(Model.OffChainPayments.Count > 0) + { +
+
+

Off-Chain payments

+ + + + + + + + + @foreach(var payment in Model.OffChainPayments) + { + + + + + } + +
CryptoBOLT11
@payment.Crypto@payment.BOLT11
+
+
+ }

Addresses

diff --git a/BTCPayServer/Views/Server/Rates.cshtml b/BTCPayServer/Views/Server/Rates.cshtml index 15b2a5522..89e145778 100644 --- a/BTCPayServer/Views/Server/Rates.cshtml +++ b/BTCPayServer/Views/Server/Rates.cshtml @@ -28,14 +28,27 @@ - -

You can find the information on bitcoinaverage api key page

+

You can find the information on bitcoinaverage api key page

+ @if(Model.RateLimits != null) + { +
Current Bitcoin Average Quotas:
+ + + + + + + + + +
Requests left@Model.RateLimits.RequestsLeft
Quota reset in@Model.RateLimits.CounterReset
+ }
diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index 5672902cc..e35466c79 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -36,10 +36,10 @@
- +

- Current price source is @Model.PreferredExchange. (using 1 minute cache) + Current price source is @Model.PreferredExchange.

@@ -81,13 +81,13 @@ - @foreach (var scheme in Model.DerivationSchemes) + @foreach(var scheme in Model.DerivationSchemes) { @scheme.Crypto @scheme.Value - @if (!string.IsNullOrWhiteSpace(scheme.Value)) + @if(!string.IsNullOrWhiteSpace(scheme.Value)) { Wallet - } @@ -117,7 +117,7 @@ - @foreach (var scheme in Model.LightningNodes) + @foreach(var scheme in Model.LightningNodes) { @scheme.CryptoCode diff --git a/BTCPayServer/wwwroot/checkout/js/langs/cs.js b/BTCPayServer/wwwroot/checkout/js/langs/cs.js index 9a0102171..f260777c5 100644 --- a/BTCPayServer/wwwroot/checkout/js/langs/cs.js +++ b/BTCPayServer/wwwroot/checkout/js/langs/cs.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í" }; diff --git a/BTCPayServer/wwwroot/checkout/js/langs/de.js b/BTCPayServer/wwwroot/checkout/js/langs/de.js index 8456bb907..902ace746 100644 --- a/BTCPayServer/wwwroot/checkout/js/langs/de.js +++ b/BTCPayServer/wwwroot/checkout/js/langs/de.js @@ -5,8 +5,8 @@ const locales_de = { "Awaiting Payment...": "Warten auf Zahlung...", "Pay with": "Bezahlen mit", "Contact and Refund Email": "Kontakt und Rückerstattungs Email", - "Contact_Body": "Bitte geben Sie unten eine E-Mail-Adresse an. Wir werden Sie unter dieser Adresse kontaktieren, wenn ein Problem mit Ihrer Zahlung vorliegt.", - "Your email": "Deine Email", + "Contact_Body": "Bitte geben Sie unten eine E-Mail-Adresse an. Wir werden Sie unter dieser Adresse kontaktieren, falls ein Problem mit Ihrer Zahlung vorliegt.", + "Your email": "Ihre Email-Adresse", "Continue": "Fortsetzen", "Please enter a valid email address": "Bitte geben Sie eine gültige E-Mail-Adresse ein", "Order Amount": "Bestellbetrag", @@ -16,19 +16,19 @@ const locales_de = { // Tabs "Scan": "Scan", "Copy": "Kopieren", - "Conversion": "Umwandlung", + "Conversion": "Umrechnung", // Scan tab - "Open in wallet": "In der Brieftasche öffnen", + "Open in wallet": "In der Wallet öffnen", // Copy tab "CompletePay_Body": "Um Ihre Zahlung abzuschließen, senden Sie bitte {{btcDue}} {{cryptoCode}} an die unten angegebene Adresse.", "Amount": "Menge", "Address": "Adresse", "Copied": "Kopiert", // Conversion tab - "ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.", - "ConversionTab_BodyDesc": "Dieser Service wird von Drittanbietern bereitgestellt. Bitte beachten Sie, dass wir keine Kontrolle darüber haben, wie die Anbieter Ihre Gelder weiterleiten. Die Rechnung wird erst bezahlt, wenn das Geld in {{cryptoCode}} Blockchain eingegangen ist.", + "ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit Altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.", + "ConversionTab_BodyDesc": "Dieser Service wird von Drittanbietern bereitgestellt. Bitte beachten Sie, dass wir keine Kontrolle darüber haben, wie die Anbieter Ihre Gelder weiterleiten. Die Rechnung wird erst als bezahlt markiert, wenn das Geld in {{cryptoCode}} Blockchain eingegangen ist.", "Shapeshift_Button_Text": "Bezahlen mit Altcoins", - "ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Conversion-Anbieter verfügbar.", + "ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Umrechnungsanbieter verfügbar.", // Invoice expired "Invoice expiring soon...": "Die Rechnung läuft bald ab...", "Invoice expired": "Die Rechnung ist abgelaufen", @@ -36,7 +36,7 @@ const locales_de = { "InvoiceExpired_Body_1": "Diese Rechnung ist abgelaufen. Eine Rechnung ist nur für {{maxTimeMinutes}} Minuten gültig. \ Sie können zu {{storeName}} zurückkehren, wenn Sie Ihre Zahlung erneut senden möchten.", "InvoiceExpired_Body_2": "Wenn Sie versucht haben, eine Zahlung zu senden, wurde sie vom Bitcoin-Netzwerk noch nicht akzeptiert. Wir haben Ihre Gelder noch nicht erhalten.", - "InvoiceExpired_Body_3": "Wenn die Transaktion vom Bitcoin-Netzwerk nicht akzeptiert wird, ist das Geld wieder in Ihrer Brieftasche verfügbar. Abhängig von Ihrem Geldbeutel, kann dies 48-72 Stunden dauern.", + "InvoiceExpired_Body_3": "Wenn die Transaktion vom Bitcoin-Netzwerk nicht akzeptiert wird, ist das Geld wieder in Ihrer Wallet verfügbar. Abhängig von Ihrer Wallet, kann dies 48-72 Stunden dauern.", "Invoice ID": "Rechnungs ID", "Order ID": "Auftrag ID", "Return to StoreName": "Zurück zu {{storeName}}", @@ -47,7 +47,7 @@ Sie können zu {{storeName}} zurückkehren, wenn Sie Ihre Zahlung erneut senden "Archived_Body": "Bitte kontaktieren Sie den Shop für Bestellinformationen oder Hilfe", // Lightning "BOLT 11 Invoice": "BOLT 11 Rechnung", - "Node Info": "Knoten Info", + "Node Info": "Netzwerkknoten Info", // "txCount": "{{count}} transaktion", "txCount_plural": "{{count}} transaktionen" diff --git a/BTCPayServer/wwwroot/checkout/js/langs/ja.js b/BTCPayServer/wwwroot/checkout/js/langs/ja.js index fe30c39f0..b5c91d127 100644 --- a/BTCPayServer/wwwroot/checkout/js/langs/ja.js +++ b/BTCPayServer/wwwroot/checkout/js/langs/ja.js @@ -47,5 +47,8 @@ "Archived_Body": "ご注文に関わる詳細などでお困りの場合はお店の担当窓口へお問い合わせください。", // Lightning "BOLT 11 Invoice": "お支払いコード", - "Node Info": "接続情報" + "Node Info": "接続情報", + // + "txCount": "取引 {{count}} 個", + "txCount_plural": "取引 {{count}} 個" }; diff --git a/BTCPayServer/wwwroot/checkout/js/langs/nl.js b/BTCPayServer/wwwroot/checkout/js/langs/nl.js index 4004f558b..33b0187b8 100644 --- a/BTCPayServer/wwwroot/checkout/js/langs/nl.js +++ b/BTCPayServer/wwwroot/checkout/js/langs/nl.js @@ -47,5 +47,8 @@ Je kan terug komen naar {{storeName}} indien je nog eens je betaling wilt prober "Archived_Body": "Bedankt om de winkel te contacteren voor bijstand met of informatie over deze bestelling", // Lightning "BOLT 11 Invoice": "BOLT 11 Factuur", - "Node Info": "Node Info" + "Node Info": "Node Info", + // + "txCount": "{{count}} transactie", + "txCount_plural": "{{count}} transacties" }; diff --git a/BTCPayServer/wwwroot/checkout/js/langs/pt.js b/BTCPayServer/wwwroot/checkout/js/langs/pt.js new file mode 100644 index 000000000..5441fede5 --- /dev/null +++ b/BTCPayServer/wwwroot/checkout/js/langs/pt.js @@ -0,0 +1,54 @@ +const locales_pt = { + nested: { + lang: 'Idioma' + }, + "Awaiting Payment...": "A Aguardar Pagamento...", + "Pay with": "Pague com", + "Contact and Refund Email": "E-mail de Contacto e Reembolso", + "Contact_Body": "Por favor indique um e-mail abaixo. Entraremos em contacto para este endereço se ocorrer algum problema com o seu pagamento.", + "Your email": "O seu e-mail", + "Continue": "Continuar", + "Please enter a valid email address": "Por favor introduza um e-mail válido", + "Order Amount": "Valor da Encomenda", + "Network Cost": "Custo da Rede", + "Already Paid": "Já Pago", + "Due": "Devido", + // Tabs + "Scan": "Digitalizar", + "Copy": "Copiar", + "Conversion": "Conversão", + // Scan tab + "Open in wallet": "Abrir na carteira", + // Copy tab + "CompletePay_Body": "Para completar o seu pagamento, por favor envie {{btcDue}} {{cryptoCode}} para o endereço abaixo.", + "Amount": "Quantia", + "Address": "Endereço", + "Copied": "Copiado", + // Conversion tab + "ConversionTab_BodyTop": "Pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.", + "ConversionTab_BodyDesc": "Este serviço é oferecido por terceiros. Por favor tenha em mente que não temos qualquer controlo sobre como os seus fundos serão utilizados. A fatura será marcada como paga apenas quando os fundos forem recebidos na Blockchain {{cryptoCode}}.", + "Shapeshift_Button_Text": "Pagar com Altcoins", + "ConversionTab_Lightning": "Não há fornecedores de conversão disponíveis para pagamentos via Lightning Network.", + // Invoice expired + "Invoice expiring soon...": "A fatura está a expirar...", + "Invoice expired": "Fatura expirada", + "What happened?": "O que aconteceu?", + "InvoiceExpired_Body_1": "Esta fatura expirou. Uma fatura é válida durante {{maxTimeMinutes}} minutos. \ +Pode voltar para {{storeName}} se quiser enviar o seu pagamento novamente.", + "InvoiceExpired_Body_2": "Se tentou enviar um pagamento, ele ainda não foi aceite pela rede Bitcoin. Nós ainda não recebemos o valor enviado.", + "InvoiceExpired_Body_3": "Se a transação não for aceite pela rede Bitcoin, o valor voltará para sua carteira. Dependendo da sua carteira, isto pode demorar entre 48 e 72 horas.", + "Invoice ID": "Nº da Fatura", + "Order ID": "Nº da Encomenda", + "Return to StoreName": "Voltar para {{storeName}}", + // Invoice paid + "This invoice has been paid": "Esta fatura foi paga", + // Invoice archived + "This invoice has been archived": "Esta fatura foi arquivada", + "Archived_Body": "Por favor, entre em contacto com o vendedor para informações e suporte", + // Lightning + "BOLT 11 Invoice": "Fatura BOLT 11", + "Node Info": "Informação do Nó", + // + "txCount": "{{count}} transação", + "txCount_plural": "{{count}} transações" +};