diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 2bbcaba46..7ae09e3fd 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -265,7 +265,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - + // Set tolerance to 50% var stores = user.GetController(); var vm = Assert.IsType(Assert.IsType(stores.UpdateStore()).Model); @@ -296,6 +296,22 @@ namespace BTCPayServer.Tests } } + [Fact] + public void RoundupCurrenciesCorrectly() + { + foreach(var test in new[] + { + (0.0005m, "$0.0005 (USD)"), + (0.001m, "$0.001 (USD)"), + (0.01m, "$0.01 (USD)"), + (0.1m, "$0.10 (USD)"), + }) + { + var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable()); + Assert.Equal(test.Item2, actual); + } + } + [Fact] public void CanPayUsingBIP70() { @@ -617,7 +633,7 @@ namespace BTCPayServer.Tests ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); - + var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10); @@ -1411,7 +1427,7 @@ namespace BTCPayServer.Tests { var provider = new BTCPayNetworkProvider(NetworkType.Mainnet); var factory = CreateBTCPayRateFactory(provider); - + foreach (var result in factory .DirectProviders .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync())) @@ -1423,8 +1439,8 @@ namespace BTCPayServer.Tests Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); // This check if the currency pair is using right currency pair - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => ( e.CurrencyPair == new CurrencyPair("BTC", "USD") || + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") || e.CurrencyPair == new CurrencyPair("BTC", "EUR") || e.CurrencyPair == new CurrencyPair("BTC", "USDT")) && e.Value > 1.0m // 1BTC will always be more than 1USD @@ -1454,7 +1470,7 @@ namespace BTCPayServer.Tests private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider) { - return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, null, new CoinAverageSettings()); + return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings()); } [Fact] @@ -1470,7 +1486,6 @@ namespace BTCPayServer.Tests RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules); var factory = CreateBTCPayRateFactory(provider); - factory.DirectProviders.Clear(); factory.CacheSpan = TimeSpan.FromSeconds(10); var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 3c9101e9f..c76e706bb 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.2.15 + 1.0.2.18 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -41,7 +41,7 @@ - + diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index f2b8b423a..546bf9b68 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), Id = invoice.Id, Status = invoice.Status, - TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : + TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" : "low", @@ -61,7 +61,7 @@ namespace BTCPayServer.Controllers MonitoringDate = invoice.MonitoringExpiration, OrderId = invoice.OrderId, BuyerInformation = invoice.BuyerInformation, - Fiat = FormatCurrency((decimal)dto.Price, dto.Currency), + Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable), NotificationUrl = invoice.NotificationURL, RedirectUrl = invoice.RedirectURL, ProductInformation = invoice.ProductInformation, @@ -291,11 +291,29 @@ namespace BTCPayServer.Controllers private string FormatCurrency(PaymentMethod paymentMethod) { string currency = paymentMethod.ParentEntity.ProductInformation.Currency; - return FormatCurrency(paymentMethod.Rate, currency); + return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable); } - public string FormatCurrency(decimal price, string currency) + public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies) { - return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})"; + var provider = ((CultureInfo)currencies.GetCurrencyProvider(currency)).NumberFormat; + var currencyData = currencies.GetCurrencyData(currency); + var divisibility = currencyData.Divisibility; + while (true) + { + var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero); + if ((Math.Abs(rounded - price) / price) < 0.001m) + { + price = rounded; + break; + } + divisibility++; + } + if(divisibility != provider.CurrencyDecimalDigits) + { + provider = (NumberFormatInfo)provider.Clone(); + provider.CurrencyDecimalDigits = divisibility; + } + return price.ToString("C", provider) + $" ({currency})"; } [HttpGet] @@ -430,7 +448,7 @@ namespace BTCPayServer.Controllers var stores = await _StoreRepository.GetStoresByUserId(GetUserId()); model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId); var store = stores.FirstOrDefault(s => s.Id == model.StoreId); - if(store == null) + if (store == null) { ModelState.AddModelError(nameof(model.StoreId), "Store not found"); } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 410739b18..12b7cf005 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -53,8 +53,7 @@ namespace BTCPayServer.Controllers ExplorerClientProvider explorerProvider, IFeeProviderFactory feeRateProvider, LanguageService langService, - IHostingEnvironment env, - CoinAverageSettings coinAverage) + IHostingEnvironment env) { _RateFactory = rateFactory; _Dashboard = dashboard; @@ -72,9 +71,7 @@ namespace BTCPayServer.Controllers _ServiceProvider = serviceProvider; _BtcpayServerOptions = btcpayServerOptions; _BTCPayEnv = btcpayEnv; - _CoinAverage = coinAverage; } - CoinAverageSettings _CoinAverage; NBXplorerDashboard _Dashboard; BTCPayServerOptions _BtcpayServerOptions; BTCPayServerEnvironment _BTCPayEnv; @@ -518,7 +515,7 @@ namespace BTCPayServer.Controllers private CoinAverageExchange[] GetSupportedExchanges() { - return _CoinAverage.AvailableExchanges + return _RateFactory.GetSupportedExchanges() .Select(c => c.Value) .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); diff --git a/BTCPayServer/Payments/Lightning/Charge/ChargeClient.cs b/BTCPayServer/Payments/Lightning/Charge/ChargeClient.cs index 062f01bdc..121e6c001 100644 --- a/BTCPayServer/Payments/Lightning/Charge/ChargeClient.cs +++ b/BTCPayServer/Payments/Lightning/Charge/ChargeClient.cs @@ -156,7 +156,7 @@ namespace BTCPayServer.Payments.Lightning.Charge async Task ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation) { - var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }); + var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation); return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" }; } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index e3e3a6361..76fc5cb18 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -36,17 +36,25 @@ namespace BTCPayServer.Payments.Lightning expiry = TimeSpan.FromSeconds(1); LightningInvoice lightningInvoice = null; - try + + string description = storeBlob.LightningDescriptionTemplate; + description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase); + using (var cts = new CancellationTokenSource(5000)) { - string description = storeBlob.LightningDescriptionTemplate; - description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase); - lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry); - } - catch (Exception ex) - { - throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex); + try + { + lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry, cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex); + } } var nodeInfo = await test; return new LightningLikePaymentMethodDetails() @@ -62,34 +70,36 @@ namespace BTCPayServer.Payments.Lightning if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new PaymentMethodUnavailableException($"Full node not available"); - var cts = new CancellationTokenSource(5000); - var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); - LightningNodeInformation info = null; - try + using (var cts = new CancellationTokenSource(5000)) { - info = await client.GetInfo(cts.Token); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); - } - catch (Exception ex) - { - throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})"); - } + var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); + LightningNodeInformation info = null; + try + { + info = await client.GetInfo(cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})"); + } - if (info.Address == null) - { - throw new PaymentMethodUnavailableException($"No lightning node public address has been configured"); - } + if (info.Address == null) + { + throw new PaymentMethodUnavailableException($"No lightning node public address has been configured"); + } - var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight); - if (blocksGap > 10) - { - throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)"); - } + var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight); + if (blocksGap > 10) + { + throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)"); + } - return new NodeInfo(info.NodeId, info.Address, info.P2PPort); + return new NodeInfo(info.NodeId, info.Address, info.P2PPort); + } } public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation) diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index b115d1d30..381c148dd 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Rating; using ExchangeSharp; @@ -35,7 +36,6 @@ namespace BTCPayServer.Services.Rates } IMemoryCache _Cache; private IOptions _CacheOptions; - CurrencyNameTable _CurrencyTable; public IMemoryCache Cache { get @@ -46,12 +46,10 @@ namespace BTCPayServer.Services.Rates CoinAverageSettings _CoinAverageSettings; public BTCPayRateProviderFactory(IOptions cacheOptions, BTCPayNetworkProvider btcpayNetworkProvider, - CurrencyNameTable currencyTable, CoinAverageSettings coinAverageSettings) { if (cacheOptions == null) throw new ArgumentNullException(nameof(cacheOptions)); - _CurrencyTable = currencyTable; _CoinAverageSettings = coinAverageSettings; _Cache = new MemoryCache(cacheOptions); _CacheOptions = cacheOptions; @@ -70,10 +68,12 @@ namespace BTCPayServer.Services.Rates DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true)); DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false)); DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false)); + DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); // Handmade providers DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/")))); DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider()); + DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, Authenticator = _CoinAverageSettings }); // Those exchanges make multiple requests when calling GetTickers so we remove them //DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true)); @@ -84,6 +84,20 @@ namespace BTCPayServer.Services.Rates //DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI())); } + public CoinAverageExchanges GetSupportedExchanges() + { + CoinAverageExchanges exchanges = new CoinAverageExchanges(); + foreach (var exchange in _CoinAverageSettings.AvailableExchanges) + { + exchanges.Add(exchange.Value); + } + + // Add other exchanges supported here + exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average")); + exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia")); + + return exchanges; + } private readonly Dictionary _DirectProviders = new Dictionary(); public Dictionary DirectProviders @@ -163,13 +177,6 @@ namespace BTCPayServer.Services.Rates } rateRule.Reevaluate(); result.Value = rateRule.Value; - - var currencyData = _CurrencyTable?.GetCurrencyData(rateRule.CurrencyPair.Right); - if(currencyData != null && result.Value.HasValue) - { - result.Value = decimal.Round(result.Value.Value, currencyData.Divisibility, MidpointRounding.AwayFromZero); - } - result.Errors = rateRule.Errors; result.EvaluatedRule = rateRule.ToString(true); result.Rule = rateRule.ToString(false); diff --git a/BTCPayServer/Services/Rates/CoinAverageSettings.cs b/BTCPayServer/Services/Rates/CoinAverageSettings.cs index f3da666f6..9d0c06df2 100644 --- a/BTCPayServer/Services/Rates/CoinAverageSettings.cs +++ b/BTCPayServer/Services/Rates/CoinAverageSettings.cs @@ -43,12 +43,11 @@ namespace BTCPayServer.Services.Rates { public CoinAverageExchanges() { - Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average")); } public void Add(CoinAverageExchange exchange) { - Add(exchange.Name, exchange); + TryAdd(exchange.Name, exchange); } } public class CoinAverageSettings : ICoinAverageAuthenticator diff --git a/BTCPayServer/Views/Account/ConfirmEmail.cshtml b/BTCPayServer/Views/Account/ConfirmEmail.cshtml index 2322bae22..0f7158654 100644 --- a/BTCPayServer/Views/Account/ConfirmEmail.cshtml +++ b/BTCPayServer/Views/Account/ConfirmEmail.cshtml @@ -7,7 +7,7 @@
- +