diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 45047255e..9d04f16d0 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.1 false NU1701,CA1816,CA1308,CA1810,CA2208 diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 980b57cf6..9feaab1f5 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -2,8 +2,11 @@ using BTCPayServer.Hosting; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Rating; +using BTCPayServer.Security; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; +using BTCPayServer.Services.Stores; using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Mocks; using Microsoft.AspNetCore.Hosting; @@ -104,15 +107,6 @@ namespace BTCPayServer.Tests .UseConfiguration(conf) .ConfigureServices(s => { - 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) @@ -126,6 +120,30 @@ namespace BTCPayServer.Tests .Build(); _Host.Start(); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); + + var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory)); + rateProvider.DirectProviders.Clear(); + + var coinAverageMock = new MockRateProvider(); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("BTC_USD"), + Value = 5000m + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("BTC_CAD"), + Value = 4500m + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("LTC_USD"), + Value = 500m + }); + rateProvider.DirectProviders.Add("coinaverage", coinAverageMock); } public string HostName @@ -142,7 +160,7 @@ namespace BTCPayServer.Tests return _Host.Services.GetRequiredService(); } - public T GetController(string userId = null) where T : Controller + public T GetController(string userId = null, string storeId = null) where T : Controller { var context = new DefaultHttpContext(); context.Request.Host = new HostString("127.0.0.1"); @@ -150,7 +168,11 @@ namespace BTCPayServer.Tests context.Request.Protocol = "http"; if (userId != null) { - context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })); + context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication)); + } + if(storeId != null) + { + context.SetStoreData(GetService().FindStore(storeId, userId).GetAwaiter().GetResult()); } var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory)); var provider = scope.CreateScope().ServiceProvider; diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index 978238de0..7f0fab553 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch +FROM microsoft/dotnet:2.1.300-rc1-sdk-alpine3.7 WORKDIR /app # caches restore result by copying csproj file separately COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj diff --git a/BTCPayServer.Tests/Mocks/MockRateProvider.cs b/BTCPayServer.Tests/Mocks/MockRateProvider.cs new file mode 100644 index 000000000..bd151c8ad --- /dev/null +++ b/BTCPayServer.Tests/Mocks/MockRateProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; + +namespace BTCPayServer.Tests.Mocks +{ + public class MockRateProvider : IRateProvider + { + public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); + public Task GetRatesAsync() + { + return Task.FromResult(ExchangeRates); + } + } +} diff --git a/BTCPayServer.Tests/RateRulesTest.cs b/BTCPayServer.Tests/RateRulesTest.cs new file mode 100644 index 000000000..09c26fc73 --- /dev/null +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using BTCPayServer.Rating; +using Xunit; + +namespace BTCPayServer.Tests +{ + public class RateRulesTest + { + [Fact] + public void CanParseRateRules() + { + // Check happy path + StringBuilder builder = new StringBuilder(); + builder.AppendLine("// Some cool comments"); + builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1"); + builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)"); + builder.AppendLine("// Some other cool comments"); + builder.AppendLine("BTC_usd = GDax(BTC_USD)"); + builder.AppendLine("BTC_X = Coinbase(BTC_X);"); + builder.AppendLine("X_X = CoinAverage(X_X) * 1.02"); + + Assert.False(RateRules.TryParse("DPW*&W&#hdi&#&3JJD", out var rules)); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + Assert.Equal( + "// Some cool comments\n" + + "DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" + + "DOGE_BTC = bittrex(DOGE_BTC);\n" + + "// Some other cool comments\n" + + "BTC_USD = gdax(BTC_USD);\n" + + "BTC_X = coinbase(BTC_X);\n" + + "X_X = coinaverage(X_X) * 1.02;", + rules.ToString()); + var tests = new[] + { + (Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"), + (Pair: "BTC_USD", Expected: "gdax(BTC_USD)"), + (Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"), + (Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"), + (Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"), + }; + foreach (var test in tests) + { + Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString()); + } + rules.GlobalMultiplier = 2.32m; + Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString()); + //////////////// + + // Check errors conditions + builder = new StringBuilder(); + builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1"); + builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)"); + builder.AppendLine("BTC_usd = GDax(BTC_USD)"); + builder.AppendLine("LTC_CHF = LTC_CHF * 1.01"); + builder.AppendLine("BTC_X = Coinbase(BTC_X)"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + + tests = new[] + { + (Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"), + (Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"), + (Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"), + }; + foreach (var test in tests) + { + Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString()); + } + ////////////////// + + // Check if we can resolve exchange rates + builder = new StringBuilder(); + builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1"); + builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)"); + builder.AppendLine("BTC_usd = GDax(BTC_USD)"); + builder.AppendLine("BTC_X = Coinbase(BTC_X)"); + builder.AppendLine("X_X = CoinAverage(X_X) * 1.02"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + + var tests2 = new[] + { + (Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"), + (Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"), + (Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"), + (Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"), + (Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"), + }; + foreach (var test in tests2) + { + var rule = rules.GetRuleFor(CurrencyPair.Parse(test.Pair)); + Assert.Equal(test.Expected, rule.ToString()); + Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType().ToArray())); + } + var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD")); + rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000); + rule2.Reevaluate(); + Assert.True(rule2.HasError); + Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true)); + Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false)); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m); + rule2.Reevaluate(); + Assert.False(rule2.HasError); + Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true)); + Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m); + //////// + + // Make sure parenthesis are correctly calculated + builder = new StringBuilder(); + builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X"); + builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5"); + builder.AppendLine("DOGE_BTC = 2000"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + rules.GlobalMultiplier = 1.1m; + + rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")); + Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString()); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m); + Assert.True(rule2.Reevaluate()); + Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true)); + Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value); + + // Test inverse + rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE")); + Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString()); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m); + Assert.True(rule2.Reevaluate()); + Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true)); + Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value); + //////// + + // Make sure kraken is not converted to CurrencyPair + builder = new StringBuilder(); + builder.AppendLine("BTC_USD = kraken(BTC_USD)"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD")); + rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), 1000m); + Assert.True(rule2.Reevaluate()); + } + } +} diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index dd886b407..085d68349 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -44,29 +44,27 @@ namespace BTCPayServer.Tests public async Task GrantAccessAsync() { await RegisterAsync(); - var store = await CreateStoreAsync(); + await CreateStoreAsync(); + var store = this.GetController(); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); Assert.IsType(await store.RequestPairing(pairingCode.ToString())); await store.Pair(pairingCode.ToString(), StoreId); } - public StoresController CreateStore() + public void CreateStore() { - return CreateStoreAsync().GetAwaiter().GetResult(); + CreateStoreAsync().GetAwaiter().GetResult(); } - public T GetController() where T : Controller + public T GetController(bool setImplicitStore = true) where T : Controller { - return parent.PayTester.GetController(UserId); + return parent.PayTester.GetController(UserId, setImplicitStore ? StoreId : null); } - public async Task CreateStoreAsync() + public async Task CreateStoreAsync() { - var store = parent.PayTester.GetController(UserId); + var store = this.GetController(); await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); StoreId = store.CreatedStoreId; - var store2 = parent.PayTester.GetController(UserId); - store2.CreatedStoreId = store.CreatedStoreId; - return store2; } public BTCPayNetwork SupportedNetwork { get; set; } @@ -78,12 +76,12 @@ namespace BTCPayServer.Tests public async Task RegisterDerivationSchemeAsync(string cryptoCode) { SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); - var store = parent.PayTester.GetController(UserId); + var store = parent.PayTester.GetController(UserId, StoreId); ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork); DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); - var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model; + var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model; vm.SpeedPolicy = SpeedPolicy.MediumSpeed; - await store.UpdateStore(StoreId, vm); + await store.UpdateStore(vm); await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() { @@ -127,7 +125,7 @@ namespace BTCPayServer.Tests public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType) { - var storeController = parent.PayTester.GetController(UserId); + var storeController = this.GetController(); await storeController.AddLightningNode(StoreId, new LightningNodeViewModel() { Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri : diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index da5747d80..a21a88bda 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -32,6 +32,12 @@ using BTCPayServer.HostedServices; using BTCPayServer.Payments.Lightning; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Stores; +using System.Net.Http; +using System.Text; +using BTCPayServer.Rating; +using BTCPayServer.Validation; +using ExchangeSharp; namespace BTCPayServer.Tests { @@ -43,6 +49,27 @@ namespace BTCPayServer.Tests Logs.LogProvider = new XUnitLogProvider(helper); } + [Fact] + public void CanHandleUriValidation() + { + var attribute = new UriAttribute(); + Assert.True(attribute.IsValid("http://localhost")); + Assert.True(attribute.IsValid("http://localhost:1234")); + Assert.True(attribute.IsValid("https://localhost")); + Assert.True(attribute.IsValid("https://127.0.0.1")); + Assert.True(attribute.IsValid("http://127.0.0.1")); + Assert.True(attribute.IsValid("http://127.0.0.1:1234")); + Assert.True(attribute.IsValid("http://gozo.com")); + Assert.True(attribute.IsValid("https://gozo.com")); + Assert.True(attribute.IsValid("https://gozo.com:1234")); + Assert.True(attribute.IsValid("https://gozo.com:1234/test.css")); + Assert.True(attribute.IsValid("https://gozo.com:1234/test.png")); + Assert.False(attribute.IsValid("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud e")); + Assert.False(attribute.IsValid(2)); + Assert.False(attribute.IsValid("http://")); + Assert.False(attribute.IsValid("httpdsadsa.com")); + } + [Fact] public void CanCalculateCryptoDue2() { @@ -105,22 +132,11 @@ namespace BTCPayServer.Tests { var entity = new InvoiceEntity(); #pragma warning disable CS0618 - entity.TxFee = Money.Coins(0.1m); - entity.Rate = 5000; - entity.Payments = new System.Collections.Generic.List(); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); entity.ProductInformation = new ProductInformation() { Price = 5000 }; - // Some check that handling legacy stuff does not break things - var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike); - paymentMethod.Calculate(); - Assert.NotNull(paymentMethod); - Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike)); - entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee }); - Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike)); - Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike)); - //////////////////// - + var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); @@ -235,6 +251,89 @@ namespace BTCPayServer.Tests #pragma warning restore CS0618 } + [Fact] + public void CanAcceptInvoiceWithTolerance() + { + var entity = new InvoiceEntity(); +#pragma warning disable CS0618 + entity.Payments = new List(); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); + entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.PaymentTolerance = 0; + + + var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); + var accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(1.1m), accounting.Due); + Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); + Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); + + entity.PaymentTolerance = 10; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); + + entity.PaymentTolerance = 100; + accounting = paymentMethod.Calculate(); + Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); + + } + + [Fact] + public void CanAcceptInvoiceWithTolerance2() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + 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); + Assert.Equal(0.0, vm.PaymentTolerance); + vm.PaymentTolerance = 50.0; + Assert.IsType(stores.UpdateStore(vm).Result); + + var invoice = user.BitPay.CreateInvoice(new Invoice() + { + Buyer = new Buyer() { email = "test@fwf.com" }, + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + // Pays 75% + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); + tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m)); + + Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("paid", localInvoice.Status); + }); + } + } + + [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() { @@ -247,7 +346,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { Buyer = new Buyer() { email = "test@fwf.com" }, - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -303,9 +402,9 @@ namespace BTCPayServer.Tests tester.Start(); var user = tester.NewAccount(); user.GrantAccess(); - var storeController = tester.PayTester.GetController(user.UserId); - Assert.IsType(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()); - Assert.IsType(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult()); + var storeController = user.GetController(); + Assert.IsType(storeController.UpdateStore()); + Assert.IsType(storeController.AddLightningNode(user.StoreId, "BTC")); var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel() { @@ -319,7 +418,7 @@ namespace BTCPayServer.Tests Url = tester.MerchantCharge.Client.Uri.AbsoluteUri }, "save", "BTC").GetAwaiter().GetResult()); - var storeVm = Assert.IsType(Assert.IsType(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model); + var storeVm = Assert.IsType(Assert.IsType(storeController.UpdateStore()).Model); Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); } } @@ -393,7 +492,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 0.01, + Price = 0.01m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -426,7 +525,7 @@ namespace BTCPayServer.Tests var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 0.01, + Price = 0.01m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -454,7 +553,7 @@ namespace BTCPayServer.Tests await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5)); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice() { - Price = 0.01, + Price = 0.01m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -479,8 +578,8 @@ namespace BTCPayServer.Tests acc.Register(); acc.CreateStore(); - var controller = tester.PayTester.GetController(acc.UserId); - var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel() + var controller = acc.GetController(); + var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel() { Facade = Facade.Merchant.ToString(), Label = "bla", @@ -507,7 +606,7 @@ namespace BTCPayServer.Tests acc.RegisterDerivationScheme("BTC"); var invoice = acc.BitPay.CreateInvoice(new Invoice() { - Price = 5.0, + Price = 5.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -538,17 +637,66 @@ namespace BTCPayServer.Tests tester.Start(); var acc = tester.NewAccount(); acc.Register(); - var store = acc.CreateStore(); + acc.CreateStore(); + var store = acc.GetController(); var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant); Assert.IsType(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult()); pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant); - var store2 = acc.CreateStore(); - store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult(); + acc.CreateStore(); + var store2 = acc.GetController(); + store2.Pair(pairingCode.ToString(), store2.StoreData.Id).GetAwaiter().GetResult(); Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase); } } + [Fact] + public void CanListInvoices() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var acc = tester.NewAccount(); + acc.GrantAccess(); + acc.RegisterDerivationScheme("BTC"); + // First we try payment with a merchant having only BTC + var invoice = acc.BitPay.CreateInvoice(new Invoice() + { + Price = 500, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + 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); + cashCow.SendToAddress(invoiceAddress, firstPayment); + Eventually(() => + { + invoice = acc.BitPay.GetInvoice(invoice.Id); + Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); + }); + + + AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}"); + AssertSearchInvoice(acc, false, invoice.Id, $"storeid:blah"); + AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}"); + AssertSearchInvoice(acc, true, invoice.Id, $"exceptionstatus:paidPartial"); + AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver"); + AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true"); + AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false"); + } + } + + private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter) + { + var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController().ListInvoices(filter).Result).Model; + Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId)); + } + [Fact] public void CanRBFPayment() { @@ -560,7 +708,7 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD" }, Facade.Merchant); var payment1 = invoice.BtcDue + Money.Coins(0.0001m); @@ -637,6 +785,36 @@ namespace BTCPayServer.Tests user.GrantAccess(); user.RegisterDerivationScheme("BTC"); Assert.True(user.BitPay.TestAccess(Facade.Merchant)); + + // Can generate API Key + var repo = tester.PayTester.GetService(); + Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.IsType(user.GetController().GenerateAPIKey().GetAwaiter().GetResult()); + + var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + /////// + + // Generating a new one remove the previous + Assert.IsType(user.GetController().GenerateAPIKey().GetAwaiter().GetResult()); + var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.NotEqual(apiKey, apiKey2); + //////// + + apiKey = apiKey2; + + // Can create an invoice with this new API Key + HttpClient client = new HttpClient(); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, tester.PayTester.ServerUri.AbsoluteUri + "invoices"); + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey))); + var invoice = new Invoice() + { + Price = 5000.0m, + Currency = "USD" + }; + message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json"); + var result = client.SendAsync(message).GetAwaiter().GetResult(); + result.EnsureSuccessStatusCode(); + ///////////////////// } } @@ -666,13 +844,13 @@ namespace BTCPayServer.Tests 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; + var storeController = user.GetController(); + var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; vm.PreferredExchange = exchange; - storeController.UpdateStore(user.StoreId, vm).Wait(); + storeController.Rates(vm).Wait(); var invoice2 = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -696,7 +874,7 @@ namespace BTCPayServer.Tests // First we try payment with a merchant having only BTC var invoice1 = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -705,16 +883,16 @@ namespace BTCPayServer.Tests }, Facade.Merchant); - var storeController = tester.PayTester.GetController(user.UserId); - var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; + var storeController = user.GetController(); + var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; Assert.Equal(1.0, vm.RateMultiplier); vm.RateMultiplier = 0.5; - storeController.UpdateStore(user.StoreId, vm).Wait(); + storeController.Rates(vm).Wait(); var invoice2 = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -749,6 +927,11 @@ namespace BTCPayServer.Tests Assert.Single(invoice.CryptoInfo); Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); + Assert.True(invoice.PaymentCodes.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled); + Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC")); + Assert.True(invoice.PaymentTotals.ContainsKey("LTC")); var cashCow = tester.LTCExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); var firstPayment = Money.Coins(0.1m); @@ -770,7 +953,7 @@ namespace BTCPayServer.Tests // Despite it is called BitcoinAddress it should be LTC because BTC is not available Assert.Null(invoice.BitcoinAddress); - Assert.NotEqual(1.0, invoice.Rate); + Assert.NotEqual(1.0m, invoice.Rate); Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due); @@ -785,6 +968,66 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanModifyRates() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + + var store = user.GetController(); + var rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.False(rateVm.ShowScripting); + Assert.Equal("coinaverage", rateVm.PreferredExchange); + Assert.Equal(1.0, rateVm.RateMultiplier); + Assert.Null(rateVm.TestRateRules); + + rateVm.PreferredExchange = "bitflyer"; + Assert.IsType(store.Rates(rateVm, "Save").Result); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.Equal("bitflyer", rateVm.PreferredExchange); + + rateVm.ScriptTest = "BTC_JPY,BTC_CAD"; + rateVm.RateMultiplier = 1.1; + store = user.GetController(); + rateVm = Assert.IsType(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); + Assert.NotNull(rateVm.TestRateRules); + Assert.Equal(2, rateVm.TestRateRules.Count); + Assert.False(rateVm.TestRateRules[0].Error); + Assert.StartsWith("(bitflyer(BTC_JPY)) * 1.10 =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase); + Assert.True(rateVm.TestRateRules[1].Error); + Assert.IsType(store.Rates(rateVm, "Save").Result); + + Assert.IsType(store.ShowRateRulesPost(true).Result); + Assert.IsType(store.Rates(rateVm, "Save").Result); + store = user.GetController(); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.Equal(rateVm.DefaultScript, rateVm.Script); + Assert.True(rateVm.ShowScripting); + rateVm.ScriptTest = "BTC_JPY"; + rateVm = Assert.IsType(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); + Assert.True(rateVm.ShowScripting); + Assert.Contains("(bitflyer(BTC_JPY)) * 1.10 = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase); + + rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD"; + rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" + + "X_CAD = quadrigacx(X_CAD);\n" + + "X_X = gdax(X_X);"; + rateVm.RateMultiplier = 0.5; + rateVm = Assert.IsType(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); + Assert.True(rateVm.TestRateRules.All(t => !t.Error)); + Assert.IsType(store.Rates(rateVm, "Save").Result); + store = user.GetController(); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.Equal(0.5, rateVm.RateMultiplier); + Assert.True(rateVm.ShowScripting); + Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase); + } + } + [Fact] public void CanPayWithTwoCurrencies() { @@ -797,7 +1040,7 @@ namespace BTCPayServer.Tests // First we try payment with a merchant having only BTC var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -823,13 +1066,23 @@ namespace BTCPayServer.Tests Assert.Single(checkout.AvailableCryptos); Assert.Equal("BTC", checkout.CryptoCode); + Assert.Single(invoice.PaymentCodes); + Assert.Single(invoice.SupportedTransactionCurrencies); + Assert.Single(invoice.SupportedTransactionCurrencies); + Assert.Single(invoice.PaymentSubtotals); + Assert.Single(invoice.PaymentTotals); + Assert.True(invoice.PaymentCodes.ContainsKey("BTC")); + Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC")); + Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled); + Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC")); + Assert.True(invoice.PaymentTotals.ContainsKey("BTC")); ////////////////////// // Retry now with LTC enabled user.RegisterDerivationScheme("LTC"); invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -871,6 +1124,18 @@ namespace BTCPayServer.Tests checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value; Assert.Equal(2, checkout.AvailableCryptos.Count); Assert.Equal("LTC", checkout.CryptoCode); + + + Assert.Equal(2, invoice.PaymentCodes.Count()); + Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count()); + Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count()); + Assert.Equal(2, invoice.PaymentSubtotals.Count()); + Assert.Equal(2, invoice.PaymentTotals.Count()); + Assert.True(invoice.PaymentCodes.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC")); + Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled); + Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC")); + Assert.True(invoice.PaymentTotals.ContainsKey("LTC")); } } @@ -944,14 +1209,14 @@ namespace BTCPayServer.Tests user.GrantAccess(); user.RegisterDerivationScheme("BTC"); user.RegisterLightningNode("BTC", LightningConnectionType.Charge); - var vm = Assert.IsType(Assert.IsType(user.GetController().CheckoutExperience(user.StoreId).Result).Model); + var vm = Assert.IsType(Assert.IsType(user.GetController().CheckoutExperience()).Model); vm.LightningMaxValue = "2 USD"; vm.OnChainMinValue = "5 USD"; - Assert.IsType(user.GetController().CheckoutExperience(user.StoreId, vm).Result); + Assert.IsType(user.GetController().CheckoutExperience(vm).Result); var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 1.5, + Price = 1.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -964,7 +1229,7 @@ namespace BTCPayServer.Tests invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5.5, + Price = 5.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1013,7 +1278,7 @@ namespace BTCPayServer.Tests Assert.Equal("$5.00", vmview.Items[0].Price.Formatted); Assert.IsType(apps.ViewPointOfSale(appId, 0, "orange").Result); var invoice = user.BitPay.GetInvoices().First(); - Assert.Equal(10.00, invoice.Price); + Assert.Equal(10.00m, invoice.Price); Assert.Equal("CAD", invoice.Currency); Assert.Equal("orange", invoice.ItemDesc); } @@ -1064,7 +1329,7 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1107,8 +1372,6 @@ namespace BTCPayServer.Tests var txFee = Money.Zero; - var rate = user.BitPay.GetRates(); - var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); @@ -1171,12 +1434,12 @@ namespace BTCPayServer.Tests { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("complete", localInvoice.Status); - Assert.NotEqual(0.0, localInvoice.Rate); + Assert.NotEqual(0.0m, localInvoice.Rate); }); invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 5000.0, + Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1212,40 +1475,102 @@ namespace BTCPayServer.Tests [Fact] public void CheckQuadrigacxRateProvider() { - var quadri = new QuadrigacxRateProvider("BTC"); + var quadri = new QuadrigacxRateProvider(); 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(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult()); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value); + Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD"))); + } - 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(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult()); - Assert.Throws(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult()); + [Fact] + public void CanQueryDirectProviders() + { + 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())) + .ToList()) + { + var exchangeRates = result.ResultAsync.Result; + Assert.NotNull(exchangeRates); + Assert.NotEmpty(exchangeRates); + 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") || + e.CurrencyPair == new CurrencyPair("BTC", "EUR") || + e.CurrencyPair == new CurrencyPair("BTC", "USDT")) + && e.Value > 1.0m // 1BTC will always be more than 1USD + ); + } + } + + [Fact] + public void CanGetRateCryptoCurrenciesByDefault() + { + var provider = new BTCPayNetworkProvider(NetworkType.Mainnet); + var factory = CreateBTCPayRateFactory(provider); + + var pairs = + provider.GetAll() + .Select(c => new CurrencyPair(c.CryptoCode, "USD")) + .ToHashSet(); + + var rules = new StoreBlob().GetDefaultRateRules(provider); + var result = factory.FetchRates(pairs, rules); + foreach (var value in result) + { + var rateResult = value.Value.GetAwaiter().GetResult(); + Assert.NotNull(rateResult.Value); + } + } + + private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider) + { + return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings()); } [Fact] public void CheckRatesProvider() { - var coinAverage = new CoinAverageRateProvider("BTC"); - var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult(); - var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult(); + var provider = new BTCPayNetworkProvider(NetworkType.Mainnet); + var coinAverage = new CoinAverageRateProvider(); + var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult(); + Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY"))); + var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult(); + Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY"))); - var cached = new CachedRateProvider("BTC", coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) })); - cached.CacheSpan = TimeSpan.FromSeconds(10); - var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult(); - var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult(); - //Manually check that cache get hit after 10 sec - var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult(); + RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules); + + var factory = CreateBTCPayRateFactory(provider); + factory.CacheSpan = TimeSpan.FromSeconds(10); + + var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.False(fetchedRate.Cached); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.True(fetchedRate.Cached); + + Thread.Sleep(11000); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.False(fetchedRate.Cached); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.True(fetchedRate.Cached); + // Should cache at exchange level so this should hit the cache + var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.True(fetchedRate.Cached); + Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value); + + // Should cache at exchange level this should not hit the cache as it is different exchange + RateRules.TryParse("X_X = bittrex(X_X);", out rateRules); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.False(fetchedRate.Cached); - var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" }; - var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult(); - Assert.Throws(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult()); } private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) diff --git a/BTCPayServer.Tests/UnitTestPeusa.cs b/BTCPayServer.Tests/UnitTestPeusa.cs index d2b99bd80..14b4e3717 100644 --- a/BTCPayServer.Tests/UnitTestPeusa.cs +++ b/BTCPayServer.Tests/UnitTestPeusa.cs @@ -1,16 +1,5 @@ -using BTCPayServer.Payments.Lightning.Lnd; +using System; using NBitcoin; -using NBitcoin.DataEncoders; -using NBitpayClient; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace BTCPayServer.Tests diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 00f5b94e2..fbc4a6e58 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -62,7 +62,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:1.0.2.2 + image: nicolasdorier/nbxplorer:1.0.2.6 ports: - "32838:32838" expose: @@ -110,14 +110,15 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: nicolasdorier/clightning:0.0.0.11-dev + image: nicolasdorier/clightning:0.0.0.16-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | bitcoin-datadir=/etc/bitcoin bitcoin-rpcconnect=bitcoind network=regtest - ipaddr=customer_lightningd + bind-addr=0.0.0.0 + announce-addr=customer_lightningd log-level=debug dev-broadcast-interval=1000 ports: @@ -151,13 +152,14 @@ services: - merchant_lightningd merchant_lightningd: - image: nicolasdorier/clightning:0.0.0.11-dev + image: nicolasdorier/clightning:0.0.0.14-dev environment: EXPOSE_TCP: "true" LIGHTNINGD_OPT: | bitcoin-datadir=/etc/bitcoin bitcoin-rpcconnect=bitcoind - ipaddr=merchant_lightningd + bind-addr=0.0.0.0 + announce-addr=merchant_lightningd network=regtest log-level=debug dev-broadcast-interval=1000 diff --git a/BTCPayServer/Authentication/TokenRepository.cs b/BTCPayServer/Authentication/TokenRepository.cs index 0473aadca..776533fe3 100644 --- a/BTCPayServer/Authentication/TokenRepository.cs +++ b/BTCPayServer/Authentication/TokenRepository.cs @@ -45,6 +45,46 @@ namespace BTCPayServer.Authentication } } + public async Task GetStoreIdFromAPIKey(string apiKey) + { + using (var ctx = _Factory.CreateContext()) + { + return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync(); + } + } + + public async Task GenerateLegacyAPIKey(string storeId) + { + // It is legacy support and Bitpay generate string of unknown format, trying to replicate them + // as good as possible. The string below got generated for me. + var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE"; + var rand = new Random(Math.Abs(RandomUtils.GetInt32())); + var generated = new char[chars.Length]; + for (int i = 0; i < generated.Length; i++) + { + generated[i] = chars[rand.Next(0, generated.Length)]; + } + + using (var ctx = _Factory.CreateContext()) + { + var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync(); + if (existing != null) + { + ctx.ApiKeys.Remove(existing); + } + ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId }); + await ctx.SaveChangesAsync().ConfigureAwait(false); + } + } + + public async Task GetLegacyAPIKeys(string storeId) + { + using (var ctx = _Factory.CreateContext()) + { + return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync(); + } + } + private BitTokenEntity CreateTokenEntity(PairedSINData data) { return new BitTokenEntity() diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer/BTCPayNetwork.cs index 540a090c8..bc0be4841 100644 --- a/BTCPayServer/BTCPayNetwork.cs +++ b/BTCPayServer/BTCPayNetwork.cs @@ -44,7 +44,6 @@ namespace BTCPayServer public string CryptoCode { get; internal set; } public string BlockExplorerLink { get; internal set; } public string UriScheme { get; internal set; } - public RateProviderDescription DefaultRateProvider { get; set; } [Obsolete("Should not be needed")] public bool IsBTC @@ -62,6 +61,7 @@ namespace BTCPayServer public BTCPayDefaultSettings DefaultSettings { get; set; } public KeyPath CoinType { get; internal set; } public int MaxTrackedConfirmation { get; internal set; } = 6; + public string[] DefaultRateRules { get; internal set; } = Array.Empty(); public override string ToString() { diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs index ca803f0a4..911edff2c 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs @@ -14,9 +14,6 @@ namespace BTCPayServer public void InitBitcoin() { var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC"); - var coinaverage = new CoinAverageRateProviderDescription("BTC"); - var bitpay = new BitpayRateProviderDescription(); - var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay }); Add(new BTCPayNetwork() { CryptoCode = nbxplorerNetwork.CryptoCode, @@ -24,7 +21,6 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "bitcoin", - DefaultRateProvider = btcRate, CryptoImagePath = "imlegacy/bitcoin-symbol.svg", LightningImagePath = "imlegacy/btc-lightning.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), diff --git a/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs b/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs new file mode 100644 index 000000000..c75c3dd18 --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs @@ -0,0 +1,29 @@ +using NBitcoin; + +namespace BTCPayServer +{ + public partial class BTCPayNetworkProvider + { + public void InitBitcoinGold() + { + var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG"); + Add(new BTCPayNetwork() + { + CryptoCode = nbxplorerNetwork.CryptoCode, + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}", + NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "bitcoingold", + DefaultRateRules = new[] + { + "BTG_X = BTG_BTC * BTC_X", + "BTG_BTC = bitfinex(BTG_BTC)", + }, + CryptoImagePath = "imlegacy/btg-symbol.svg", + LightningImagePath = "imlegacy/btg-symbol.svg", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'") + }); + } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs b/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs index 18091ad91..e12ceaff7 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs @@ -20,7 +20,11 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "dogecoin", - DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"), + DefaultRateRules = new[] + { + "DOGE_X = DOGE_BTC * BTC_X", + "DOGE_BTC = bittrex(DOGE_BTC)" + }, CryptoImagePath = "imlegacy/dogecoin.png", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'") diff --git a/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs b/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs index 98550f9e0..42b3de248 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs @@ -20,7 +20,6 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "litecoin", - DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"), CryptoImagePath = "imlegacy/litecoin-symbol.svg", LightningImagePath = "imlegacy/ltc-lightning.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), diff --git a/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs b/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs new file mode 100644 index 000000000..bb086132b --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.Monacoin.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Rates; +using NBitcoin; +using NBXplorer; + +namespace BTCPayServer +{ + public partial class BTCPayNetworkProvider + { + public void InitMonacoin() + { + var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA"); + Add(new BTCPayNetwork() + { + CryptoCode = nbxplorerNetwork.CryptoCode, + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}", + NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "monacoin", + DefaultRateRules = new[] + { + "MONA_X = MONA_BTC * BTC_X", + "MONA_BTC = zaif(MONA_BTC)" + }, + CryptoImagePath = "imlegacy/monacoin.png", + LightningImagePath = "imlegacy/mona-lightning.svg", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'") + }); + } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs index 1717adce2..1aadb7bc3 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -48,6 +48,8 @@ namespace BTCPayServer InitBitcoin(); InitLitecoin(); InitDogecoin(); + InitBitcoinGold(); + InitMonacoin(); } /// @@ -86,7 +88,11 @@ namespace BTCPayServer public BTCPayNetwork GetNetwork(string cryptoCode) { - _Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network); + if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network)) + { + if (cryptoCode == "XBT") + return GetNetwork("BTC"); + } return network; } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index f98b986e4..ca3aa0595 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -1,8 +1,8 @@ - + Exe - netcoreapp2.0 - 1.0.1.93 + netcoreapp2.1 + 1.0.2.21 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -30,38 +30,35 @@ - - + + - + - - + + - - + + - + - - - + + - - + + - - diff --git a/BTCPayServer/Controllers/AccessTokenController.cs b/BTCPayServer/Controllers/AccessTokenController.cs index 25d42ca6b..ab6f15028 100644 --- a/BTCPayServer/Controllers/AccessTokenController.cs +++ b/BTCPayServer/Controllers/AccessTokenController.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; namespace BTCPayServer.Controllers { + [BitpayAPIConstraint] public class AccessTokenController : Controller { TokenRepository _TokenRepository; diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 26c291ace..89ab09a8a 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -16,10 +16,11 @@ using BTCPayServer.Services; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Stores; using BTCPayServer.Logging; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { - [Authorize] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [Route("[controller]/[action]")] public class AccountController : Controller { diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index 1cb1218f5..27290739b 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -162,7 +162,8 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{appId}/pos")] - public async Task ViewPointOfSale(string appId, double amount, string choiceKey) + [IgnoreAntiforgeryToken] + public async Task ViewPointOfSale(string appId, decimal amount, string choiceKey) { var app = await GetApp(appId, AppType.PointOfSale); if (string.IsNullOrEmpty(choiceKey) && amount <= 0) @@ -177,7 +178,7 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); } string title = null; - double price = 0.0; + var price = 0.0m; if (!string.IsNullOrEmpty(choiceKey)) { var choices = Parse(settings.Template, settings.Currency); @@ -185,7 +186,7 @@ namespace BTCPayServer.Controllers if (choice == null) return NotFound(); title = choice.Title; - price = (double)choice.Price.Value; + price = choice.Price.Value; } else { diff --git a/BTCPayServer/Controllers/AppsController.cs b/BTCPayServer/Controllers/AppsController.cs index 5e79741fc..143f6d92a 100644 --- a/BTCPayServer/Controllers/AppsController.cs +++ b/BTCPayServer/Controllers/AppsController.cs @@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) .SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId)) .FirstOrDefaultAsync(); + if (app == null) + return null; if (type != null && type.Value.ToString() != app.AppType) return null; return app; @@ -174,24 +176,19 @@ namespace BTCPayServer.Controllers using (var ctx = _ContextFactory.CreateContext()) { return await ctx.UserStore - .Where(us => us.ApplicationUserId == userId) - .Select(us => new - { - IsOwner = us.Role == StoreRoles.Owner, - StoreId = us.StoreDataId, - StoreName = us.StoreData.StoreName, - Apps = us.StoreData.Apps - }) - .SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel() - { - IsOwner = us.IsOwner, - AppName = app.Name, - AppType = app.AppType, - Id = app.Id, - StoreId = us.StoreId, - StoreName = us.StoreName - })) - .ToArrayAsync(); + .Where(us => us.ApplicationUserId == userId) + .Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, + (us, app) => + new ListAppsViewModel.ListAppViewModel() + { + IsOwner = us.Role == StoreRoles.Owner, + StoreId = us.StoreDataId, + StoreName = us.StoreData.StoreName, + AppName = app.Name, + AppType = app.AppType, + Id = app.Id + }) + .ToArrayAsync(); } } diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index 49e7036f1..336ea6118 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -14,24 +14,5 @@ namespace BTCPayServer.Controllers { return View("Home"); } - - public IActionResult About() - { - ViewData["Message"] = "Your application description page."; - - return View(); - } - - public IActionResult Contact() - { - ViewData["Message"] = "Your contact page."; - - return View(); - } - - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } } } diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index 7dba3be1c..aac4ee764 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -13,26 +13,26 @@ using BTCPayServer.Data; using BTCPayServer.Services.Invoices; using Microsoft.AspNetCore.Cors; using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authorization; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { [EnableCors("BitpayAPI")] [BitpayAPIConstraint] + [Authorize(Policies.CanUseStore.Key)] public class InvoiceControllerAPI : Controller { private InvoiceController _InvoiceController; private InvoiceRepository _InvoiceRepository; - private StoreRepository _StoreRepository; private BTCPayNetworkProvider _NetworkProvider; public InvoiceControllerAPI(InvoiceController invoiceController, InvoiceRepository invoceRepository, - StoreRepository storeRepository, BTCPayNetworkProvider networkProvider) { this._InvoiceController = invoiceController; this._InvoiceRepository = invoceRepository; - this._StoreRepository = storeRepository; this._NetworkProvider = networkProvider; } @@ -41,20 +41,15 @@ namespace BTCPayServer.Controllers [MediaTypeConstraint("application/json")] public async Task> CreateInvoice([FromBody] Invoice invoice) { - var store = await _StoreRepository.FindStore(this.User.GetStoreId()); - if (store == null) - throw new BitpayHttpException(401, "Can't access to store"); - return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot()); + return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot()); } [HttpGet] [Route("invoices/{id}")] + [AllowAnonymous] public async Task> GetInvoice(string id, string token) { - var store = await _StoreRepository.FindStore(this.User.GetStoreId()); - if (store == null) - throw new BitpayHttpException(401, "Can't access to store"); - var invoice = await _InvoiceRepository.GetInvoice(store.Id, id); + var invoice = await _InvoiceRepository.GetInvoice(null, id); if (invoice == null) throw new BitpayHttpException(404, "Object not found"); var resp = invoice.EntityToDTO(_NetworkProvider); @@ -75,10 +70,7 @@ namespace BTCPayServer.Controllers { if (dateEnd != null) dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day - - var store = await _StoreRepository.FindStore(this.User.GetStoreId()); - if (store == null) - throw new BitpayHttpException(401, "Can't access to store"); + var query = new InvoiceQuery() { Count = limit, @@ -88,10 +80,9 @@ namespace BTCPayServer.Controllers OrderId = orderId, ItemCode = itemCode, Status = status == null ? null : new[] { status }, - StoreId = new[] { store.Id } + StoreId = new[] { this.HttpContext.GetStoreData().Id } }; - var entities = (await _InvoiceRepository.GetInvoices(query)) .Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray(); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 356ed2486..7515c18da 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -22,6 +22,7 @@ using BTCPayServer.Events; using NBXplorer; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { @@ -50,14 +51,17 @@ 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" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low", + TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : + invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : + invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" : + "low", RefundEmail = invoice.RefundMail, CreatedDate = invoice.InvoiceTime, ExpirationDate = invoice.ExpirationTime, 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, @@ -74,6 +78,7 @@ namespace BTCPayServer.Controllers cryptoPayment.PaymentMethod = ToString(paymentMethodId); cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}"; cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}"; + cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}"; var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (onchainMethod != null) @@ -199,6 +204,12 @@ namespace BTCPayServer.Controllers var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr); var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + if (network == null && isDefaultCrypto) + { + network = _NetworkProvider.GetAll().FirstOrDefault(); + paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); + paymentMethodIdStr = paymentMethodId.ToString(); + } if (invoice == null || network == null) return null; if (!invoice.Support(paymentMethodId)) @@ -208,6 +219,7 @@ namespace BTCPayServer.Controllers var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First(); network = paymentMethodTemp.Network; paymentMethodId = paymentMethodTemp.GetId(); + paymentMethodIdStr = paymentMethodId.ToString(); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider); @@ -226,11 +238,13 @@ namespace BTCPayServer.Controllers OrderId = invoice.OrderId, InvoiceId = invoice.Id, DefaultLang = storeBlob.DefaultLang ?? "en-US", + HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice", CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri, CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri, BtcAddress = paymentMethodDetails.GetPaymentDestination(), - OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), BtcDue = accounting.Due.ToString(), + OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), + OrderAmountFiat = OrderAmountFiat(invoice.ProductInformation), CustomerEmail = invoice.RefundMail, RequiresRefundEmail = storeBlob.RequiresRefundEmail, ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), @@ -278,11 +292,39 @@ 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 = currencies.GetNumberFormatInfo(currency); + 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})"; + } + private string OrderAmountFiat(ProductInformation productInformation) + { + // check if invoice source currency is crypto... if it is there is no "order amount in fiat" + if (_NetworkProvider.GetNetwork(productInformation.Currency) != null) + { + return null; + } + + return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable); } [HttpGet] @@ -355,7 +397,7 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("invoices")] - [Authorize(AuthenticationSchemes = "Identity.Application")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] public async Task ListInvoices(string searchTerm = null, int skip = 0, int count = 50) { @@ -367,14 +409,19 @@ namespace BTCPayServer.Controllers Count = count, Skip = skip, UserId = GetUserId(), + Unusual = !filterString.Filters.ContainsKey("unusual") ? null + : !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null + : r, Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null, + ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null, StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null })) { model.SearchTerm = searchTerm; model.Invoices.Add(new InvoiceModel() { - Status = invoice.Status, + Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"), + ShowCheckout = invoice.Status == "new", Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago", InvoiceId = invoice.Id, OrderId = invoice.OrderId ?? string.Empty, @@ -390,11 +437,11 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("invoices/create")] - [Authorize(AuthenticationSchemes = "Identity.Application")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] public async Task CreateInvoice() { - var stores = await GetStores(GetUserId()); + var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null); if (stores.Count() == 0) { StatusMessage = "Error: You need to create at least one store before creating a transaction"; @@ -405,18 +452,23 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("invoices/create")] - [Authorize(AuthenticationSchemes = "Identity.Application")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] public async Task CreateInvoice(CreateInvoiceModel model) { - model.Stores = await GetStores(GetUserId(), model.StoreId); + 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) + { + ModelState.AddModelError(nameof(model.StoreId), "Store not found"); + } if (!ModelState.IsValid) { return View(model); } - var store = await _StoreRepository.FindStore(model.StoreId, GetUserId()); StatusMessage = null; - if (store.Role != StoreRoles.Owner) + if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice"); return View(model); @@ -454,20 +506,15 @@ namespace BTCPayServer.Controllers StatusMessage = $"Invoice {result.Data.Id} just created!"; return RedirectToAction(nameof(ListInvoices)); } - catch (RateUnavailableException) + catch (BitpayHttpException ex) { - ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency"); + ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}"); return View(model); } } - private async Task GetStores(string userId, string storeId = null) - { - return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); - } - [HttpPost] - [Authorize(AuthenticationSchemes = "Identity.Application")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] public IActionResult SearchInvoice(InvoicesModel invoices) { @@ -481,7 +528,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("invoices/invalidatepaid")] - [Authorize(AuthenticationSchemes = "Identity.Application")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] public async Task InvalidatePaidInvoice(string invoiceId) { diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 03a89cc84..f10757151 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy; using NBXplorer; using BTCPayServer.HostedServices; using BTCPayServer.Payments; +using BTCPayServer.Rating; namespace BTCPayServer.Controllers { public partial class InvoiceController : Controller { InvoiceRepository _InvoiceRepository; - IRateProviderFactory _RateProviders; + BTCPayRateProviderFactory _RateProvider; StoreRepository _StoreRepository; UserManager _UserManager; private CurrencyNameTable _CurrencyNameTable; @@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers InvoiceRepository invoiceRepository, CurrencyNameTable currencyNameTable, UserManager userManager, - IRateProviderFactory rateProviders, + BTCPayRateProviderFactory rateProvider, StoreRepository storeRepository, EventAggregator eventAggregator, BTCPayWalletProvider walletProvider, @@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); - _RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders)); + _RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider)); _UserManager = userManager; _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; @@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers entity.ExtendedNotifications = invoice.ExtendedNotifications; entity.NotificationURL = notificationUri?.AbsoluteUri; entity.BuyerInformation = Map(invoice); + entity.PaymentTolerance = storeBlob.PaymentTolerance; //Another way of passing buyer info to support FillBuyerInfo(invoice.Buyer, entity.BuyerInformation); if (entity?.BuyerInformation?.BuyerEmail != null) @@ -111,6 +113,23 @@ namespace BTCPayServer.Controllers entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); + HashSet currencyPairsToFetch = new HashSet(); + var rules = storeBlob.GetRateRules(_NetworkProvider); + + foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) + .Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)) + .Where(c => c != null)) + { + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency)); + if (storeBlob.LightningMaxValue != null) + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency)); + if (storeBlob.OnChainMinValue != null) + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency)); + } + + var rateRules = storeBlob.GetRateRules(_NetworkProvider); + var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules); + var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) .Select(c => (Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())), @@ -119,19 +138,45 @@ namespace BTCPayServer.Controllers .Where(c => c.Network != null) .Select(o => (SupportedPaymentMethod: o.SupportedPaymentMethod, - PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store))) + PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store))) .ToList(); List paymentMethodErrors = new List(); List supported = new List(); var paymentMethods = new PaymentMethodDictionary(); + + foreach(var pair in fetchingByCurrencyPair) + { + var rateResult = await pair.Value; + bool hasError = false; + if(rateResult.Errors.Count != 0) + { + var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray()); + paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})"); + hasError = true; + } + if(rateResult.ExchangeExceptions.Count != 0) + { + foreach(var ex in rateResult.ExchangeExceptions) + { + paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})"); + } + hasError = true; + } + if(hasError) + { + paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}"); + paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}"); + } + } + foreach (var o in supportedPaymentMethods) { try { var paymentMethod = await o.PaymentMethod; if (paymentMethod == null) - throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)"); + throw new PaymentMethodUnavailableException("Payment method unavailable"); supported.Add(o.SupportedPaymentMethod); paymentMethods.Add(paymentMethod); } @@ -158,23 +203,6 @@ namespace BTCPayServer.Controllers entity.SetSupportedPaymentMethods(supported); entity.SetPaymentMethods(paymentMethods); -#pragma warning disable CS0618 - // Legacy Bitpay clients expect information for BTC information, even if the store do not support it - var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain); - if (!legacyBTCisSet && _NetworkProvider.BTC != null) - { - var btc = _NetworkProvider.BTC; - var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc); - var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules()); - if (feeProvider != null && rateProvider != null) - { - var gettingFee = feeProvider.GetFeeRateAsync(); - var gettingRate = rateProvider.GetRateAsync(invoice.Currency); - entity.TxFee = GetTxFee(storeBlob, await gettingFee); - entity.Rate = await gettingRate; - } -#pragma warning restore CS0618 - } entity.PosData = invoice.PosData; entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider); @@ -183,15 +211,17 @@ namespace BTCPayServer.Controllers return new DataWrapper(resp) { Facade = "pos/invoice" }; } - private async Task CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) + private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) { var storeBlob = store.GetStoreBlob(); - var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency); + var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; + if (rate.Value == null) + return null; PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; paymentMethod.SetId(supportedPaymentMethod.PaymentId); - paymentMethod.Rate = rate; + paymentMethod.Rate = rate.Value.Value; var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network); if (storeBlob.NetworkFeeDisabled) paymentDetails.SetNoTxFee(); @@ -217,16 +247,14 @@ namespace BTCPayServer.Controllers if (compare != null) { - var limitValueRate = 0.0m; - if (limitValue.Currency == entity.ProductInformation.Currency) - limitValueRate = paymentMethod.Rate; - else - limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency); - - var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate); - if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) + var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)]; + if (limitValueRate.Value.HasValue) { - throw new PaymentMethodUnavailableException(errorMessage); + var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value); + if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) + { + throw new PaymentMethodUnavailableException(errorMessage); + } } } /////////////// @@ -243,19 +271,13 @@ namespace BTCPayServer.Controllers return paymentMethod; } -#pragma warning disable CS0618 - private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate) - { - return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100); - } -#pragma warning restore CS0618 - private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy) { if (transactionSpeed == null) return defaultPolicy; var mappings = new Dictionary(); mappings.Add("low", SpeedPolicy.LowSpeed); + mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed); mappings.Add("medium", SpeedPolicy.MediumSpeed); mappings.Add("high", SpeedPolicy.HighSpeed); if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy)) diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 800e1a3d3..84b0552a2 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -21,10 +21,11 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Mails; using System.Globalization; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { - [Authorize] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [Route("[controller]/[action]")] public class ManageController : Controller { diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 63336ed0b..9f5f9a4a2 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -8,17 +8,19 @@ using System.Threading.Tasks; using BTCPayServer.Filters; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; +using BTCPayServer.Rating; +using Newtonsoft.Json; namespace BTCPayServer.Controllers { public class RateController : Controller { - IRateProviderFactory _RateProviderFactory; + BTCPayRateProviderFactory _RateProviderFactory; BTCPayNetworkProvider _NetworkProvider; CurrencyNameTable _CurrencyNameTable; StoreRepository _StoreRepo; public RateController( - IRateProviderFactory rateProviderFactory, + BTCPayRateProviderFactory rateProviderFactory, BTCPayNetworkProvider networkProvider, StoreRepository storeRepo, CurrencyNameTable currencyNameTable) @@ -32,45 +34,101 @@ namespace BTCPayServer.Controllers [Route("rates")] [HttpGet] [BitpayAPIConstraint] - public async Task GetRates(string cryptoCode = null, string storeId = null) + public async Task GetRates(string currencyPairs, string storeId) { - var result = await GetRates2(cryptoCode, storeId); - var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[]; - if(rates == null) + storeId = storeId ?? this.HttpContext.GetStoreData()?.Id; + var result = await GetRates2(currencyPairs, storeId); + var rates = (result as JsonResult)?.Value as Rate[]; + if (rates == null) return result; - return Json(new DataWrapper(rates)); + return Json(new DataWrapper(rates)); } [Route("api/rates")] [HttpGet] - public async Task GetRates2(string cryptoCode = null, string storeId = null) + public async Task GetRates2(string currencyPairs, string storeId) { - cryptoCode = cryptoCode ?? "BTC"; - var network = _NetworkProvider.GetNetwork(cryptoCode); - if (network == null) - return NotFound(); - - RateRules rules = null; - if (storeId != null) + if(storeId == null || currencyPairs == null) { - var store = await _StoreRepo.FindStore(storeId); - if (store == null) - return NotFound(); - rules = store.GetStoreBlob().GetRateRules(); + var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" }); + result.StatusCode = 400; + return result; } - var rateProvider = _RateProviderFactory.GetRateProvider(network, rules); - if (rateProvider == null) - return NotFound(); + var store = this.HttpContext.GetStoreData(); + if(store == null || store.Id != storeId) + store = await _StoreRepo.FindStore(storeId); + if (store == null) + { + var result = Json(new BitpayErrorsModel() { Error = "Store not found" }); + result.StatusCode = 404; + return result; + } + var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); - var allRates = (await rateProvider.GetRatesAsync()); - return Json(allRates.Select(r => - new NBitpayClient.Rate() + HashSet pairs = new HashSet(); + foreach(var currency in currencyPairs.Split(',')) + { + if(!CurrencyPair.TryParse(currency, out var pair)) + { + var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" }); + result.StatusCode = 400; + return result; + } + pairs.Add(pair); + } + + var fetching = _RateProviderFactory.FetchRates(pairs, rules); + await Task.WhenAll(fetching.Select(f => f.Value).ToArray()); + return Json(pairs + .Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value)) + .Where(r => r.Value.HasValue) + .Select(r => + new Rate() { - Code = r.Currency, - Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name, - Value = r.Value + CryptoCode = r.Pair.Left, + Code = r.Pair.Right, + CurrencyPair = r.Pair.ToString(), + Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name, + Value = r.Value.Value }).Where(n => n.Name != null).ToArray()); } + + public class Rate + { + + [JsonProperty(PropertyName = "name")] + public string Name + { + get; + set; + } + [JsonProperty(PropertyName = "cryptoCode")] + public string CryptoCode + { + get; + set; + } + + [JsonProperty(PropertyName = "currencyPair")] + public string CurrencyPair + { + get; + set; + } + + [JsonProperty(PropertyName = "code")] + public string Code + { + get; + set; + } + [JsonProperty(PropertyName = "rate")] + public decimal Value + { + get; + set; + } + } } } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 08c41e878..2c10440d0 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -19,15 +19,15 @@ using System.Threading.Tasks; namespace BTCPayServer.Controllers { - [Authorize(Roles = Roles.ServerAdmin)] + [Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)] public class ServerController : Controller { private UserManager _UserManager; SettingsRepository _SettingsRepository; - private IRateProviderFactory _RateProviderFactory; + private BTCPayRateProviderFactory _RateProviderFactory; public ServerController(UserManager userManager, - IRateProviderFactory rateProviderFactory, + BTCPayRateProviderFactory rateProviderFactory, SettingsRepository settingsRepository) { _UserManager = userManager; @@ -99,7 +99,7 @@ namespace BTCPayServer.Controllers }; if (!withAuth || settings.GetCoinAverageSignature() != null) { - return new CoinAverageRateProvider("BTC") + return new CoinAverageRateProvider() { Authenticator = settings }; } return null; @@ -241,10 +241,13 @@ namespace BTCPayServer.Controllers { if (command == "Test") { - if (!ModelState.IsValid) - return View(model); try { + if(!model.Settings.IsComplete()) + { + model.StatusMessage = "Error: Required fields missing"; + return View(model); + } var client = model.Settings.CreateSmtpClient(); await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test"); model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it"; @@ -255,11 +258,8 @@ namespace BTCPayServer.Controllers } return View(model); } - else + else // if(command == "Save") { - ModelState.Remove(nameof(model.TestEmail)); - if (!ModelState.IsValid) - return View(model); await _SettingsRepository.UpdateSetting(model.Settings); model.StatusMessage = "Email settings saved"; return View(model); diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 0bb73d207..944c97f4f 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -10,6 +10,7 @@ using BTCPayServer.Data; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Services; +using LedgerWallet; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBXplorer.DerivationStrategy; @@ -21,9 +22,9 @@ namespace BTCPayServer.Controllers { [HttpGet] [Route("{storeId}/derivations/{cryptoCode}")] - public async Task AddDerivationScheme(string storeId, string cryptoCode) + public IActionResult AddDerivationScheme(string storeId, string cryptoCode) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); @@ -60,7 +61,7 @@ namespace BTCPayServer.Controllers { vm.ServerUrl = GetStoreUrl(storeId); vm.CryptoCode = cryptoCode; - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); @@ -188,7 +189,7 @@ namespace BTCPayServer.Controllers { if (!HttpContext.WebSockets.IsWebSocketRequest) return NotFound(); - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); @@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers { var strategy = GetDirectDerivationStrategy(store, network); var strategyBase = GetDerivationStrategy(store, network); - if (strategy == null || !await hw.SupportDerivation(network, strategy)) + if (strategy == null || await hw.GetKeyPath(network, strategy) == null) { throw new Exception($"This store is not configured to use this ledger"); } @@ -286,11 +287,76 @@ namespace BTCPayServer.Controllers var unspentCoins = await wallet.GetUnspentCoins(strategyBase); var changeAddress = await change; - var transaction = await hw.SendToAddress(strategy, unspentCoins, network, - new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) }, - feeRateValue, - changeAddress.Item1, - changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee); + var send = new[] { ( + destination: destinationAddress as IDestination, + amount: amountBTC, + substractFees: subsctractFeesValue) }; + + foreach (var element in send) + { + if (element.destination == null) + throw new ArgumentNullException(nameof(element.destination)); + if (element.amount == null) + throw new ArgumentNullException(nameof(element.amount)); + if (element.amount <= Money.Zero) + throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); + } + + var foundKeyPath = await hw.GetKeyPath(network, strategy); + if (foundKeyPath == null) + { + throw new HardwareWalletException($"This store is not configured to use this ledger"); + } + + TransactionBuilder builder = new TransactionBuilder(); + builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; + builder.SetConsensusFactory(network.NBitcoinNetwork); + builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray()); + + foreach (var element in send) + { + builder.Send(element.destination, element.amount); + if (element.substractFees) + builder.SubtractFees(); + } + builder.SetChange(changeAddress.Item1); + builder.SendEstimatedFees(feeRateValue); + builder.Shuffle(); + var unsigned = builder.BuildTransaction(false); + + var keypaths = new Dictionary(); + foreach (var c in unspentCoins) + { + keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); + } + + var hasChange = unsigned.Outputs.Count == 2; + var usedCoins = builder.FindSpentCoins(unsigned); + + Dictionary parentTransactions = new Dictionary(); + + if(!strategy.Segwit) + { + var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet(); + var explorer = _ExplorerProvider.GetExplorerClient(network); + var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList(); + foreach(var getTransactionAsync in getTransactionAsyncs) + { + var tx = (await getTransactionAsync.Op); + if(tx == null) + throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found"); + parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction); + } + } + + var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest + { + InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash), + InputCoin = c, + KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), + PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey + }).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null); + try { var broadcastResult = await wallet.BroadcastTransactionsAsync(new List() { transaction }); @@ -336,8 +402,6 @@ namespace BTCPayServer.Controllers var directStrategy = strategy as DirectDerivationStrategy; if (directStrategy == null) directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; - if (!directStrategy.Segwit) - return null; return directStrategy; } diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index 546c2d757..1b51b51b4 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/lightning/{cryptoCode}")] - public async Task AddLightningNode(string storeId, string cryptoCode) + public IActionResult AddLightningNode(string storeId, string cryptoCode) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); LightningNodeViewModel vm = new LightningNodeViewModel(); @@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers public async Task AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) { vm.CryptoCode = cryptoCode; - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 5b33e4c63..12b7cf005 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -4,6 +4,8 @@ using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Rating; +using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; @@ -19,6 +21,7 @@ using NBitcoin.DataEncoders; using NBXplorer.DerivationStrategy; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -27,11 +30,12 @@ using System.Threading.Tasks; namespace BTCPayServer.Controllers { [Route("stores")] - [Authorize(AuthenticationSchemes = "Identity.Application")] - [Authorize(Policy = StorePolicies.OwnStore)] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] + [Authorize(Policy = Policies.CanModifyStoreSettings.Key)] [AutoValidateAntiforgeryToken] public partial class StoresController : Controller { + BTCPayRateProviderFactory _RateFactory; public string CreatedStoreId { get; set; } public StoresController( NBXplorerDashboard dashboard, @@ -45,12 +49,13 @@ namespace BTCPayServer.Controllers AccessTokenController tokenController, BTCPayWalletProvider walletProvider, BTCPayNetworkProvider networkProvider, + BTCPayRateProviderFactory rateFactory, ExplorerClientProvider explorerProvider, IFeeProviderFactory feeRateProvider, LanguageService langService, - IHostingEnvironment env, - CoinAverageSettings coinAverage) + IHostingEnvironment env) { + _RateFactory = rateFactory; _Dashboard = dashboard; _Repo = repo; _TokenRepository = tokenRepo; @@ -66,9 +71,7 @@ namespace BTCPayServer.Controllers _ServiceProvider = serviceProvider; _BtcpayServerOptions = btcpayServerOptions; _BTCPayEnv = btcpayEnv; - _CoinAverage = coinAverage; } - CoinAverageSettings _CoinAverage; NBXplorerDashboard _Dashboard; BTCPayServerOptions _BtcpayServerOptions; BTCPayServerEnvironment _BTCPayEnv; @@ -93,13 +96,10 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/wallet/{cryptoCode}")] - public async Task Wallet(string storeId, string cryptoCode) + public IActionResult Wallet(string cryptoCode) { - var store = await _Repo.FindStore(storeId, GetUserId()); - if (store == null) - return NotFound(); WalletModel model = new WalletModel(); - model.ServerUrl = GetStoreUrl(storeId); + model.ServerUrl = GetStoreUrl(StoreData.Id); model.CryptoCurrency = cryptoCode; return View(model); } @@ -111,17 +111,17 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/users")] - public async Task StoreUsers(string storeId) + public async Task StoreUsers() { StoreUsersViewModel vm = new StoreUsersViewModel(); - await FillUsers(storeId, vm); + await FillUsers(vm); return View(vm); } - private async Task FillUsers(string storeId, StoreUsersViewModel vm) + private async Task FillUsers(StoreUsersViewModel vm) { - var users = await _Repo.GetStoreUsers(storeId); - vm.StoreId = storeId; + var users = await _Repo.GetStoreUsers(StoreData.Id); + vm.StoreId = StoreData.Id; vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel() { Email = u.Email, @@ -130,11 +130,20 @@ namespace BTCPayServer.Controllers }).ToList(); } + public StoreData StoreData + { + get + { + return this.HttpContext.GetStoreData(); + } + } + + [HttpPost] [Route("{storeId}/users")] - public async Task StoreUsers(string storeId, StoreUsersViewModel vm) + public async Task StoreUsers(StoreUsersViewModel vm) { - await FillUsers(storeId, vm); + await FillUsers(vm); if (!ModelState.IsValid) { return View(vm); @@ -150,7 +159,7 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(vm.Role), "Invalid role"); return View(vm); } - if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role)) + if (!await _Repo.AddStoreUser(StoreData.Id, user.Id, vm.Role)) { ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store"); return View(vm); @@ -161,19 +170,16 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/users/{userId}/delete")] - public async Task DeleteStoreUser(string storeId, string userId) + public async Task DeleteStoreUser(string userId) { StoreUsersViewModel vm = new StoreUsersViewModel(); - var store = await _Repo.FindStore(storeId, userId); - if (store == null) - return NotFound(); var user = await _UserManager.FindByIdAsync(userId); if (user == null) return NotFound(); return View("Confirm", new ConfirmModel() { Title = $"Remove store user", - Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?", + Description = $"Are you sure to remove access to remove access to {user.Email}?", Action = "Delete" }); } @@ -188,15 +194,151 @@ namespace BTCPayServer.Controllers } [HttpGet] - [Route("{storeId}/checkout")] - public async Task CheckoutExperience(string storeId) + [Route("{storeId}/rates")] + public IActionResult Rates() { - var store = await _Repo.FindStore(storeId, GetUserId()); - if (store == null) - return NotFound(); - var storeBlob = store.GetStoreBlob(); + var storeBlob = StoreData.GetStoreBlob(); + var vm = new RatesViewModel(); + vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName); + vm.RateMultiplier = (double)storeBlob.GetRateMultiplier(); + vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); + vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); + vm.AvailableExchanges = GetSupportedExchanges(); + vm.ShowScripting = storeBlob.RateScripting; + return View(vm); + } + + [HttpPost] + [Route("{storeId}/rates")] + public async Task Rates(RatesViewModel model, string command = null) + { + model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange); + if (!ModelState.IsValid) + { + return View(model); + } + if (model.PreferredExchange != null) + model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); + + var blob = StoreData.GetStoreBlob(); + model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); + model.AvailableExchanges = GetSupportedExchanges(); + + blob.PreferredExchange = model.PreferredExchange; + blob.SetRateMultiplier(model.RateMultiplier); + + if (!model.ShowScripting) + { + if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase)) + { + ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); + return View(model); + } + } + RateRules rules = null; + if (model.ShowScripting) + { + if (!RateRules.TryParse(model.Script, out rules, out var errors)) + { + errors = errors ?? new List(); + var errorString = String.Join(", ", errors.ToArray()); + ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); + return View(model); + } + else + { + blob.RateScript = rules.ToString(); + ModelState.Remove(nameof(model.Script)); + model.Script = blob.RateScript; + } + } + rules = blob.GetRateRules(_NetworkProvider); + + if (command == "Test") + { + if (string.IsNullOrWhiteSpace(model.ScriptTest)) + { + ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)"); + return View(model); + } + var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries); + + var pairs = new List(); + foreach (var pair in splitted) + { + if (!CurrencyPair.TryParse(pair, out var currencyPair)) + { + ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); + return View(model); + } + pairs.Add(currencyPair); + } + + var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules); + var testResults = new List(); + foreach (var fetch in fetchs) + { + var testResult = await (fetch.Value); + testResults.Add(new RatesViewModel.TestResultViewModel() + { + CurrencyPair = fetch.Key.ToString(), + Error = testResult.Errors.Count != 0, + Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture) + : testResult.EvaluatedRule + }); + } + model.TestRateRules = testResults; + return View(model); + } + else // command == Save + { + if (StoreData.SetStoreBlob(blob)) + { + await _Repo.UpdateStore(StoreData); + StatusMessage = "Rate settings updated"; + } + return RedirectToAction(nameof(Rates), new + { + storeId = StoreData.Id + }); + } + } + + [HttpGet] + [Route("{storeId}/rates/confirm")] + public IActionResult ShowRateRules(bool scripting) + { + return View("Confirm", new ConfirmModel() + { + Action = "Continue", + Title = "Rate rule scripting", + Description = scripting ? + "This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)" + : "This action will delete your rate script. Are you sure to turn off rate rules scripting?", + ButtonClass = "btn-primary" + }); + } + + [HttpPost] + [Route("{storeId}/rates/confirm")] + public async Task ShowRateRulesPost(bool scripting) + { + var blob = StoreData.GetStoreBlob(); + blob.RateScripting = scripting; + blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); + StoreData.SetStoreBlob(blob); + await _Repo.UpdateStore(StoreData); + StatusMessage = "Rate rules scripting activated"; + return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id }); + } + + [HttpGet] + [Route("{storeId}/checkout")] + public IActionResult CheckoutExperience() + { + var storeBlob = StoreData.GetStoreBlob(); var vm = new CheckoutExperienceViewModel(); - vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto()); + vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto()); vm.SetLanguages(_LangService, storeBlob.DefaultLang); vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? ""; vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? ""; @@ -204,12 +346,13 @@ namespace BTCPayServer.Controllers vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri; vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri; + vm.HtmlTitle = storeBlob.HtmlTitle; return View(vm); } [HttpPost] [Route("{storeId}/checkout")] - public async Task CheckoutExperience(string storeId, CheckoutExperienceViewModel model) + public async Task CheckoutExperience(CheckoutExperienceViewModel model) { CurrencyValue lightningMaxValue = null; if (!string.IsNullOrWhiteSpace(model.LightningMaxValue)) @@ -228,16 +371,12 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value"); } } - - var store = await _Repo.FindStore(storeId, GetUserId()); - if (store == null) - return NotFound(); bool needUpdate = false; - var blob = store.GetStoreBlob(); - if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency) + var blob = StoreData.GetStoreBlob(); + if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency) { needUpdate = true; - store.SetDefaultCrypto(model.DefaultCryptoCurrency); + StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency); } model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency); model.SetLanguages(_LangService, model.DefaultLang); @@ -253,33 +392,33 @@ namespace BTCPayServer.Controllers blob.OnChainMinValue = onchainMinValue; blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute); blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute); - if (store.SetStoreBlob(blob)) + blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; + if (StoreData.SetStoreBlob(blob)) { needUpdate = true; } if (needUpdate) { - await _Repo.UpdateStore(store); + await _Repo.UpdateStore(StoreData); StatusMessage = "Store successfully updated"; } return RedirectToAction(nameof(CheckoutExperience), new { - storeId = storeId + storeId = StoreData.Id }); } [HttpGet] [Route("{storeId}")] - public async Task UpdateStore(string storeId) + public IActionResult UpdateStore() { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); 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; @@ -288,8 +427,8 @@ namespace BTCPayServer.Controllers AddPaymentMethods(store, vm); vm.MonitoringExpiration = storeBlob.MonitoringExpiration; vm.InvoiceExpiration = storeBlob.InvoiceExpiration; - vm.RateMultiplier = (double)storeBlob.GetRateMultiplier(); vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; + vm.PaymentTolerance = storeBlob.PaymentTolerance; return View(vm); } @@ -329,81 +468,57 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{storeId}")] - public async Task UpdateStore(string storeId, StoreViewModel model) + public async Task UpdateStore(StoreViewModel model) { - model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange); - if (!ModelState.IsValid) - { - return View(model); - } - if (model.PreferredExchange != null) - model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); - var store = await _Repo.FindStore(storeId, GetUserId()); - if (store == null) - return NotFound(); - AddPaymentMethods(store, model); + AddPaymentMethods(StoreData, model); bool needUpdate = false; - if (store.SpeedPolicy != model.SpeedPolicy) + if (StoreData.SpeedPolicy != model.SpeedPolicy) { needUpdate = true; - store.SpeedPolicy = model.SpeedPolicy; + StoreData.SpeedPolicy = model.SpeedPolicy; } - if (store.StoreName != model.StoreName) + if (StoreData.StoreName != model.StoreName) { needUpdate = true; - store.StoreName = model.StoreName; + StoreData.StoreName = model.StoreName; } - if (store.StoreWebsite != model.StoreWebsite) + if (StoreData.StoreWebsite != model.StoreWebsite) { needUpdate = true; - store.StoreWebsite = model.StoreWebsite; + StoreData.StoreWebsite = model.StoreWebsite; } - var blob = store.GetStoreBlob(); + var blob = StoreData.GetStoreBlob(); blob.NetworkFeeDisabled = !model.NetworkFee; blob.MonitoringExpiration = model.MonitoringExpiration; blob.InvoiceExpiration = model.InvoiceExpiration; blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty; + blob.PaymentTolerance = model.PaymentTolerance; - bool newExchange = blob.PreferredExchange != model.PreferredExchange; - blob.PreferredExchange = model.PreferredExchange; - - blob.SetRateMultiplier(model.RateMultiplier); - - if (store.SetStoreBlob(blob)) + if (StoreData.SetStoreBlob(blob)) { needUpdate = true; } - if (!blob.PreferredExchange.IsCoinAverage() && newExchange) - { - - if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase)) - { - ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); - return View(model); - } - } - if (needUpdate) { - await _Repo.UpdateStore(store); + await _Repo.UpdateStore(StoreData); StatusMessage = "Store successfully updated"; } return RedirectToAction(nameof(UpdateStore), new { - storeId = storeId + storeId = StoreData.Id }); } - private (String DisplayName, String Name)[] GetSupportedExchanges() + private CoinAverageExchange[] GetSupportedExchanges() { - return new[] { ("Coin Average", "coinaverage") } - .Concat(_CoinAverage.AvailableExchanges) - .OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase) - .ToArray(); + return _RateFactory.GetSupportedExchanges() + .Select(c => c.Value) + .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); } private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) @@ -415,10 +530,10 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/Tokens")] - public async Task ListTokens(string storeId) + public async Task ListTokens() { var model = new TokensViewModel(); - var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId); + var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id); model.StatusMessage = StatusMessage; model.Tokens = tokens.Select(t => new TokenViewModel() { @@ -427,30 +542,43 @@ namespace BTCPayServer.Controllers SIN = t.SIN, Id = t.Value }).ToArray(); + + model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(StoreData.Id)).FirstOrDefault(); + if (model.ApiKey == null) + model.EncodedApiKey = "*API Key*"; + else + model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); return View(model); } [HttpPost] [Route("/api-tokens")] [Route("{storeId}/Tokens/Create")] - public async Task CreateToken(string storeId, CreateTokenViewModel model) + [AllowAnonymous] + public async Task CreateToken(CreateTokenViewModel model) { if (!ModelState.IsValid) { return View(model); } model.Label = model.Label ?? String.Empty; - storeId = model.StoreId ?? storeId; var userId = GetUserId(); if (userId == null) - return Unauthorized(); - var store = await _Repo.FindStore(storeId, userId); - if (store == null) - return Unauthorized(); - if (store.Role != StoreRoles.Owner) + return Challenge(Policies.CookieAuthentication); + + var store = StoreData; + var storeId = StoreData?.Id; + if (storeId == null) { - StatusMessage = "Error: You need to be owner of this store to request pairing codes"; - return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); + storeId = model.StoreId; + store = await _Repo.FindStore(storeId, userId); + if (store == null) + return Challenge(Policies.CookieAuthentication); + } + + if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) + { + return Challenge(Policies.CookieAuthentication); } var tokenRequest = new TokenRequest() @@ -491,11 +619,20 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("/api-tokens")] [Route("{storeId}/Tokens/Create")] - public async Task CreateToken(string storeId) + [AllowAnonymous] + public async Task CreateToken() { var userId = GetUserId(); if (string.IsNullOrWhiteSpace(userId)) - return Unauthorized(); + return Challenge(Policies.CookieAuthentication); + var storeId = StoreData?.Id; + if (StoreData != null) + { + if (!StoreData.HasClaim(Policies.CanModifyStoreSettings.Key)) + { + return Challenge(Policies.CookieAuthentication); + } + } var model = new CreateTokenViewModel(); model.Facade = "merchant"; ViewBag.HidePublicKey = storeId == null; @@ -504,20 +641,25 @@ namespace BTCPayServer.Controllers model.StoreId = storeId; if (storeId == null) { - model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); + var stores = await _Repo.GetStoresByUserId(userId); + model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); + if (model.Stores.Count() == 0) + { + StatusMessage = "Error: You need to be owner of at least one store before pairing"; + return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); + } } - return View(model); } [HttpPost] [Route("{storeId}/Tokens/Delete")] - public async Task DeleteToken(string storeId, string tokenId) + public async Task DeleteToken(string tokenId) { var token = await _TokenRepository.GetToken(tokenId); if (token == null || - token.StoreId != storeId || + token.StoreId != StoreData.Id || !await _TokenRepository.DeleteToken(tokenId)) StatusMessage = "Failure to revoke this token"; else @@ -525,11 +667,26 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ListTokens)); } + [HttpPost] + [Route("{storeId}/tokens/apikey")] + public async Task GenerateAPIKey() + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + await _TokenRepository.GenerateLegacyAPIKey(StoreData.Id); + StatusMessage = "API Key re-generated"; + return RedirectToAction(nameof(ListTokens)); + } [HttpGet] [Route("/api-access-request")] + [AllowAnonymous] public async Task RequestPairing(string pairingCode, string selectedStore = null) { + var userId = GetUserId(); + if (userId == null) + return Challenge(Policies.CookieAuthentication); if (pairingCode == null) return NotFound(); var pairing = await _TokenRepository.GetPairingAsync(pairingCode); @@ -540,7 +697,7 @@ namespace BTCPayServer.Controllers } else { - var stores = await _Repo.GetStoresByUserId(GetUserId()); + var stores = await _Repo.GetStoresByUserId(userId); return View(new PairingModel() { Id = pairing.Id, @@ -548,7 +705,7 @@ namespace BTCPayServer.Controllers Label = pairing.Label, SIN = pairing.SIN ?? "Server-Initiated Pairing", SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id, - Stores = stores.Select(s => new PairingModel.StoreViewModel() + Stores = stores.Where(u => u.HasClaim(Policies.CanModifyStoreSettings.Key)).Select(s => new PairingModel.StoreViewModel() { Id = s.Id, Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName @@ -559,19 +716,22 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("/api-access-request")] + [AllowAnonymous] public async Task Pair(string pairingCode, string selectedStore) { if (pairingCode == null) return NotFound(); - var store = await _Repo.FindStore(selectedStore, GetUserId()); + var userId = GetUserId(); + if (userId == null) + return Challenge(Policies.CookieAuthentication); + var store = await _Repo.FindStore(selectedStore, userId); var pairing = await _TokenRepository.GetPairingAsync(pairingCode); if (store == null || pairing == null) return NotFound(); - if (store.Role != StoreRoles.Owner) + if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { - StatusMessage = "Error: You can't approve a pairing without being owner of the store"; - return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); + return Challenge(Policies.CookieAuthentication); } var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); @@ -597,6 +757,8 @@ namespace BTCPayServer.Controllers private string GetUserId() { + if (User.Identity.AuthenticationType != Policies.CookieAuthentication) + return null; return _UserManager.GetUserId(User); } } diff --git a/BTCPayServer/Controllers/UserStoresController.cs b/BTCPayServer/Controllers/UserStoresController.cs index b0000ee07..417277356 100644 --- a/BTCPayServer/Controllers/UserStoresController.cs +++ b/BTCPayServer/Controllers/UserStoresController.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Security; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; @@ -15,7 +16,7 @@ using NBXplorer.DerivationStrategy; namespace BTCPayServer.Controllers { [Route("stores")] - [Authorize(AuthenticationSchemes = "Identity.Application")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [AutoValidateAntiforgeryToken] public partial class UserStoresController : Controller { @@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers } [HttpGet] [Route("{storeId}/delete")] - public async Task DeleteStore(string storeId) + public IActionResult DeleteStore(string storeId) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); return View("Confirm", new ConfirmModel() @@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers public async Task DeleteStorePost(string storeId) { var userId = GetUserId(); - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); await _Repo.RemoveStore(storeId, userId); @@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers Id = store.Id, Name = store.StoreName, WebSite = store.StoreWebsite, - IsOwner = store.Role == StoreRoles.Owner, - Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty() + IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key), + Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty() }); } return View(result); diff --git a/BTCPayServer/Data/APIKeyData.cs b/BTCPayServer/Data/APIKeyData.cs new file mode 100644 index 000000000..e826c32f7 --- /dev/null +++ b/BTCPayServer/Data/APIKeyData.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Data +{ + public class APIKeyData + { + [MaxLength(50)] + public string Id + { + get; set; + } + + [MaxLength(50)] + public string StoreId + { + get; set; + } + } +} diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index 9ac63c03b..fb4507a99 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -86,6 +86,11 @@ namespace BTCPayServer.Data get; set; } + public DbSet ApiKeys + { + get; set; + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -112,6 +117,8 @@ namespace BTCPayServer.Data t.StoreDataId }); + builder.Entity() + .HasIndex(o => o.StoreId); builder.Entity() .HasOne(a => a.StoreData); diff --git a/BTCPayServer/Data/ApplicationDbContextFactory.cs b/BTCPayServer/Data/ApplicationDbContextFactory.cs index be1d86ffd..8e6171d49 100644 --- a/BTCPayServer/Data/ApplicationDbContextFactory.cs +++ b/BTCPayServer/Data/ApplicationDbContextFactory.cs @@ -41,10 +41,12 @@ namespace BTCPayServer.Data public void ConfigureHangfireBuilder(IGlobalConfiguration builder) { - if (_Type == DatabaseType.Sqlite) - builder.UseMemoryStorage(); //Sql provider does not support multiple workers - else if (_Type == DatabaseType.Postgres) - builder.UsePostgreSqlStorage(_ConnectionString); + builder.UseMemoryStorage(); + //We always use memory storage because of incompatibilities with the latest postgres in 2.1 + //if (_Type == DatabaseType.Sqlite) + // builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers + //else if (_Type == DatabaseType.Postgres) + // builder.UsePostgreSqlStorage(_ConnectionString); } } } diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 58923bbf1..9fa4224a2 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -14,6 +14,11 @@ using Newtonsoft.Json.Linq; using BTCPayServer.Services.Rates; using BTCPayServer.Payments; using BTCPayServer.JsonConverters; +using System.ComponentModel.DataAnnotations; +using BTCPayServer.Services; +using System.Security.Claims; +using BTCPayServer.Security; +using BTCPayServer.Rating; namespace BTCPayServer.Data { @@ -120,7 +125,7 @@ namespace BTCPayServer.Data } } - if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain) + if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain) { DerivationStrategy = null; } @@ -151,10 +156,35 @@ namespace BTCPayServer.Data } [NotMapped] + [Obsolete] public string Role { get; set; } + + public Claim[] GetClaims() + { + List claims = new List(); +#pragma warning disable CS0612 // Type or member is obsolete + var role = Role; +#pragma warning restore CS0612 // Type or member is obsolete + if (role == StoreRoles.Owner) + { + claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id)); + claims.Add(new Claim(Policies.CanUseStore.Key, Id)); + } + if (role == StoreRoles.Guest) + { + claims.Add(new Claim(Policies.CanUseStore.Key, Id)); + } + return claims.ToArray(); + } + + public bool HasClaim(string claim) + { + return GetClaims().Any(c => c.Type == claim); + } + public byte[] StoreBlob { get; @@ -178,7 +208,10 @@ namespace BTCPayServer.Data public StoreBlob GetStoreBlob() { - return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject(Encoding.UTF8.GetString(StoreBlob)); + var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject(Encoding.UTF8.GetString(StoreBlob)); + if (result.PreferredExchange == null) + result.PreferredExchange = CoinAverageRateProvider.CoinAverageName; + return result; } public bool SetStoreBlob(StoreBlob storeBlob) @@ -192,9 +225,9 @@ namespace BTCPayServer.Data } } - public class RateRule + public class RateRule_Obsolete { - public RateRule() + public RateRule_Obsolete() { RuleName = "Multiplier"; } @@ -214,6 +247,7 @@ namespace BTCPayServer.Data { InvoiceExpiration = 15; MonitoringExpiration = 60; + PaymentTolerance = 0; RequiresRefundEmail = true; } public bool NetworkFeeDisabled @@ -246,8 +280,8 @@ namespace BTCPayServer.Data public void SetRateMultiplier(double rate) { - RateRules = new List(); - RateRules.Add(new RateRule() { Multiplier = rate }); + RateRules = new List(); + RateRules.Add(new RateRule_Obsolete() { Multiplier = rate }); } public decimal GetRateMultiplier() { @@ -261,7 +295,7 @@ namespace BTCPayServer.Data return rate; } - public List RateRules { get; set; } = new List(); + public List RateRules { get; set; } = new List(); public string PreferredExchange { get; set; } [JsonConverter(typeof(CurrencyValueJsonConverter))] @@ -273,6 +307,11 @@ namespace BTCPayServer.Data public Uri CustomLogo { get; set; } [JsonConverter(typeof(UriJsonConverter))] public Uri CustomCSS { get; set; } + public string HtmlTitle { get; set; } + + public bool RateScripting { get; set; } + + public string RateScript { get; set; } string _LightningDescriptionTemplate; @@ -288,12 +327,48 @@ namespace BTCPayServer.Data } } - public RateRules GetRateRules() + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public double PaymentTolerance { get; set; } + + public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider) { - return new RateRules(RateRules) + if (!RateScripting || + string.IsNullOrEmpty(RateScript) || + !BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules)) { - PreferredExchange = PreferredExchange - }; + return GetDefaultRateRules(networkProvider); + } + else + { + rules.GlobalMultiplier = GetRateMultiplier(); + return rules; + } + } + + public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider) + { + StringBuilder builder = new StringBuilder(); + foreach (var network in networkProvider.GetAll()) + { + if (network.DefaultRateRules.Length != 0) + { + builder.AppendLine($"// Default rate rules for {network.CryptoCode}"); + foreach (var line in network.DefaultRateRules) + { + builder.AppendLine(line); + } + builder.AppendLine($"////////"); + builder.AppendLine(); + } + } + + var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange; + builder.AppendLine($"X_X = {preferredExchange}(X_X);"); + + BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules); + rules.GlobalMultiplier = GetRateMultiplier(); + return rules; } } } diff --git a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs index 15280bb19..5774140ff 100644 --- a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs +++ b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs @@ -6,21 +6,6 @@ using BTCPayServer.HostedServices; namespace BTCPayServer.Events { - public class NBXplorerErrorEvent - { - public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage) - { - Message = errorMessage; - Network = network; - } - public string Message { get; set; } - public BTCPayNetwork Network { get; set; } - - public override string ToString() - { - return $"{Network.CryptoCode}: NBXplorer error `{Message}`"; - } - } public class NBXplorerStateChangedEvent { public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index cef8a007e..e6e7e3520 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -30,6 +30,7 @@ using BTCPayServer.Models; using System.Security.Claims; using System.Globalization; using BTCPayServer.Services; +using BTCPayServer.Data; namespace BTCPayServer { @@ -103,12 +104,6 @@ namespace BTCPayServer return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; } - public static bool IsCoinAverage(this string exchangeName) - { - string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" }; - return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false; - } - public static async Task> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken)) { hashes = hashes.Distinct().ToArray(); @@ -134,6 +129,14 @@ namespace BTCPayServer request.PathBase.ToUriComponent()); } + public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl) + { + bool isRelative = + (redirectUrl.Length > 0 && redirectUrl[0] == '/') + || !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri; + return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl; + } + public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf) { services.Configure(o => @@ -153,19 +156,49 @@ namespace BTCPayServer return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault(); } + public static void SetIsBitpayAPI(this HttpContext ctx, bool value) + { + NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value); + } + + public static void AddRange(this HashSet hashSet, IEnumerable items) + { + foreach(var item in items) + { + hashSet.Add(item); + } + } + public static bool GetIsBitpayAPI(this HttpContext ctx) + { + return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) && + obj is bool b && b; + } + + public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value) + { + NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value); + } + + public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx) + { + ctx.Items.TryGetValue("BitpayAuth", out object obj); + return ((string Signature, String Id, String Authorization))obj; + } + + public static StoreData GetStoreData(this HttpContext ctx) + { + return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData; + } + public static void SetStoreData(this HttpContext ctx, StoreData storeData) + { + ctx.Items["BTCPAY.STOREDATA"] = storeData; + } + private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; public static string ToJson(this object o) { var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings); return res; } - - public static HtmlString ToJSVariableModel(this object o, string variableName) - { - var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson()); - return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');"); - } - - } } diff --git a/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs b/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs index abca0f908..afb9e597a 100644 --- a/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs +++ b/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs @@ -43,9 +43,7 @@ namespace BTCPayServer.Filters public bool Accept(ActionConstraintContext context) { - var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any(); - var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any(); - return (hasVersion || hasIdentity) == IsBitpayAPI; + return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI; } } diff --git a/BTCPayServer/HostedServices/CssThemeManager.cs b/BTCPayServer/HostedServices/CssThemeManager.cs index e3afcf35b..b0e32673b 100644 --- a/BTCPayServer/HostedServices/CssThemeManager.cs +++ b/BTCPayServer/HostedServices/CssThemeManager.cs @@ -41,6 +41,13 @@ namespace BTCPayServer.HostedServices { get { return _creativeStartUri; } } + + public bool ShowRegister { get; set; } + + internal void Update(PoliciesSettings data) + { + ShowRegister = !data.LockSubscription; + } } public class CssThemeManagerHostedService : BaseAsyncService @@ -58,10 +65,19 @@ namespace BTCPayServer.HostedServices { return new[] { - CreateLoopTask(ListenForThemeChanges) + CreateLoopTask(ListenForThemeChanges), + CreateLoopTask(ListenForPoliciesChanges), }; } + async Task ListenForPoliciesChanges() + { + await new SynchronizationContextRemover(); + var data = (await _SettingsRepository.GetSettingAsync()) ?? new PoliciesSettings(); + _CssThemeManager.Update(data); + await _SettingsRepository.WaitSettingsChanged(Cancellation); + } + async Task ListenForThemeChanges() { await new SynchronizationContextRemover(); diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 86f24d7d4..6605e11c8 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -198,7 +198,11 @@ namespace BTCPayServer.HostedServices PosData = dto.PosData, Price = dto.Price, Status = dto.Status, - BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) } + BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }, + PaymentSubtotals = dto.PaymentSubtotals, + PaymentTotals = dto.PaymentTotals, + AmountPaid = dto.AmountPaid, + ExchangeRates = dto.ExchangeRates }; // We keep backward compatibility with bitpay by passing BTC info to the notification @@ -207,7 +211,7 @@ namespace BTCPayServer.HostedServices if (btcCryptoInfo != null) { #pragma warning disable CS0618 - notification.Rate = (double)dto.Rate; + notification.Rate = dto.Rate; notification.Url = dto.Url; notification.BTCDue = dto.BTCDue; notification.BTCPaid = dto.BTCPaid; @@ -305,7 +309,10 @@ namespace BTCPayServer.HostedServices leases.Add(_EventAggregator.Subscribe(async e => { var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId); - await SaveEvent(invoice.Id, e); + List tasks = new List(); + + // Awaiting this later help make sure invoices should arrive in order + tasks.Add(SaveEvent(invoice.Id, e)); // we need to use the status in the event and not in the invoice. The invoice might now be in another status. if (invoice.FullNotifications) @@ -315,20 +322,22 @@ namespace BTCPayServer.HostedServices e.Name == "invoice_failedToConfirm" || e.Name == "invoice_markedInvalid" || e.Name == "invoice_failedToConfirm" || - e.Name == "invoice_completed" + e.Name == "invoice_completed" || + e.Name == "invoice_expiredPaidPartial" ) - await Notify(invoice); + tasks.Add(Notify(invoice)); } if (e.Name == "invoice_confirmed") { - await Notify(invoice); + tasks.Add(Notify(invoice)); } if (invoice.ExtendedNotifications) { - await Notify(invoice, e.EventCode, e.Name); + tasks.Add(Notify(invoice, e.EventCode, e.Name)); } + await Task.WhenAll(tasks.ToArray()); })); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 5aef0a738..a6a2be0d0 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -68,6 +68,8 @@ namespace BTCPayServer.HostedServices context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired")); invoice.Status = "expired"; + if(invoice.ExceptionStatus == "paidPartial") + context.Events.Add(new InvoiceEvent(invoice, 2000, "invoice_expiredPaidPartial")); } var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray(); @@ -78,7 +80,7 @@ namespace BTCPayServer.HostedServices var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); if (invoice.Status == "new" || invoice.Status == "expired") { - if (accounting.Paid >= accounting.TotalDue) + if (accounting.Paid >= accounting.MinimumTotalDue) { if (invoice.Status == "new") { @@ -96,17 +98,17 @@ namespace BTCPayServer.HostedServices } } - if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") + if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") { - invoice.ExceptionStatus = "paidPartial"; - context.MarkDirty(); + invoice.ExceptionStatus = "paidPartial"; + context.MarkDirty(); } } // Just make sure RBF did not cancelled a payment if (invoice.Status == "paid") { - if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver") + if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver") { invoice.ExceptionStatus = null; context.MarkDirty(); @@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices context.MarkDirty(); } - if (accounting.Paid < accounting.TotalDue) + if (accounting.Paid < accounting.MinimumTotalDue) { invoice.Status = "new"; invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial"; @@ -134,14 +136,14 @@ namespace BTCPayServer.HostedServices (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed - (confirmedAccounting.Paid < accounting.TotalDue)) + (confirmedAccounting.Paid < accounting.MinimumTotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } - else if (confirmedAccounting.Paid >= accounting.TotalDue) + else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); @@ -153,7 +155,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "confirmed") { var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network)); - if (completedAccounting.Paid >= accounting.TotalDue) + if (completedAccounting.Paid >= accounting.MinimumTotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; @@ -289,7 +291,7 @@ namespace BTCPayServer.HostedServices if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus); - updateContext.Events.Add(new InvoiceDataChangedEvent(invoice)); + updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } foreach (var evt in updateContext.Events) diff --git a/BTCPayServer/HostedServices/NBXplorerWaiter.cs b/BTCPayServer/HostedServices/NBXplorerWaiter.cs index 3ac9b52ce..72eb0f9dc 100644 --- a/BTCPayServer/HostedServices/NBXplorerWaiter.cs +++ b/BTCPayServer/HostedServices/NBXplorerWaiter.cs @@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices { State = NBXplorerState.NotConnected; status = null; - _Aggregator.Publish(new NBXplorerErrorEvent(_Network, error)); + Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`"); } _Dashboard.Publish(_Network, State, status, error); diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index dcb0b404d..77d4dde18 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -17,15 +17,15 @@ namespace BTCPayServer.HostedServices public class RatesHostedService : BaseAsyncService { private SettingsRepository _SettingsRepository; - private IRateProviderFactory _RateProviderFactory; private CoinAverageSettings _coinAverageSettings; + BTCPayRateProviderFactory _RateProviderFactory; public RatesHostedService(SettingsRepository repo, - CoinAverageSettings coinAverageSettings, - IRateProviderFactory rateProviderFactory) + BTCPayRateProviderFactory rateProviderFactory, + CoinAverageSettings coinAverageSettings) { this._SettingsRepository = repo; - _RateProviderFactory = rateProviderFactory; _coinAverageSettings = coinAverageSettings; + _RateProviderFactory = rateProviderFactory; } internal override Task[] InitializeTasks() @@ -40,11 +40,15 @@ namespace BTCPayServer.HostedServices async Task RefreshCoinAverageSupportedExchanges() { await new SynchronizationContextRemover(); - var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync(); - _coinAverageSettings.AvailableExchanges = tickers + var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync(); + var exchanges = new CoinAverageExchanges(); + foreach(var item in tickers .Exchanges - .Select(c => (c.DisplayName, c.Name)) - .ToArray(); + .Select(c => new CoinAverageExchange(c.Name, c.DisplayName))) + { + exchanges.Add(item); + } + _coinAverageSettings.AvailableExchanges = exchanges; await Task.Delay(TimeSpan.FromHours(5), Cancellation); } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index f2ce5b78a..fe6216073 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -12,7 +12,6 @@ using NBitcoin; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; using System.IO; -using Microsoft.Data.Sqlite; using NBXplorer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; @@ -38,55 +37,13 @@ using Microsoft.Extensions.Caching.Memory; using BTCPayServer.Logging; using BTCPayServer.HostedServices; using Meziantou.AspNetCore.BundleTagHelpers; +using System.Security.Claims; +using BTCPayServer.Security; namespace BTCPayServer.Hosting { public static class BTCPayServerServices { - public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement - { - public OwnStoreAuthorizationRequirement() - { - } - - public OwnStoreAuthorizationRequirement(string role) - { - Role = role; - } - - public string Role - { - get; set; - } - } - - public class OwnStoreHandler : AuthorizationHandler - { - StoreRepository _StoreRepository; - UserManager _UserManager; - public OwnStoreHandler(StoreRepository storeRepository, UserManager userManager) - { - _StoreRepository = storeRepository; - _UserManager = userManager; - } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement) - { - object storeId = null; - if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId)) - context.Succeed(requirement); - else if (storeId != null) - { - var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User); - if (user != null) - { - var store = await _StoreRepository.FindStore((string)storeId, user); - if (store != null) - if (requirement.Role == null || requirement.Role == store.Role) - context.Succeed(requirement); - } - } - } - } public static IServiceCollection AddBTCPayServer(this IServiceCollection services) { services.AddDbContext((provider, o) => @@ -110,7 +67,6 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); @@ -160,6 +116,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient, BTCPayClaimsFilter>(); + services.AddTransient, BitpayClaimsFilter>(); services.TryAddSingleton(); services.TryAddSingleton(o => @@ -169,30 +127,17 @@ namespace BTCPayServer.Hosting else return new Bitpay(new Key(), new Uri("https://test.bitpay.com/")); }); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddScoped(); - services.TryAddSingleton(); services.AddTransient(); services.AddTransient(); // Add application services. services.AddTransient(); - - services.AddAuthorization(o => - { - o.AddPolicy(StorePolicies.CanAccessStores, builder => - { - builder.AddRequirements(new OwnStoreAuthorizationRequirement()); - }); - - o.AddPolicy(StorePolicies.OwnStore, builder => - { - builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner)); - }); - }); - // bundling + services.AddAuthorization(o => Policies.AddBTCPayPolicies(o)); + services.AddBundles(); services.AddTransient(provider => { diff --git a/BTCPayServer/Hosting/BTCpayMiddleware.cs b/BTCPayServer/Hosting/BTCpayMiddleware.cs index 5eab8d6c5..a74c8d723 100644 --- a/BTCPayServer/Hosting/BTCpayMiddleware.cs +++ b/BTCPayServer/Hosting/BTCpayMiddleware.cs @@ -6,41 +6,25 @@ using System.Collections.Generic; using System.Text; using System.Linq; using System.Threading.Tasks; -using NBitcoin; -using NBitcoin.Crypto; -using NBitcoin.DataEncoders; -using Microsoft.AspNetCore.Http.Internal; using System.IO; using BTCPayServer.Authentication; -using System.Security.Principal; -using NBitpayClient.Extensions; using BTCPayServer.Logging; using Newtonsoft.Json; using BTCPayServer.Models; using BTCPayServer.Configuration; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Http.Extensions; -using BTCPayServer.Controllers; using System.Net.WebSockets; -using System.Security.Claims; -using BTCPayServer.Services; -using NBitpayClient; -using Newtonsoft.Json.Linq; +using BTCPayServer.Services.Stores; namespace BTCPayServer.Hosting { public class BTCPayMiddleware { - TokenRepository _TokenRepository; RequestDelegate _Next; BTCPayServerOptions _Options; public BTCPayMiddleware(RequestDelegate next, - TokenRepository tokenRepo, BTCPayServerOptions options) { - _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo)); _Next = next ?? throw new ArgumentNullException(nameof(next)); _Options = options ?? throw new ArgumentNullException(nameof(options)); } @@ -49,15 +33,15 @@ namespace BTCPayServer.Hosting public async Task Invoke(HttpContext httpContext) { RewriteHostIfNeeded(httpContext); - httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values); - var sig = values.FirstOrDefault(); - httpContext.Request.Headers.TryGetValue("x-identity", out values); - var id = values.FirstOrDefault(); + try { - if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id)) + var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth); + var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth); + httpContext.SetIsBitpayAPI(isBitpayAPI); + if (isBitpayAPI) { - await HandleBitId(httpContext, sig, id); + httpContext.SetBitpayAuth(bitpayAuth); } await _Next(httpContext); } @@ -76,7 +60,57 @@ namespace BTCPayServer.Hosting Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware"); throw; } - } + } + + private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth) + { + httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values); + var sig = values.FirstOrDefault(); + httpContext.Request.Headers.TryGetValue("x-identity", out values); + var id = values.FirstOrDefault(); + httpContext.Request.Headers.TryGetValue("Authorization", out values); + var auth = values.FirstOrDefault(); + hasBitpayAuth = auth != null || (sig != null && id != null); + return (sig, id, auth); + } + + private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth) + { + if (!httpContext.Request.Path.HasValue) + return false; + + var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase); + var path = httpContext.Request.Path.Value; + if ( + bitpayAuth && + path == "/invoices" && + httpContext.Request.Method == "POST" && + isJson) + return true; + + if ( + bitpayAuth && + path == "/invoices" && + httpContext.Request.Method == "GET") + return true; + + if ( + path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) && + httpContext.Request.Method == "GET" && + (isJson || httpContext.Request.Query.ContainsKey("token"))) + return true; + + if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) && + httpContext.Request.Method == "GET") + return true; + + if ( + path.Equals("/tokens", StringComparison.Ordinal) && + ( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST")) + return true; + + return false; + } private void RewriteHostIfNeeded(HttpContext httpContext) { @@ -170,90 +204,5 @@ namespace BTCPayServer.Hosting await writer.FlushAsync(); } } - - - private async Task HandleBitId(HttpContext httpContext, string sig, string id) - { - httpContext.Request.EnableRewind(); - - string body = string.Empty; - if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) - { - using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) - { - body = reader.ReadToEnd(); - } - httpContext.Request.Body.Position = 0; - } - - var url = httpContext.Request.GetEncodedUrl(); - try - { - var key = new PubKey(id); - if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) - { - var sin = key.GetBitIDSIN(); - var identity = ((ClaimsIdentity)httpContext.User.Identity); - identity.AddClaim(new Claim(Claims.SIN, sin)); - - string token = null; - if (httpContext.Request.Query.TryGetValue("token", out var tokenValues)) - { - token = tokenValues[0]; - } - - if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST") - { - try - { - token = JObject.Parse(body)?.Property("token")?.Value?.Value(); - } - catch { } - } - - if (token != null) - { - var bitToken = await GetTokenPermissionAsync(sin, token); - if (bitToken == null) - { - throw new BitpayHttpException(401, $"This endpoint does not support this facade"); - } - identity.AddClaim(new Claim(Claims.OwnStore, bitToken.StoreId)); - } - Logs.PayServer.LogDebug($"BitId signature check success for SIN {sin}"); - } - } - catch (FormatException) { } - if (!httpContext.User.HasClaim(c => c.Type == Claims.SIN)) - Logs.PayServer.LogDebug("BitId signature check failed"); - } - - private async Task GetTokenPermissionAsync(string sin, string expectedToken) - { - var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray(); - actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray(); - - var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal)); - if (expectedToken == null || actualToken == null) - { - Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}"); - return null; - } - return actualToken; - } - - private IEnumerable GetCompatibleTokens(BitTokenEntity token) - { - if (token.Facade == Facade.Merchant.ToString()) - { - yield return token.Clone(Facade.User); - yield return token.Clone(Facade.PointOfSale); - } - if (token.Facade == Facade.PointOfSale.ToString()) - { - yield return token.Clone(Facade.User); - } - yield return token; - } } } diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index f983f7603..b5107aa6f 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -35,7 +35,6 @@ using Hangfire.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Threading; using Microsoft.Extensions.Options; -using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.AspNetCore.Mvc.Cors.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Net; @@ -104,10 +103,6 @@ namespace BTCPayServer.Hosting b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin(); }); }); - services.Configure>(o => - { - o.Value.DeveloperMode = _Env.IsDevelopment(); - }); // Needed to debug U2F for ledger support //services.Configure(kestrel => @@ -146,12 +141,8 @@ namespace BTCPayServer.Hosting if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - app.UseBrowserLink(); } - //App insight do not that by itself... - loggerFactory.AddApplicationInsights(prov, LogLevel.Information); - app.UsePayServer(); app.UseStaticFiles(); app.UseAuthentication(); diff --git a/BTCPayServer/Logging/ConsoleLogger.cs b/BTCPayServer/Logging/ConsoleLogger.cs index ba4e7650b..bb442cdbb 100644 --- a/BTCPayServer/Logging/ConsoleLogger.cs +++ b/BTCPayServer/Logging/ConsoleLogger.cs @@ -1,13 +1,14 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Console; -using Microsoft.Extensions.Logging.Console.Internal; -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions.Internal; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Logging.Console.Internal; namespace BTCPayServer.Logging { @@ -20,19 +21,18 @@ namespace BTCPayServer.Logging } public ILogger CreateLogger(string categoryName) { - return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor); + return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor); } public void Dispose() { - } } /// /// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category /// - public class CustomConsoleLogger : ILogger + public class CustomerConsoleLogger : ILogger { private static readonly string _loglevelPadding = ": "; private static readonly string _messagePadding; @@ -47,19 +47,33 @@ namespace BTCPayServer.Logging [ThreadStatic] private static StringBuilder _logBuilder; - static CustomConsoleLogger() + static CustomerConsoleLogger() { var logLevelString = GetLogLevelString(LogLevel.Information); _messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); _newLineWithMessagePadding = Environment.NewLine + _messagePadding; } - public CustomConsoleLogger(string name, Func filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor) + public CustomerConsoleLogger(string name, Func filter, bool includeScopes) + : this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor()) { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Filter = filter ?? ((category, logLevel) => true); - IncludeScopes = includeScopes; + } + internal CustomerConsoleLogger(string name, Func filter, IExternalScopeProvider scopeProvider) + : this(name, filter, scopeProvider, new ConsoleLoggerProcessor()) + { + } + + internal CustomerConsoleLogger(string name, Func filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + Filter = filter ?? ((category, logLevel) => true); + ScopeProvider = scopeProvider; _queueProcessor = loggerProcessor; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -80,7 +94,12 @@ namespace BTCPayServer.Logging } set { - _queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value)); + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _queueProcessor.Console = value; } } @@ -92,13 +111,13 @@ namespace BTCPayServer.Logging } set { - _filter = value ?? throw new ArgumentNullException(nameof(value)); - } - } + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } - public bool IncludeScopes - { - get; set; + _filter = value; + } } public string Name @@ -106,6 +125,16 @@ namespace BTCPayServer.Logging get; } + internal IExternalScopeProvider ScopeProvider + { + get; set; + } + + public bool DisableColors + { + get; set; + } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (!IsEnabled(logLevel)) @@ -154,10 +183,7 @@ namespace BTCPayServer.Logging while (lenAfter++ < 18) logBuilder.Append(" "); // scope information - if (IncludeScopes) - { - GetScopeInformation(logBuilder); - } + GetScopeInformation(logBuilder); if (!string.IsNullOrEmpty(message)) { @@ -202,18 +228,15 @@ namespace BTCPayServer.Logging public bool IsEnabled(LogLevel logLevel) { + if (logLevel == LogLevel.None) + { + return false; + } + return Filter(Name, logLevel); } - public IDisposable BeginScope(TState state) - { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - return ConsoleLogScope.Push(Name, state); - } + public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance; private static string GetLogLevelString(LogLevel logLevel) { @@ -238,6 +261,11 @@ namespace BTCPayServer.Logging private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel) { + if (DisableColors) + { + return new ConsoleColors(null, null); + } + // We must explicitly set the background color if we are setting the foreground color, // since just setting one can look bad on the users console. switch (logLevel) @@ -259,30 +287,25 @@ namespace BTCPayServer.Logging } } - private void GetScopeInformation(StringBuilder builder) + private void GetScopeInformation(StringBuilder stringBuilder) { - var current = ConsoleLogScope.Current; - string scopeLog = string.Empty; - var length = builder.Length; - - while (current != null) + var scopeProvider = ScopeProvider; + if (scopeProvider != null) { - if (length == builder.Length) - { - scopeLog = $"=> {current}"; - } - else - { - scopeLog = $"=> {current} "; - } + var initialLength = stringBuilder.Length; - builder.Insert(length, scopeLog); - current = current.Parent; - } - if (builder.Length > length) - { - builder.Insert(length, _messagePadding); - builder.AppendLine(); + scopeProvider.ForEachScope((scope, state) => + { + var (builder, length) = state; + var first = length == builder.Length; + builder.Append(first ? "=> " : " => ").Append(scope); + }, (stringBuilder, initialLength)); + + if (stringBuilder.Length > initialLength) + { + stringBuilder.Insert(initialLength, _messagePadding); + stringBuilder.AppendLine(); + } } } @@ -333,9 +356,9 @@ namespace BTCPayServer.Logging // Start Console message queue processor _outputTask = Task.Factory.StartNew( ProcessLogQueue, - this, - default(CancellationToken), - TaskCreationOptions.LongRunning, TaskScheduler.Default); + state: this, + cancellationToken: default(CancellationToken), + creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default); } public virtual void EnqueueMessage(LogMessageEntry message) diff --git a/BTCPayServer/Migrations/20170913143004_Init.cs b/BTCPayServer/Migrations/20170913143004_Init.cs index 7d58bd4b1..149900b1f 100644 --- a/BTCPayServer/Migrations/20170913143004_Init.cs +++ b/BTCPayServer/Migrations/20170913143004_Init.cs @@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations name: "AspNetRoles", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true) + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true) }, constraints: table => { @@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations name: "AspNetUsers", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - AccessFailedCount = table.Column(type: "INTEGER", nullable: false), - ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), - Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(maxLength: 256, nullable: true), EmailConfirmed = table.Column(nullable: false), LockoutEnabled = table.Column(nullable: false), LockoutEnd = table.Column(nullable: true), - NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), - PasswordHash = table.Column(type: "TEXT", nullable: true), - PhoneNumber = table.Column(type: "TEXT", nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), PhoneNumberConfirmed = table.Column(nullable: false), - SecurityStamp = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(nullable: true), TwoFactorEnabled = table.Column(nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true) + UserName = table.Column(maxLength: 256, nullable: true) }, constraints: table => { @@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations name: "Stores", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - DerivationStrategy = table.Column(type: "TEXT", nullable: true), - SpeedPolicy = table.Column(type: "INTEGER", nullable: false), + Id = table.Column(nullable: false), + DerivationStrategy = table.Column(nullable: true), + SpeedPolicy = table.Column(nullable: false), StoreCertificate = table.Column(nullable: true), - StoreName = table.Column(type: "TEXT", nullable: true), - StoreWebsite = table.Column(type: "TEXT", nullable: true) + StoreName = table.Column(nullable: true), + StoreWebsite = table.Column(nullable: true) }, constraints: table => { @@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations name: "AspNetRoleClaims", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(nullable: false) .Annotation("Sqlite:Autoincrement", true), - ClaimType = table.Column(type: "TEXT", nullable: true), - ClaimValue = table.Column(type: "TEXT", nullable: true), - RoleId = table.Column(type: "TEXT", nullable: false) + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: false) }, constraints: table => { @@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations name: "AspNetUserClaims", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(nullable: false) .Annotation("Sqlite:Autoincrement", true), - ClaimType = table.Column(type: "TEXT", nullable: true), - ClaimValue = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "TEXT", nullable: false) + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: false) }, constraints: table => { @@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations name: "AspNetUserLogins", columns: table => new { - LoginProvider = table.Column(type: "TEXT", nullable: false), - ProviderKey = table.Column(type: "TEXT", nullable: false), - ProviderDisplayName = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "TEXT", nullable: false) + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) }, constraints: table => { @@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations name: "AspNetUserRoles", columns: table => new { - UserId = table.Column(type: "TEXT", nullable: false), - RoleId = table.Column(type: "TEXT", nullable: false) + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) }, constraints: table => { @@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations name: "AspNetUserTokens", columns: table => new { - UserId = table.Column(type: "TEXT", nullable: false), - LoginProvider = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: false), - Value = table.Column(type: "TEXT", nullable: true) + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) }, constraints: table => { @@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations name: "Invoices", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), Blob = table.Column(nullable: true), Created = table.Column(nullable: false), - CustomerEmail = table.Column(type: "TEXT", nullable: true), - ExceptionStatus = table.Column(type: "TEXT", nullable: true), - ItemCode = table.Column(type: "TEXT", nullable: true), - OrderId = table.Column(type: "TEXT", nullable: true), - Status = table.Column(type: "TEXT", nullable: true), - StoreDataId = table.Column(type: "TEXT", nullable: true) + CustomerEmail = table.Column(nullable: true), + ExceptionStatus = table.Column(nullable: true), + ItemCode = table.Column(nullable: true), + OrderId = table.Column(nullable: true), + Status = table.Column(nullable: true), + StoreDataId = table.Column(nullable: true) }, constraints: table => { @@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations name: "UserStore", columns: table => new { - ApplicationUserId = table.Column(type: "TEXT", nullable: false), - StoreDataId = table.Column(type: "TEXT", nullable: false), - Role = table.Column(type: "TEXT", nullable: true) + ApplicationUserId = table.Column(nullable: false), + StoreDataId = table.Column(nullable: false), + Role = table.Column(nullable: true) }, constraints: table => { @@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations name: "Payments", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), Blob = table.Column(nullable: true), - InvoiceDataId = table.Column(type: "TEXT", nullable: true) + InvoiceDataId = table.Column(nullable: true) }, constraints: table => { @@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations name: "RefundAddresses", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), Blob = table.Column(nullable: true), - InvoiceDataId = table.Column(type: "TEXT", nullable: true) + InvoiceDataId = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20170926073744_Settings.cs b/BTCPayServer/Migrations/20170926073744_Settings.cs index 60d8f3fde..efca94c61 100644 --- a/BTCPayServer/Migrations/20170926073744_Settings.cs +++ b/BTCPayServer/Migrations/20170926073744_Settings.cs @@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations name: "Settings", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - Value = table.Column(type: "TEXT", nullable: true) + Id = table.Column(nullable: false), + Value = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171006013443_AddressMapping.cs b/BTCPayServer/Migrations/20171006013443_AddressMapping.cs index aba4aa2ed..18a312349 100644 --- a/BTCPayServer/Migrations/20171006013443_AddressMapping.cs +++ b/BTCPayServer/Migrations/20171006013443_AddressMapping.cs @@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations name: "AddressInvoices", columns: table => new { - Address = table.Column(type: "TEXT", nullable: false), - InvoiceDataId = table.Column(type: "TEXT", nullable: true) + Address = table.Column(nullable: false), + InvoiceDataId = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171010082424_Tokens.cs b/BTCPayServer/Migrations/20171010082424_Tokens.cs index dc2669da3..9847f8a15 100644 --- a/BTCPayServer/Migrations/20171010082424_Tokens.cs +++ b/BTCPayServer/Migrations/20171010082424_Tokens.cs @@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations name: "PairedSINData", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), - Facade = table.Column(type: "TEXT", nullable: true), - Label = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", nullable: true), + Id = table.Column(nullable: false), + Facade = table.Column(nullable: true), + Label = table.Column(nullable: true), + Name = table.Column(nullable: true), PairingTime = table.Column(nullable: false), - SIN = table.Column(type: "TEXT", nullable: true), - StoreDataId = table.Column(type: "TEXT", nullable: true) + SIN = table.Column(nullable: true), + StoreDataId = table.Column(nullable: true) }, constraints: table => { @@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations name: "PairingCodes", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false), + Id = table.Column(nullable: false), DateCreated = table.Column(nullable: false), Expiration = table.Column(nullable: false), - Facade = table.Column(type: "TEXT", nullable: true), - Label = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", nullable: true), - SIN = table.Column(type: "TEXT", nullable: true), - StoreDataId = table.Column(type: "TEXT", nullable: true), - TokenValue = table.Column(type: "TEXT", nullable: true) + Facade = table.Column(nullable: true), + Label = table.Column(nullable: true), + Name = table.Column(nullable: true), + SIN = table.Column(nullable: true), + StoreDataId = table.Column(nullable: true), + TokenValue = table.Column(nullable: true) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs b/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs index 5ac527ac3..1711c54e6 100644 --- a/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs +++ b/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations name: "PendingInvoices", columns: table => new { - Id = table.Column(type: "TEXT", nullable: false) + Id = table.Column(nullable: false) }, constraints: table => { diff --git a/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs b/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs index d852c5224..83b785dc4 100644 --- a/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs +++ b/BTCPayServer/Migrations/20171024163354_RenewUsedAddresses.cs @@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations name: "HistoricalAddressInvoices", columns: table => new { - InvoiceDataId = table.Column(type: "TEXT", nullable: false), - Address = table.Column(type: "TEXT", nullable: false), + InvoiceDataId = table.Column(nullable: false), + Address = table.Column(nullable: false), Assigned = table.Column(nullable: false), UnAssigned = table.Column(nullable: true) }, diff --git a/BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs b/BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs new file mode 100644 index 000000000..b478184d6 --- /dev/null +++ b/BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs @@ -0,0 +1,553 @@ +// +using BTCPayServer.Data; +using BTCPayServer.Services.Invoices; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20180429083930_legacyapikey")] + partial class legacyapikey + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("CreatedTime"); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(50); + + b.Property("StoreId") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AppType"); + + b.Property("Created"); + + b.Property("Name"); + + b.Property("Settings"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId"); + + b.Property("Address"); + + b.Property("Assigned"); + + b.Property("CryptoCode"); + + b.Property("UnAssigned"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId"); + + b.Property("UniqueId"); + + b.Property("Message"); + + b.Property("Timestamp"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Accounted"); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DefaultCrypto"); + + b.Property("DerivationStrategies"); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreBlob"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Apps") + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany() + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData") + .WithMany("Events") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20180429083930_legacyapikey.cs b/BTCPayServer/Migrations/20180429083930_legacyapikey.cs new file mode 100644 index 000000000..0a1d21bb5 --- /dev/null +++ b/BTCPayServer/Migrations/20180429083930_legacyapikey.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Migrations +{ + public partial class legacyapikey : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeys", + columns: table => new + { + Id = table.Column(maxLength: 50, nullable: false), + StoreId = table.Column(maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_StoreId", + table: "ApiKeys", + column: "StoreId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys"); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 196e557f9..c31f3ff53 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => { @@ -36,6 +36,22 @@ namespace BTCPayServer.Migrations b.ToTable("AddressInvoices"); }); + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(50); + + b.Property("StoreId") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.ToTable("ApiKeys"); + }); + modelBuilder.Entity("BTCPayServer.Data.AppData", b => { b.Property("Id") diff --git a/BTCPayServer/Models/ConfirmModel.cs b/BTCPayServer/Models/ConfirmModel.cs index 172db1eb5..a882e9ef8 100644 --- a/BTCPayServer/Models/ConfirmModel.cs +++ b/BTCPayServer/Models/ConfirmModel.cs @@ -19,5 +19,6 @@ namespace BTCPayServer.Models { get; set; } + public string ButtonClass { get; set; } = "btn-danger"; } } diff --git a/BTCPayServer/Models/ErrorViewModel.cs b/BTCPayServer/Models/ErrorViewModel.cs deleted file mode 100644 index b32ee4e43..000000000 --- a/BTCPayServer/Models/ErrorViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace BTCPayServer.Models -{ - public class ErrorViewModel - { - public string RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} \ No newline at end of file diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index 4ff6f21ed..c7ad99dff 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -79,7 +79,7 @@ namespace BTCPayServer.Models //"price":5 [JsonProperty("price")] - public double Price + public decimal Price { get; set; } @@ -94,7 +94,7 @@ namespace BTCPayServer.Models //"exRates":{"USD":4320.02} [JsonProperty("exRates")] [Obsolete("Use CryptoInfo.ExRates instead")] - public Dictionary ExRates + public Dictionary ExRates { get; set; } @@ -224,6 +224,29 @@ namespace BTCPayServer.Models { get; set; } + + [JsonProperty("paymentSubtotals")] + public Dictionary PaymentSubtotals { get; set; } + + [JsonProperty("paymentTotals")] + public Dictionary PaymentTotals { get; set; } + + [JsonProperty("amountPaid")] + public long AmountPaid { get; set; } + + [JsonProperty("minerFees")] + public long MinerFees { get; set; } + + [JsonProperty("exchangeRates")] + public Dictionary> ExchangeRates{ get; set; } + + [JsonProperty("supportedTransactionCurrencies")] + public Dictionary SupportedTransactionCurrencies { get; set; } + + [JsonProperty("addresses")] + public Dictionary Addresses { get; set; } + [JsonProperty("paymentCodes")] + public Dictionary PaymentCodes{get; set;} } public class Flags { @@ -233,4 +256,5 @@ namespace BTCPayServer.Models get; set; } } + } diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index 39b77c8dc..631303ae0 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Validation; namespace BTCPayServer.Models.InvoicingModels { @@ -14,7 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels Currency = "USD"; } [Required] - public double? Amount + public decimal? Amount { get; set; } @@ -52,8 +53,7 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } - - [Url] + [Uri] public string NotificationUrl { get; set; diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 36b270c2b..32717a939 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -18,6 +18,7 @@ namespace BTCPayServer.Models.InvoicingModels public string Address { get; internal set; } public string Rate { get; internal set; } public string PaymentUrl { get; internal set; } + public string Overpaid { get; set; } } public class AddressModel { diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 594e28c40..63385532d 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -49,6 +49,8 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } + public bool ShowCheckout { get; set; } + public string ExceptionStatus { get; set; } public string AmountCurrency { get; set; diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 9cd26cbf1..da28a8975 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -13,6 +13,7 @@ namespace BTCPayServer.Models.InvoicingModels public string CryptoImage { get; set; } public string Link { get; set; } } + public string HtmlTitle { get; set; } public string CustomCSSLink { get; set; } public string CustomLogoLink { get; set; } public string DefaultLang { get; set; } @@ -36,6 +37,7 @@ namespace BTCPayServer.Models.InvoicingModels public string TimeLeft { get; set; } public string Rate { get; set; } public string OrderAmount { get; set; } + public string OrderAmountFiat { get; set; } public string InvoiceBitcoinUrl { get; set; } public string InvoiceBitcoinUrlQR { get; set; } public int TxCount { get; set; } diff --git a/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs b/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs index 47307dfc2..11c983beb 100644 --- a/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs @@ -18,8 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels { get; set; } - - [Required] + [EmailAddress] public string TestEmail { diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs index 758350d49..ae5176ffd 100644 --- a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Services; +using BTCPayServer.Validation; using Microsoft.AspNetCore.Mvc.Rendering; namespace BTCPayServer.Models.StoreViewModels @@ -42,12 +43,16 @@ namespace BTCPayServer.Models.StoreViewModels public string OnChainMinValue { get; set; } [Display(Name = "Link to a custom CSS stylesheet")] - [Url] + [Uri] public string CustomCSS { get; set; } [Display(Name = "Link to a custom logo")] - [Url] + [Uri] public string CustomLogo { get; set; } + [Display(Name = "Custom HTML title to display on Checkout page")] + public string HtmlTitle { get; set; } + + public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto) { var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray(); diff --git a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs new file mode 100644 index 000000000..a6e0c1346 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class RatesViewModel + { + public class TestResultViewModel + { + public string CurrencyPair { get; set; } + public string Rule { get; set; } + public bool Error { get; set; } + } + class Format + { + public string Name { get; set; } + public string Value { get; set; } + } + public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange) + { + var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName; + var choices = supportedList.Select(o => new Format() { Name = o.Display, 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 List TestRateRules { get; set; } + + public SelectList Exchanges { get; set; } + + public bool ShowScripting { get; set; } + + [Display(Name = "Rate rules")] + [MaxLength(2000)] + public string Script { get; set; } + public string DefaultScript { get; set; } + public string ScriptTest { get; set; } + public CoinAverageExchange[] AvailableExchanges { get; set; } + + [Display(Name = "Multiply the rate by ...")] + [Range(0.01, 10.0)] + public double RateMultiplier + { + get; + set; + } + + [Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")] + public string PreferredExchange { get; set; } + + public string RateSource + { + get + { + return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}"; + } + } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index 1ed1dce96..1b86a07c2 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -1,5 +1,7 @@ using BTCPayServer.Services; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; +using BTCPayServer.Validation; using BTCPayServer.Validations; using Microsoft.AspNetCore.Mvc.Rendering; using System; @@ -12,11 +14,6 @@ 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; } @@ -38,7 +35,7 @@ namespace BTCPayServer.Models.StoreViewModels get; set; } - [Url] + [Uri] [Display(Name = "Store Website")] [MaxLength(500)] public string StoreWebsite @@ -49,36 +46,6 @@ 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; } - - public string RateSource - { - get - { - return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}"; - } - } - - [Display(Name = "Multiply the original rate by ...")] - [Range(0.01, 10.0)] - public double RateMultiplier - { - get; - set; - } - [Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")] [Range(1, 60 * 24 * 24)] public int InvoiceExpiration @@ -119,5 +86,13 @@ namespace BTCPayServer.Models.StoreViewModels { get; set; } = new List(); + + [Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")] + [Range(0, 100)] + public double PaymentTolerance + { + get; + set; + } } } diff --git a/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs b/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs index 694b5d64d..84e9791b5 100644 --- a/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs @@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels get; set; } + + [Display(Name = "API Key")] + public string ApiKey { get; set; } + public string EncodedApiKey { get; set; } } } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs index c0d29584a..9026aaf13 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs @@ -68,6 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin { return ConfirmationCount >= 1; } + else if (speedPolicy == SpeedPolicy.LowMediumSpeed) + { + return ConfirmationCount >= 2; + } else if (speedPolicy == SpeedPolicy.LowSpeed) { return ConfirmationCount >= 6; diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index d3f2856d5..4b0e6129d 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -28,7 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin { EventAggregator _Aggregator; ExplorerClientProvider _ExplorerClients; - IApplicationLifetime _Lifetime; + Microsoft.Extensions.Hosting.IApplicationLifetime _Lifetime; InvoiceRepository _InvoiceRepository; private TaskCompletionSource _RunningTask; private CancellationTokenSource _Cts; @@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin BTCPayWalletProvider wallets, InvoiceRepository invoiceRepository, BTCPayNetworkProvider networkProvider, - EventAggregator aggregator, IApplicationLifetime lifetime) + EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime) { PollInterval = TimeSpan.FromMinutes(1.0); _Wallets = wallets; 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/Program.cs b/BTCPayServer/Program.cs index c284c41a0..aed3bf2d9 100644 --- a/BTCPayServer/Program.cs +++ b/BTCPayServer/Program.cs @@ -40,7 +40,6 @@ namespace BTCPayServer .UseIISIntegration() .UseContentRoot(Directory.GetCurrentDirectory()) .UseConfiguration(conf) - .UseApplicationInsights() .ConfigureLogging(l => { l.AddFilter("Microsoft", LogLevel.Error); diff --git a/BTCPayServer/Rating/CurrencyPair.cs b/BTCPayServer/Rating/CurrencyPair.cs new file mode 100644 index 000000000..adeacd603 --- /dev/null +++ b/BTCPayServer/Rating/CurrencyPair.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Rating +{ + public class CurrencyPair + { + static readonly BTCPayNetworkProvider _NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet); + public CurrencyPair(string left, string right) + { + if (right == null) + throw new ArgumentNullException(nameof(right)); + if (left == null) + throw new ArgumentNullException(nameof(left)); + Right = right.ToUpperInvariant(); + Left = left.ToUpperInvariant(); + } + public string Left { get; private set; } + public string Right { get; private set; } + + public static CurrencyPair Parse(string str) + { + if (!TryParse(str, out var result)) + throw new FormatException("Invalid currency pair"); + return result; + } + public static bool TryParse(string str, out CurrencyPair value) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + value = null; + str = str.Trim(); + if (str.Length > 12) + return false; + var splitted = str.Split(new[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length == 2) + { + value = new CurrencyPair(splitted[0], splitted[1]); + return true; + } + else if (splitted.Length == 1) + { + var currencyPair = splitted[0]; + if (currencyPair.Length < 6 || currencyPair.Length > 10) + return false; + if (currencyPair.Length == 6) + { + value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3)); + return true; + } + for (int i = 3; i < 5; i++) + { + var potentialCryptoName = currencyPair.Substring(0, i); + var network = _NetworkProvider.GetNetwork(potentialCryptoName); + if (network != null) + { + value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i)); + return true; + } + } + } + + return false; + } + + + public override bool Equals(object obj) + { + CurrencyPair item = obj as CurrencyPair; + if (item == null) + return false; + return ToString().Equals(item.ToString(), StringComparison.OrdinalIgnoreCase); + } + public static bool operator ==(CurrencyPair a, CurrencyPair b) + { + if (System.Object.ReferenceEquals(a, b)) + return true; + if (((object)a == null) || ((object)b == null)) + return false; + return a.ToString() == b.ToString(); + } + + public static bool operator !=(CurrencyPair a, CurrencyPair b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(StringComparison.OrdinalIgnoreCase); + } + public override string ToString() + { + return $"{Left}_{Right}"; + } + + public CurrencyPair Inverse() + { + return new CurrencyPair(Right, Left); + } + } +} diff --git a/BTCPayServer/Rating/ExchangeRates.cs b/BTCPayServer/Rating/ExchangeRates.cs new file mode 100644 index 000000000..868c70a3b --- /dev/null +++ b/BTCPayServer/Rating/ExchangeRates.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Rating +{ + public class ExchangeRates : IEnumerable + { + Dictionary _AllRates = new Dictionary(); + public ExchangeRates() + { + + } + public ExchangeRates(IEnumerable rates) + { + foreach (var rate in rates) + { + Add(rate); + } + } + List _Rates = new List(); + public MultiValueDictionary ByExchange + { + get; + private set; + } = new MultiValueDictionary(); + + public void Add(ExchangeRate rate) + { + // 1 DOGE is always 1 DOGE + if (rate.CurrencyPair.Left == rate.CurrencyPair.Right) + return; + var key = $"({rate.Exchange}) {rate.CurrencyPair}"; + if (_AllRates.TryAdd(key, rate)) + { + _Rates.Add(rate); + ByExchange.Add(rate.Exchange, rate); + } + else + { + if (rate.Value.HasValue) + { + _AllRates[key].Value = rate.Value; + } + } + } + + public IEnumerator GetEnumerator() + { + return _Rates.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value) + { + if (ByExchange.TryGetValue(exchangeName, out var rates)) + { + var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair); + if (rate != null) + rate.Value = value; + } + } + public decimal? GetRate(string exchangeName, CurrencyPair currencyPair) + { + if (currencyPair.Left == currencyPair.Right) + return 1.0m; + if (ByExchange.TryGetValue(exchangeName, out var rates)) + { + var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair); + if (rate != null) + return rate.Value; + } + return null; + } + } + public class ExchangeRate + { + public string Exchange { get; set; } + public CurrencyPair CurrencyPair { get; set; } + public decimal? Value { get; set; } + + public override string ToString() + { + if (Value == null) + return $"{Exchange}({CurrencyPair})"; + return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}"; + } + } +} diff --git a/BTCPayServer/Rating/RateRules.cs b/BTCPayServer/Rating/RateRules.cs new file mode 100644 index 000000000..e7fe02e0d --- /dev/null +++ b/BTCPayServer/Rating/RateRules.cs @@ -0,0 +1,538 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace BTCPayServer.Rating +{ + public enum RateRulesErrors + { + Ok, + TooMuchNestedCalls, + InvalidCurrencyIdentifier, + NestedInvocation, + UnsupportedOperator, + MissingArgument, + DivideByZero, + PreprocessError, + RateUnavailable, + InvalidExchangeName, + } + public class RateRules + { + class NormalizeCurrencyPairsRewritter : CSharpSyntaxRewriter + { + public List Errors = new List(); + + bool IsInvocation; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + if (IsInvocation) + { + Errors.Add(RateRulesErrors.NestedInvocation); + return base.VisitInvocationExpression(node); + } + if (node.Expression is IdentifierNameSyntax id) + { + IsInvocation = true; + var arglist = (ArgumentListSyntax)this.Visit(node.ArgumentList); + IsInvocation = false; + return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(id.Identifier.ValueText.ToLowerInvariant()), arglist) + .WithTriviaFrom(id); + } + else + { + Errors.Add(RateRulesErrors.InvalidExchangeName); + return node; + } + } + public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) + { + if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currencyPair)) + { + return SyntaxFactory.IdentifierName(currencyPair.ToString()) + .WithTriviaFrom(node); + } + else + { + Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier); + return base.VisitIdentifierName(node); + } + } + } + class RuleList : CSharpSyntaxWalker + { + public Dictionary ExpressionsByPair = new Dictionary(); + public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) + { + if (node.Kind() == SyntaxKind.SimpleAssignmentExpression + && node.Left is IdentifierNameSyntax id + && node.Right is ExpressionSyntax expression) + { + if (CurrencyPair.TryParse(id.Identifier.ValueText, out var currencyPair)) + { + expression = expression.WithTriviaFrom(expression); + ExpressionsByPair.Add(currencyPair, (expression, id)); + } + } + base.VisitAssignmentExpression(node); + } + + public SyntaxNode GetSyntaxNode() + { + return SyntaxFactory.Block( + ExpressionsByPair.Select(e => + SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(e.Key.ToString()).WithTriviaFrom(e.Value.Trivia), + e.Value.Expression) + )) + ); + } + } + + SyntaxNode root; + RuleList ruleList; + + public decimal GlobalMultiplier { get; set; } = 1.0m; + + RateRules(SyntaxNode root) + { + ruleList = new RuleList(); + ruleList.Visit(root); + // Remove every irrelevant statements + this.root = ruleList.GetSyntaxNode(); + } + public static bool TryParse(string str, out RateRules rules) + { + return TryParse(str, out rules, out var unused); + } + public static bool TryParse(string str, out RateRules rules, out List errors) + { + rules = null; + errors = null; + var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script)); + var rewriter = new NormalizeCurrencyPairsRewritter(); + // Rename BTC_usd to BTC_USD and verify structure + var root = rewriter.Visit(expression.GetRoot()); + if (rewriter.Errors.Count > 0) + { + errors = rewriter.Errors; + return false; + } + rules = new RateRules(root); + return true; + } + + public RateRule GetRuleFor(CurrencyPair currencyPair) + { + if (currencyPair.Left == "X" || currencyPair.Right == "X") + throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency"); + var candidate = FindBestCandidate(currencyPair); + if (GlobalMultiplier != decimal.One) + { + candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}"); + } + return new RateRule(this, currencyPair, candidate); + } + + public ExpressionSyntax FindBestCandidate(CurrencyPair p) + { + var invP = p.Inverse(); + var candidates = new List<(CurrencyPair Pair, int Prioriy, ExpressionSyntax Expression, bool Inverse)>(); + foreach (var pair in new[] + { + (Pair: p, Priority: 0, Inverse: false), + (Pair: new CurrencyPair(p.Left, "X"), Priority: 1, Inverse: false), + (Pair: new CurrencyPair("X", p.Right), Priority: 1, Inverse: false), + (Pair: invP, Priority: 2, Inverse: true), + (Pair: new CurrencyPair(invP.Left, "X"), Priority: 3, Inverse: true), + (Pair: new CurrencyPair("X", invP.Right), Priority: 3, Inverse: true), + (Pair: new CurrencyPair("X", "X"), Priority: 4, Inverse: false) + }) + { + if (ruleList.ExpressionsByPair.TryGetValue(pair.Pair, out var expression)) + { + candidates.Add((pair.Pair, pair.Priority, expression.Expression, pair.Inverse)); + } + } + if (candidates.Count == 0) + return CreateExpression($"ERR_NO_RULE_MATCH({p})"); + var best = candidates + .OrderBy(c => c.Prioriy) + .ThenBy(c => c.Expression.Span.Start) + .First(); + return best.Inverse + ? CreateExpression($"1 / {invP}") + : best.Expression; + } + + internal static ExpressionSyntax CreateExpression(string str) + { + return (ExpressionSyntax)CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script)).GetRoot().ChildNodes().First().ChildNodes().First().ChildNodes().First(); + } + + public override string ToString() + { + return root.NormalizeWhitespace("", "\n") + .ToFullString() + .Replace("{\n", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("\n}", string.Empty, StringComparison.OrdinalIgnoreCase); + } + } + + public class RateRule + { + class ReplaceExchangeRateRewriter : CSharpSyntaxRewriter + { + public List Errors = new List(); + public ExchangeRates Rates; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + var exchangeName = node.Expression.ToString(); + if (exchangeName.StartsWith("ERR_", StringComparison.OrdinalIgnoreCase)) + { + Errors.Add(RateRulesErrors.PreprocessError); + return base.VisitInvocationExpression(node); + } + + var currencyPair = node.ArgumentList.ChildNodes().FirstOrDefault()?.ToString(); + if (currencyPair == null || !CurrencyPair.TryParse(currencyPair, out var pair)) + { + Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier); + return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})"); + } + else + { + var rate = Rates.GetRate(exchangeName, pair); + if (rate == null) + { + Errors.Add(RateRulesErrors.RateUnavailable); + return RateRules.CreateExpression($"ERR_RATE_UNAVAILABLE({exchangeName}, {pair.ToString()})"); + } + else + { + var token = SyntaxFactory.ParseToken(rate.Value.ToString(CultureInfo.InvariantCulture)); + return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, token); + } + } + } + } + + class CalculateWalker : CSharpSyntaxWalker + { + public Stack Values = new Stack(); + public List Errors = new List(); + + public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node) + { + base.VisitPrefixUnaryExpression(node); + bool invalid = false; + switch (node.Kind()) + { + case SyntaxKind.UnaryMinusExpression: + case SyntaxKind.UnaryPlusExpression: + if (Values.Count < 1) + { + invalid = true; + Errors.Add(RateRulesErrors.MissingArgument); + } + break; + default: + invalid = true; + Errors.Add(RateRulesErrors.UnsupportedOperator); + break; + } + + if (invalid) + return; + + switch (node.Kind()) + { + case SyntaxKind.UnaryMinusExpression: + Values.Push(-Values.Pop()); + break; + case SyntaxKind.UnaryPlusExpression: + Values.Push(+Values.Pop()); + break; + default: + throw new NotSupportedException("Should never happen"); + } + } + + public override void VisitBinaryExpression(BinaryExpressionSyntax node) + { + base.VisitBinaryExpression(node); + + + bool invalid = false; + switch (node.Kind()) + { + case SyntaxKind.AddExpression: + case SyntaxKind.MultiplyExpression: + case SyntaxKind.DivideExpression: + case SyntaxKind.SubtractExpression: + if (Values.Count < 2) + { + invalid = true; + Errors.Add(RateRulesErrors.MissingArgument); + } + break; + } + + if (invalid) + return; + + var b = Values.Pop(); + var a = Values.Pop(); + + switch (node.Kind()) + { + case SyntaxKind.AddExpression: + Values.Push(a + b); + break; + case SyntaxKind.MultiplyExpression: + Values.Push(a * b); + break; + case SyntaxKind.DivideExpression: + if (b == decimal.Zero) + { + Errors.Add(RateRulesErrors.DivideByZero); + } + else + { + Values.Push(a / b); + } + break; + case SyntaxKind.SubtractExpression: + Values.Push(a - b); + break; + default: + throw new NotSupportedException("Should never happen"); + } + } + + public override void VisitLiteralExpression(LiteralExpressionSyntax node) + { + switch (node.Kind()) + { + case SyntaxKind.NumericLiteralExpression: + Values.Push(decimal.Parse(node.ToString(), CultureInfo.InvariantCulture)); + break; + } + } + } + + class HasBinaryOperations : CSharpSyntaxWalker + { + public bool Result = false; + public override void VisitBinaryExpression(BinaryExpressionSyntax node) + { + base.VisitBinaryExpression(node); + switch (node.Kind()) + { + case SyntaxKind.AddExpression: + case SyntaxKind.MultiplyExpression: + case SyntaxKind.DivideExpression: + case SyntaxKind.MinusToken: + Result = true; + break; + } + } + } + class FlattenExpressionRewriter : CSharpSyntaxRewriter + { + RateRules parent; + CurrencyPair pair; + int nested = 0; + public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair) + { + this.pair = pair; + this.parent = parent; + } + + public ExchangeRates ExchangeRates = new ExchangeRates(); + bool IsInvocation; + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + if (IsInvocation) + { + Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier); + return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})"); + } + IsInvocation = true; + _ExchangeName = node.Expression.ToString(); + var result = base.VisitInvocationExpression(node); + IsInvocation = false; + return result; + } + + bool IsArgumentList; + public override SyntaxNode VisitArgumentList(ArgumentListSyntax node) + { + IsArgumentList = true; + var result = base.VisitArgumentList(node); + IsArgumentList = false; + return result; + } + + string _ExchangeName = null; + + public List Errors = new List(); + const int MaxNestedCount = 8; + public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) + { + if ( + (!IsInvocation || IsArgumentList) && + CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair)) + { + var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? pair.Left : currentPair.Left, + right: currentPair.Right == "X" ? pair.Right : currentPair.Right); + if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD) + { + ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName }); + return SyntaxFactory.IdentifierName(replacedPair.ToString()); + } + else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD + { + var bestCandidate = parent.FindBestCandidate(replacedPair); + if (nested > MaxNestedCount) + { + Errors.Add(RateRulesErrors.TooMuchNestedCalls); + return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})"); + } + var innerFlatten = CreateNewContext(replacedPair); + var replaced = innerFlatten.Visit(bestCandidate); + if (replaced is ExpressionSyntax expression) + { + var hasBinaryOps = new HasBinaryOperations(); + hasBinaryOps.Visit(expression); + if (hasBinaryOps.Result) + { + replaced = SyntaxFactory.ParenthesizedExpression(expression); + } + } + if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls)) + { + return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})"); + } + return replaced; + } + } + return base.VisitIdentifierName(node); + } + + private FlattenExpressionRewriter CreateNewContext(CurrencyPair pair) + { + return new FlattenExpressionRewriter(parent, pair) + { + Errors = Errors, + nested = nested + 1, + ExchangeRates = ExchangeRates, + }; + } + } + private SyntaxNode expression; + FlattenExpressionRewriter flatten; + + public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate) + { + _CurrencyPair = currencyPair; + flatten = new FlattenExpressionRewriter(parent, currencyPair); + this.expression = flatten.Visit(candidate); + } + + + private readonly CurrencyPair _CurrencyPair; + public CurrencyPair CurrencyPair + { + get + { + return _CurrencyPair; + } + } + + public ExchangeRates ExchangeRates + { + get + { + return flatten.ExchangeRates; + } + } + + + public bool Reevaluate() + { + _Value = null; + _EvaluatedNode = null; + _Evaluated = null; + Errors.Clear(); + + var rewriter = new ReplaceExchangeRateRewriter(); + rewriter.Rates = ExchangeRates; + var result = rewriter.Visit(this.expression); + Errors.AddRange(rewriter.Errors); + _Evaluated = result.NormalizeWhitespace("", "\n").ToString(); + if (HasError) + return false; + + var calculate = new CalculateWalker(); + calculate.Visit(result); + if (calculate.Values.Count != 1 || calculate.Errors.Count != 0) + { + Errors.AddRange(calculate.Errors); + return false; + } + _Value = calculate.Values.Pop(); + _EvaluatedNode = result; + return true; + } + + + private readonly HashSet _Errors = new HashSet(); + public HashSet Errors + { + get + { + return _Errors; + } + } + + SyntaxNode _EvaluatedNode; + string _Evaluated; + public bool HasError + { + get + { + return _Errors.Count != 0; + } + } + + public string ToString(bool evaluated) + { + if (!evaluated) + return ToString(); + if (_Evaluated == null) + return "Call Evaluate() first"; + return _Evaluated; + } + + public override string ToString() + { + return expression.NormalizeWhitespace("", "\n").ToString(); + } + + decimal? _Value; + public decimal? Value + { + get + { + return _Value; + } + } + } +} diff --git a/BTCPayServer/Security/BTCPayClaimsFilter.cs b/BTCPayServer/Security/BTCPayClaimsFilter.cs new file mode 100644 index 000000000..5ab53ef12 --- /dev/null +++ b/BTCPayServer/Security/BTCPayClaimsFilter.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using BTCPayServer.Models; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Options; + +namespace BTCPayServer.Security +{ + + public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions + { + UserManager _UserManager; + StoreRepository _StoreRepository; + public BTCPayClaimsFilter( + UserManager userManager, + StoreRepository storeRepository) + { + _UserManager = userManager; + _StoreRepository = storeRepository; + } + + void IConfigureOptions.Configure(MvcOptions options) + { + options.Filters.Add(typeof(BTCPayClaimsFilter)); + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var principal = context.HttpContext.User; + if (!context.HttpContext.GetIsBitpayAPI()) + { + var identity = ((ClaimsIdentity)principal.Identity); + if (principal.IsInRole(Roles.ServerAdmin)) + { + identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true")); + } + if (context.RouteData.Values.TryGetValue("storeId", out var storeId)) + { + var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + if (claim != null) + { + var store = await _StoreRepository.FindStore((string)storeId, claim.Value); + if (store == null) + context.Result = new ChallengeResult(Policies.CookieAuthentication); + else + { + context.HttpContext.SetStoreData(store); + if (store != null) + { + identity.AddClaims(store.GetClaims()); + } + } + } + } + } + } + } +} diff --git a/BTCPayServer/Security/BitpayClaimsFilter.cs b/BTCPayServer/Security/BitpayClaimsFilter.cs new file mode 100644 index 000000000..463ab57cf --- /dev/null +++ b/BTCPayServer/Security/BitpayClaimsFilter.cs @@ -0,0 +1,196 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Http.Extensions; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Authentication; +using BTCPayServer.Models; +using BTCPayServer.Services; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Options; +using NBitcoin; +using NBitcoin.DataEncoders; +using NBitpayClient; +using NBitpayClient.Extensions; +using Newtonsoft.Json.Linq; +using BTCPayServer.Logging; +using Microsoft.AspNetCore.Http.Internal; + +namespace BTCPayServer.Security +{ + public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions + { + UserManager _UserManager; + StoreRepository _StoreRepository; + TokenRepository _TokenRepository; + + public BitpayClaimsFilter( + UserManager userManager, + TokenRepository tokenRepository, + StoreRepository storeRepository) + { + _UserManager = userManager; + _StoreRepository = storeRepository; + _TokenRepository = tokenRepository; + } + + void IConfigureOptions.Configure(MvcOptions options) + { + options.Filters.Add(typeof(BitpayClaimsFilter)); + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var principal = context.HttpContext.User; + if (context.HttpContext.GetIsBitpayAPI()) + { + var bitpayAuth = context.HttpContext.GetBitpayAuth(); + string storeId = null; + var failedAuth = false; + if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id)) + { + storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id); + if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN)) + { + Logs.PayServer.LogDebug("BitId signature check failed"); + failedAuth = true; + } + } + else if (!string.IsNullOrEmpty(bitpayAuth.Authorization)) + { + storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization); + if (storeId == null) + { + Logs.PayServer.LogDebug("API key check failed"); + failedAuth = true; + } + } + + if (storeId != null) + { + var identity = ((ClaimsIdentity)context.HttpContext.User.Identity); + identity.AddClaim(new Claim(Policies.CanUseStore.Key, storeId)); + var store = await _StoreRepository.FindStore(storeId); + context.HttpContext.SetStoreData(store); + } + else if (failedAuth) + { + throw new BitpayHttpException(401, "Invalid credentials"); + } + } + } + + private async Task CheckBitId(HttpContext httpContext, string sig, string id) + { + httpContext.Request.EnableRewind(); + + string storeId = null; + string body = string.Empty; + if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) + { + using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) + { + body = reader.ReadToEnd(); + } + httpContext.Request.Body.Position = 0; + } + + var url = httpContext.Request.GetEncodedUrl(); + try + { + var key = new PubKey(id); + if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) + { + var sin = key.GetBitIDSIN(); + var identity = ((ClaimsIdentity)httpContext.User.Identity); + identity.AddClaim(new Claim(Claims.SIN, sin)); + + string token = null; + if (httpContext.Request.Query.TryGetValue("token", out var tokenValues)) + { + token = tokenValues[0]; + } + + if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST") + { + try + { + token = JObject.Parse(body)?.Property("token")?.Value?.Value(); + } + catch { } + } + + if (token != null) + { + var bitToken = await GetTokenPermissionAsync(sin, token); + if (bitToken == null) + { + return null; + } + storeId = bitToken.StoreId; + } + } + } + catch (FormatException) { } + return storeId; + } + + private async Task CheckLegacyAPIKey(HttpContext httpContext, string auth) + { + var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string apiKey = null; + try + { + apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1])); + } + catch + { + return null; + } + return await _TokenRepository.GetStoreIdFromAPIKey(apiKey); + } + + private async Task GetTokenPermissionAsync(string sin, string expectedToken) + { + var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray(); + actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray(); + + var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal)); + if (expectedToken == null || actualToken == null) + { + Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}"); + return null; + } + return actualToken; + } + + private IEnumerable GetCompatibleTokens(BitTokenEntity token) + { + if (token.Facade == Facade.Merchant.ToString()) + { + yield return token.Clone(Facade.User); + yield return token.Clone(Facade.PointOfSale); + } + if (token.Facade == Facade.PointOfSale.ToString()) + { + yield return token.Clone(Facade.User); + } + yield return token; + } + } +} diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs new file mode 100644 index 000000000..470c7058b --- /dev/null +++ b/BTCPayServer/Security/Policies.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace BTCPayServer.Security +{ + public static class Policies + { + public const string CookieAuthentication = "Identity.Application"; + public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) + { + AddClaim(options, CanUseStore.Key); + AddClaim(options, CanModifyStoreSettings.Key); + AddClaim(options, CanModifyServerSettings.Key); + return options; + } + + private static void AddClaim(AuthorizationOptions options, string key) + { + options.AddPolicy(key, o => o.RequireClaim(key)); + } + + public class CanModifyServerSettings + { + public const string Key = "btcpay.store.canmodifyserversettings"; + } + public class CanUseStore + { + public const string Key = "btcpay.store.canusestore"; + } + public class CanModifyStoreSettings + { + public const string Key = "btcpay.store.canmodifystoresettings"; + } + } +} diff --git a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs index f5f1f8d3b..7a55d51d5 100644 --- a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs +++ b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs @@ -39,6 +39,8 @@ namespace BTCPayServer.Services.Fees ExplorerClient _ExplorerClient; public async Task GetFeeRateAsync() { + if (!_ExplorerClient.Network.SupportEstimatesSmartFee) + return _Factory.Fallback; try { return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate; diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index a44f07de7..c7695a365 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -118,18 +118,7 @@ namespace BTCPayServer.Services } } - public async Task SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (strategy == null) - throw new ArgumentNullException(nameof(strategy)); - if (!strategy.Segwit) - return false; - return await GetKeyPath(_Ledger, network, strategy) != null; - } - - private static async Task GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy) + public async Task GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy) { List derivations = new List(); if(network.NBitcoinNetwork.Consensus.SupportSegwit) @@ -143,7 +132,7 @@ namespace BTCPayServer.Services { try { - var extpubkey = await GetExtPubKey(ledger, network, account, true); + var extpubkey = await GetExtPubKey(_Ledger, network, account, true); if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) { foundKeyPath = account; @@ -159,79 +148,12 @@ namespace BTCPayServer.Services return foundKeyPath; } - public async Task SendToAddress(DirectDerivationStrategy strategy, - ReceivedCoin[] coins, BTCPayNetwork network, - (IDestination destination, Money amount, bool substractFees)[] send, - FeeRate feeRate, - IDestination changeAddress, - KeyPath changeKeyPath, - FeeRate minTxRelayFee) + public async Task SignTransactionAsync(SignatureRequest[] signatureRequests, + Transaction unsigned, + KeyPath changeKeyPath) { - if (strategy == null) - throw new ArgumentNullException(nameof(strategy)); - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (feeRate == null) - throw new ArgumentNullException(nameof(feeRate)); - if (changeAddress == null) - throw new ArgumentNullException(nameof(changeAddress)); - if (feeRate.FeePerK <= Money.Zero) - { - throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero"); - } - - foreach (var element in send) - { - if (element.destination == null) - throw new ArgumentNullException(nameof(element.destination)); - if (element.amount == null) - throw new ArgumentNullException(nameof(element.amount)); - if (element.amount <= Money.Zero) - throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); - } - - var foundKeyPath = await GetKeyPath(Ledger, network, strategy); - - if (foundKeyPath == null) - { - throw new HardwareWalletException($"This store is not configured to use this ledger"); - } - - TransactionBuilder builder = new TransactionBuilder(); - builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee; - builder.SetConsensusFactory(network.NBitcoinNetwork); - builder.AddCoins(coins.Select(c=>c.Coin).ToArray()); - - foreach (var element in send) - { - builder.Send(element.destination, element.amount); - if (element.substractFees) - builder.SubtractFees(); - } - builder.SetChange(changeAddress); - builder.SendEstimatedFees(feeRate); - builder.Shuffle(); - var unsigned = builder.BuildTransaction(false); - - var keypaths = new Dictionary(); - foreach(var c in coins) - { - keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); - } - - var hasChange = unsigned.Outputs.Count == 2; - var usedCoins = builder.FindSpentCoins(unsigned); _Transport.Timeout = TimeSpan.FromMinutes(5); - var fullySigned = await Ledger.SignTransactionAsync( - usedCoins.Select(c => new SignatureRequest - { - InputCoin = c, - KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), - PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey - }).ToArray(), - unsigned, - hasChange ? foundKeyPath.Derive(changeKeyPath) : null); - return fullySigned; + return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath); } } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 9330a218a..27204ed0f 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -12,6 +12,7 @@ using NBXplorer.Models; using NBXplorer; using NBXplorer.DerivationStrategy; using BTCPayServer.Payments; +using NBitpayClient; namespace BTCPayServer.Services.Invoices { @@ -100,7 +101,8 @@ namespace BTCPayServer.Services.Invoices { HighSpeed = 0, MediumSpeed = 1, - LowSpeed = 2 + LowSpeed = 2, + LowMediumSpeed = 3 } public class InvoiceEntity { @@ -314,6 +316,7 @@ namespace BTCPayServer.Services.Invoices } public bool ExtendedNotifications { get; set; } public List Events { get; internal set; } + public double PaymentTolerance { get; set; } public bool IsExpired() { @@ -334,18 +337,35 @@ namespace BTCPayServer.Services.Invoices ExpirationTime = ExpirationTime, Status = Status, Currency = ProductInformation.Currency, - Flags = new Flags() { Refundable = Refundable } + Flags = new Flags() { Refundable = Refundable }, + + PaymentSubtotals = new Dictionary(), + PaymentTotals= new Dictionary(), + SupportedTransactionCurrencies = new Dictionary(), + Addresses = new Dictionary(), + PaymentCodes = new Dictionary(), + ExchangeRates = new Dictionary>() }; + dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; dto.CryptoInfo = new List(); - foreach (var info in this.GetPaymentMethods(networkProvider, true)) + foreach (var info in this.GetPaymentMethods(networkProvider)) { + var accounting = info.Calculate(); var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); - cryptoInfo.CryptoCode = info.GetId().CryptoCode; + var subtotalPrice = accounting.TotalDue - accounting.NetworkFee; + var cryptoCode = info.GetId().CryptoCode; + var address = info.GetPaymentMethodDetails()?.GetPaymentDestination(); + var exrates = new Dictionary + { + { ProductInformation.Currency, cryptoInfo.Rate } + }; + + cryptoInfo.CryptoCode = cryptoCode; cryptoInfo.PaymentType = info.GetId().PaymentType.ToString(); cryptoInfo.Rate = info.Rate; - cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString(); + cryptoInfo.Price = subtotalPrice.ToString(); cryptoInfo.Due = accounting.Due.ToString(); cryptoInfo.Paid = accounting.Paid.ToString(); @@ -354,19 +374,16 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.TxCount = accounting.TxCount; cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); - cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination(); - cryptoInfo.ExRates = new Dictionary - { - { ProductInformation.Currency, (double)cryptoInfo.Rate } - }; - + cryptoInfo.Address = address; + + cryptoInfo.ExRates = exrates; + var paymentId = info.GetId(); var scheme = info.Network.UriScheme; - var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; - cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id; + cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}"; - - if (info.GetId().PaymentType == PaymentTypes.BTCLike) + if (paymentId.PaymentType == PaymentTypes.BTCLike) { + var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() { BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", @@ -375,7 +392,7 @@ namespace BTCPayServer.Services.Invoices BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", }; } - var paymentId = info.GetId(); + if (paymentId.PaymentType == PaymentTypes.LightningLike) { cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() @@ -386,7 +403,6 @@ namespace BTCPayServer.Services.Invoices #pragma warning disable CS0618 if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike) { - dto.Url = cryptoInfo.Url; dto.BTCPrice = cryptoInfo.Price; dto.Rate = cryptoInfo.Rate; dto.ExRates = cryptoInfo.ExRates; @@ -395,17 +411,28 @@ namespace BTCPayServer.Services.Invoices dto.BTCDue = cryptoInfo.Due; dto.PaymentUrls = cryptoInfo.PaymentUrls; } + #pragma warning restore CS0618 - if (!info.IsPhantomBTC) - dto.CryptoInfo.Add(cryptoInfo); + dto.CryptoInfo.Add(cryptoInfo); + + dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls); + dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi); + dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi); + dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency() + { + Enabled = true + }); + dto.Addresses.Add(paymentId.ToString(), address); + dto.ExchangeRates.TryAdd(cryptoCode, exrates); } + //dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice + Populate(ProductInformation, dto); Populate(BuyerInformation, dto); dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for dto.Guid = Guid.NewGuid().ToString(); - dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus); return dto; } @@ -432,26 +459,15 @@ namespace BTCPayServer.Services.Invoices return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider); } - public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false) + public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider) { PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider); var serializer = new Serializer(Dummy); - PaymentMethod phantom = null; #pragma warning disable CS0618 - // Legacy - if (alwaysIncludeBTC) - { - var btcNetwork = networkProvider?.GetNetwork("BTC"); - phantom = new PaymentMethod() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork }; - if (btcNetwork != null || networkProvider == null) - rates.Add(phantom); - } if (PaymentMethod != null) { foreach (var prop in PaymentMethod.Properties()) { - if (prop.Name == "BTC" && phantom != null) - rates.Remove(phantom); var r = serializer.ToObject(prop.Value.ToString()); var paymentMethodId = PaymentMethodId.Parse(prop.Name); r.CryptoCode = paymentMethodId.CryptoCode; @@ -537,6 +553,10 @@ namespace BTCPayServer.Services.Invoices /// Total amount of network fee to pay to the invoice /// public Money NetworkFee { get; set; } + /// + /// Minimum required to be paid in order to accept invocie as paid + /// + public Money MinimumTotalDue { get; set; } } public class PaymentMethod @@ -635,20 +655,17 @@ namespace BTCPayServer.Services.Invoices [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] public string DepositAddress { get; set; } - [JsonIgnore] - public bool IsPhantomBTC { get; set; } - public PaymentMethodAccounting Calculate(Func paymentPredicate = null) { paymentPredicate = paymentPredicate ?? new Func((p) => true); - var paymentMethods = ParentEntity.GetPaymentMethods(null, IsPhantomBTC); + var paymentMethods = ParentEntity.GetPaymentMethods(null); var totalDue = ParentEntity.ProductInformation.Price / Rate; var paid = 0m; var cryptoPaid = 0.0m; int precision = 8; - var paidTxFee = 0m; + var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision)); bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); int txRequired = 0; var payments = @@ -662,9 +679,8 @@ namespace BTCPayServer.Services.Invoices if (!paidEnough) { totalDue += txFee; - paidTxFee += txFee; } - paidEnough |= paid >= Extensions.RoundUp(totalDue, precision); + paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision); if (GetId() == _.GetPaymentMethodId()) { cryptoPaid += _.GetCryptoPaymentData().GetValue(); @@ -680,16 +696,16 @@ namespace BTCPayServer.Services.Invoices { txRequired++; totalDue += GetTxFee(); - paidTxFee += GetTxFee(); } accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision)); - accounting.Paid = Money.Coins(paid); + accounting.Paid = Money.Coins(Extensions.RoundUp(paid, precision)); accounting.TxRequired = txRequired; - accounting.CryptoPaid = Money.Coins(cryptoPaid); + accounting.CryptoPaid = Money.Coins(Extensions.RoundUp(cryptoPaid, precision)); accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); accounting.DueUncapped = accounting.TotalDue - accounting.Paid; - accounting.NetworkFee = Money.Coins(paidTxFee); + accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; + accounting.MinimumTotalDue = Money.Max(Money.Satoshis(1), Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)))); return accounting; } @@ -762,7 +778,7 @@ namespace BTCPayServer.Services.Invoices paymentData.Outpoint = Outpoint; return paymentData; } - if(GetPaymentMethodId().PaymentType== PaymentTypes.LightningLike) + if (GetPaymentMethodId().PaymentType == PaymentTypes.LightningLike) { return JsonConvert.DeserializeObject(CryptoPaymentData); } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index d4c90b718..61c79259e 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices invoice.StoreId = storeId; using (var context = _ContextFactory.CreateContext()) { - context.Invoices.Add(new InvoiceData() + context.Invoices.Add(new Data.InvoiceData() { StoreDataId = storeId, Id = invoice.Id, @@ -267,7 +267,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) return; var invoiceEntity = ToObject(invoiceData.Blob, null); @@ -307,7 +307,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) return; invoiceData.Status = status; @@ -320,7 +320,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData?.Status != "paid") return; invoiceData.Status = "invalid"; @@ -331,7 +331,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - IQueryable query = + IQueryable query = context .Invoices .Include(o => o.Payments) @@ -351,7 +351,7 @@ namespace BTCPayServer.Services.Invoices } } - private InvoiceEntity ToEntity(InvoiceData invoice) + private InvoiceEntity ToEntity(Data.InvoiceData invoice) { var entity = ToObject(invoice.Blob, null); #pragma warning disable CS0618 @@ -386,7 +386,7 @@ namespace BTCPayServer.Services.Invoices { using (var context = _ContextFactory.CreateContext()) { - IQueryable query = context + IQueryable query = context .Invoices .Include(o => o.Payments) .Include(o => o.RefundAddresses); @@ -436,6 +436,18 @@ namespace BTCPayServer.Services.Invoices query = query.Where(i => statusSet.Contains(i.Status)); } + if(queryObject.Unusual != null) + { + var unused = queryObject.Unusual.Value; + query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null)); + } + + if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0) + { + var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet(); + query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus)); + } + query = query.OrderByDescending(q => q.Created); if (queryObject.Skip != null) @@ -451,6 +463,29 @@ namespace BTCPayServer.Services.Invoices } + private string NormalizeExceptionStatus(string status) + { + status = status.ToLowerInvariant(); + switch (status) + { + case "paidover": + case "over": + case "overpaid": + status = "paidOver"; + break; + case "paidlate": + case "late": + status = "paidLate"; + break; + case "paidpartial": + case "underpaid": + case "partial": + status = "paidPartial"; + break; + } + return status; + } + public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network) { if (outputs.Length == 0) @@ -614,10 +649,18 @@ namespace BTCPayServer.Services.Invoices get; set; } + public bool? Unusual { get; set; } + public string[] Status { get; set; } + + public string[] ExceptionStatus + { + get; set; + } + public string InvoiceId { get; diff --git a/BTCPayServer/Services/LanguageService.cs b/BTCPayServer/Services/LanguageService.cs index c738be363..bfaeb64ed 100644 --- a/BTCPayServer/Services/LanguageService.cs +++ b/BTCPayServer/Services/LanguageService.cs @@ -31,6 +31,7 @@ namespace BTCPayServer.Services new Language("nl-NL", "Dutch"), new Language("cs-CZ", "Česky"), new Language("is-IS", "Íslenska"), + new Language("hr-HR", "Croatian"), }; } } diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index c97c2cbbd..2e51db317 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -24,8 +24,8 @@ namespace BTCPayServer.Services.Mails } public async Task SendEmailAsync(string email, string subject, string message) { - var settings = await _Repository.GetSettingAsync(); - if (settings == null) + var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); + if (!settings.IsComplete()) { Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); return; @@ -36,8 +36,8 @@ namespace BTCPayServer.Services.Mails public async Task SendMailCore(string email, string subject, string message) { - var settings = await _Repository.GetSettingAsync(); - if (settings == null) + var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); + if (!settings.IsComplete()) throw new InvalidOperationException("Email settings not configured"); var smtp = settings.CreateSmtpClient(); MailMessage mail = new MailMessage(settings.From, email, subject, message); diff --git a/BTCPayServer/Services/Mails/EmailSettings.cs b/BTCPayServer/Services/Mails/EmailSettings.cs index 289b18c3a..93b062eec 100644 --- a/BTCPayServer/Services/Mails/EmailSettings.cs +++ b/BTCPayServer/Services/Mails/EmailSettings.cs @@ -10,30 +10,25 @@ namespace BTCPayServer.Services.Mails { public class EmailSettings { - [Required] public string Server { get; set; } - [Required] public int? Port { get; set; } - [Required] public String Login { get; set; } - [Required] public String Password { get; set; } - [EmailAddress] public string From { @@ -45,6 +40,18 @@ namespace BTCPayServer.Services.Mails get; set; } + public bool IsComplete() + { + SmtpClient smtp = null; + try + { + smtp = CreateSmtpClient(); + return true; + } + catch { } + return false; + } + public SmtpClient CreateSmtpClient() { SmtpClient client = new SmtpClient(Server, Port.Value); diff --git a/BTCPayServer/Services/PoliciesSettings.cs b/BTCPayServer/Services/PoliciesSettings.cs index 431e902eb..b1ef0a423 100644 --- a/BTCPayServer/Services/PoliciesSettings.cs +++ b/BTCPayServer/Services/PoliciesSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; @@ -8,12 +9,14 @@ namespace BTCPayServer.Services { public class PoliciesSettings { + [Display(Name = "Requires a confirmation mail for registering")] public bool RequiresConfirmedEmail { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + [Display(Name = "Disable registration")] public bool LockSubscription { get; set; } } } diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index 1d71ec469..381c148dd 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -2,17 +2,40 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; +using BTCPayServer.Rating; +using ExchangeSharp; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; namespace BTCPayServer.Services.Rates { - public class BTCPayRateProviderFactory : IRateProviderFactory + public class ExchangeException { + public Exception Exception { get; set; } + public string ExchangeName { get; set; } + } + public class RateResult + { + public List ExchangeExceptions { get; set; } = new List(); + public string Rule { get; set; } + public string EvaluatedRule { get; set; } + public HashSet Errors { get; set; } + public decimal? Value { get; set; } + public bool Cached { get; internal set; } + } + + public class BTCPayRateProviderFactory + { + class QueryRateResult + { + public bool CachedResult { get; set; } + public List Exceptions { get; set; } + public ExchangeRates ExchangeRates { get; set; } + } IMemoryCache _Cache; private IOptions _CacheOptions; - public IMemoryCache Cache { get @@ -20,18 +43,73 @@ namespace BTCPayServer.Services.Rates return _Cache; } } - public BTCPayRateProviderFactory(IOptions cacheOptions, IServiceProvider serviceProvider) + CoinAverageSettings _CoinAverageSettings; + public BTCPayRateProviderFactory(IOptions cacheOptions, + BTCPayNetworkProvider btcpayNetworkProvider, + CoinAverageSettings coinAverageSettings) { if (cacheOptions == null) throw new ArgumentNullException(nameof(cacheOptions)); + _CoinAverageSettings = coinAverageSettings; _Cache = new MemoryCache(cacheOptions); _CacheOptions = cacheOptions; // We use 15 min because of limits with free version of bitcoinaverage CacheSpan = TimeSpan.FromMinutes(15.0); - this.serviceProvider = serviceProvider; + this.btcpayNetworkProvider = btcpayNetworkProvider; + InitExchanges(); } - IServiceProvider serviceProvider; + public bool UseCoinAverageAsFallback { get; set; } = true; + + private void InitExchanges() + { + // We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request + DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true)); + 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)); + //DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI())); + //DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI())); + //DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI())); + //DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI())); + //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 + { + get + { + return _DirectProviders; + } + } + + + BTCPayNetworkProvider btcpayNetworkProvider; TimeSpan _CacheSpan; public TimeSpan CacheSpan { @@ -51,45 +129,87 @@ namespace BTCPayServer.Services.Rates _Cache = new MemoryCache(_CacheOptions); } - public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) + public async Task FetchRate(CurrencyPair pair, RateRules rules) { - 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); + return await FetchRates(new HashSet(new[] { pair }), rules).First().Value; } - private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange) + public Dictionary> FetchRates(HashSet pairs, RateRules rules) + { + if (rules == null) + throw new ArgumentNullException(nameof(rules)); + + var fetchingRates = new Dictionary>(); + var fetchingExchanges = new Dictionary>(); + var consolidatedRates = new ExchangeRates(); + + foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p)))) + { + var dependentQueries = new List>(); + foreach (var requiredExchange in i.RateRule.ExchangeRates) + { + if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) + { + fetching = QueryRates(requiredExchange.Exchange); + fetchingExchanges.Add(requiredExchange.Exchange, fetching); + } + dependentQueries.Add(fetching); + } + fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule)); + } + return fetchingRates; + } + + private async Task GetRuleValue(List> dependentQueries, RateRule rateRule) + { + var result = new RateResult(); + result.Cached = true; + foreach (var queryAsync in dependentQueries) + { + var query = await queryAsync; + if (!query.CachedResult) + result.Cached = false; + result.ExchangeExceptions.AddRange(query.Exceptions); + foreach (var rule in query.ExchangeRates) + { + rateRule.ExchangeRates.Add(rule); + } + } + rateRule.Reevaluate(); + result.Value = rateRule.Value; + result.Errors = rateRule.Errors; + result.EvaluatedRule = rateRule.ToString(true); + result.Rule = rateRule.ToString(false); + return result; + } + + + private async Task QueryRates(string exchangeName) { List providers = new List(); - - if(exchange == "quadrigacx") + if (DirectProviders.TryGetValue(exchangeName, out var directProvider)) + providers.Add(directProvider); + if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName)) { - providers.Add(new QuadrigacxRateProvider(network.CryptoCode)); + providers.Add(new CoinAverageRateProvider() + { + Exchange = exchangeName, + Authenticator = _CoinAverageSettings + }); } - - var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider); - coinAverage.Exchange = exchange; - providers.Add(coinAverage); - return new FallbackRateProvider(providers.ToArray()); - } - - 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) - { - if(network.DefaultRateProvider == null) + var fallback = new FallbackRateProvider(providers.ToArray()); + var cached = new CachedRateProvider(exchangeName, fallback, _Cache) { - throw new RateUnavailableException(network.CryptoCode); - } - return network.DefaultRateProvider.CreateRateProvider(serviceProvider); + CacheSpan = CacheSpan + }; + var value = await cached.GetRatesAsync(); + return new QueryRateResult() + { + CachedResult = !fallback.Used, + ExchangeRates = value, + Exceptions = fallback.Exceptions + .Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList() + }; } } } diff --git a/BTCPayServer/Services/Rates/BitpayRateProvider.cs b/BTCPayServer/Services/Rates/BitpayRateProvider.cs index edf44aab1..0898fd883 100644 --- a/BTCPayServer/Services/Rates/BitpayRateProvider.cs +++ b/BTCPayServer/Services/Rates/BitpayRateProvider.cs @@ -5,18 +5,13 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using NBitcoin; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { - public class BitpayRateProviderDescription : RateProviderDescription - { - public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) - { - return new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))); - } - } public class BitpayRateProvider : IRateProvider { + public const string BitpayName = "bitpay"; Bitpay _Bitpay; public BitpayRateProvider(Bitpay bitpay) { @@ -24,21 +19,13 @@ namespace BTCPayServer.Services.Rates throw new ArgumentNullException(nameof(bitpay)); _Bitpay = bitpay; } - public async Task GetRateAsync(string currency) - { - var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false); - var rate = rates.GetRate(currency); - if (rate == 0m) - throw new RateUnavailableException(currency); - return (decimal)rate; - } - public async Task> GetRatesAsync() + public async Task GetRatesAsync() { - return (await _Bitpay.GetRatesAsync().ConfigureAwait(false)) + return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false)) .AllRates - .Select(r => new Rate() { Currency = r.Code, Value = r.Value }) - .ToList(); + .Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value }) + .ToList()); } } } diff --git a/BTCPayServer/Services/Rates/CachedRateProvider.cs b/BTCPayServer/Services/Rates/CachedRateProvider.cs index 9f6016b6c..6f1e6bbe4 100644 --- a/BTCPayServer/Services/Rates/CachedRateProvider.cs +++ b/BTCPayServer/Services/Rates/CachedRateProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using BTCPayServer.Rating; using BTCPayServer.Services.Rates; using Microsoft.Extensions.Caching.Memory; @@ -10,9 +11,8 @@ namespace BTCPayServer.Services.Rates { private IRateProvider _Inner; private IMemoryCache _MemoryCache; - private string _CryptoCode; - public CachedRateProvider(string cryptoCode, IRateProvider inner, IMemoryCache memoryCache) + public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache) { if (inner == null) throw new ArgumentNullException(nameof(inner)); @@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates throw new ArgumentNullException(nameof(memoryCache)); this._Inner = inner; this.MemoryCache = memoryCache; - this._CryptoCode = cryptoCode; + this.ExchangeName = exchangeName; } public IRateProvider Inner @@ -31,31 +31,22 @@ namespace BTCPayServer.Services.Rates } } + public string ExchangeName { get; set; } + public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0); public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; } - - public Task GetRateAsync(string currency) - { - return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan; - return _Inner.GetRateAsync(currency); - }); - } - public Task> GetRatesAsync() + public Task GetRatesAsync() { - return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) => + return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan; return _Inner.GetRatesAsync(); }); } - - public string AdditionalScope { get; set; } } } diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs index 5abe33b48..0ee5c26b1 100644 --- a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs +++ b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.ComponentModel; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { @@ -21,29 +22,6 @@ namespace BTCPayServer.Services.Rates } } - public class CoinAverageRateProviderDescription : RateProviderDescription - { - public CoinAverageRateProviderDescription(string crypto) - { - CryptoCode = crypto; - } - - public string CryptoCode { get; set; } - - 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 @@ -69,18 +47,18 @@ namespace BTCPayServer.Services.Rates public interface ICoinAverageAuthenticator { Task AddHeader(HttpRequestMessage message); - } + } public class CoinAverageRateProvider : IRateProvider { + public const string CoinAverageName = "coinaverage"; + public CoinAverageRateProvider() + { + + } static HttpClient _Client = new HttpClient(); - public CoinAverageRateProvider(string cryptoCode) - { - CryptoCode = cryptoCode ?? "BTC"; - } - - public string Exchange { get; set; } + public string Exchange { get; set; } = CoinAverageName; public string CryptoCode { get; set; } @@ -88,27 +66,19 @@ namespace BTCPayServer.Services.Rates { get; set; } = "global"; - public async Task GetRateAsync(string currency) - { - var rates = await GetRatesCore(); - return GetRate(rates, currency); - } - - private decimal GetRate(Dictionary rates, string currency) - { - if (currency == "BTC") - return 1.0m; - if (rates.TryGetValue(currency, out decimal result)) - return result; - throw new RateUnavailableException(currency); - } public ICoinAverageAuthenticator Authenticator { get; set; } - private async Task> GetRatesCore() + private bool TryToDecimal(JProperty p, out decimal v) { - string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short" - : $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}"; + JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"]; + return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); + } + + public async Task GetRatesAsync() + { + string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short" + : $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}"; var request = new HttpRequestMessage(HttpMethod.Get, url); var auth = Authenticator; @@ -128,36 +98,29 @@ namespace BTCPayServer.Services.Rates throw new CoinAverageException("Unauthorized access to the API, premium plan needed"); resp.EnsureSuccessStatusCode(); var rates = JObject.Parse(await resp.Content.ReadAsStringAsync()); - if(Exchange != null) + if (Exchange != CoinAverageName) { rates = (JObject)rates["symbols"]; } - return rates.Properties() - .Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused)) - .ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => - { - TryToDecimal(p, out decimal v); - return v; - }); + + var exchangeRates = new ExchangeRates(); + foreach (var prop in rates.Properties()) + { + ExchangeRate exchangeRate = new ExchangeRate(); + exchangeRate.Exchange = Exchange; + if (!TryToDecimal(prop, out decimal value)) + continue; + exchangeRate.Value = value; + if(CurrencyPair.TryParse(prop.Name, out var pair)) + { + exchangeRate.CurrencyPair = pair; + exchangeRates.Add(exchangeRate); + } + } + return exchangeRates; } } - private bool TryToDecimal(JProperty p, out decimal v) - { - JToken token = p.Value[Exchange == null ? "last" : "bid"]; - return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); - } - - public async Task> GetRatesAsync() - { - var rates = await GetRatesCore(); - return rates.Select(o => new Rate() - { - Currency = o.Key, - Value = o.Value - }).ToList(); - } - public async Task TestAuthAsync() { var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff"); @@ -217,7 +180,7 @@ namespace BTCPayServer.Services.Rates var exchanges = (JObject)jobj["exchanges"]; response.Exchanges = exchanges .Properties() - .Select(p => + .Select(p => { var exchange = JsonConvert.DeserializeObject(p.Value.ToString()); exchange.Name = p.Name; diff --git a/BTCPayServer/Services/Rates/CoinAverageSettings.cs b/BTCPayServer/Services/Rates/CoinAverageSettings.cs index d76be9d64..9d0c06df2 100644 --- a/BTCPayServer/Services/Rates/CoinAverageSettings.cs +++ b/BTCPayServer/Services/Rates/CoinAverageSettings.cs @@ -20,12 +20,42 @@ namespace BTCPayServer.Services.Rates return _Settings.AddHeader(message); } } + + public class CoinAverageExchange + { + public CoinAverageExchange(string name, string display) + { + Name = name; + Display = display; + } + public string Name { get; set; } + public string Display { get; set; } + public string Url + { + get + { + return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short" + : $"https://apiv2.bitcoinaverage.com/exchanges/{Name}"; + } + } + } + public class CoinAverageExchanges : Dictionary + { + public CoinAverageExchanges() + { + } + + public void Add(CoinAverageExchange exchange) + { + TryAdd(exchange.Name, exchange); + } + } 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 CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges(); public CoinAverageSettings() { @@ -37,8 +67,9 @@ namespace BTCPayServer.Services.Rates // b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),"); //} //b.AppendLine("}.ToArray()"); - - AvailableExchanges = new[] { + AvailableExchanges = new CoinAverageExchanges(); + foreach(var item in + new[] { (DisplayName: "BitBargain", Name: "bitbargain"), (DisplayName: "Tidex", Name: "tidex"), (DisplayName: "LocalBitcoins", Name: "localbitcoins"), @@ -89,7 +120,10 @@ namespace BTCPayServer.Services.Rates (DisplayName: "Quoine", Name: "quoine"), (DisplayName: "BTC Markets", Name: "btcmarkets"), (DisplayName: "Bitso", Name: "bitso"), - }.ToArray(); + }) + { + AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName)); + } } public Task AddHeader(HttpRequestMessage message) diff --git a/BTCPayServer/Services/Rates/CurrencyNameTable.cs b/BTCPayServer/Services/Rates/CurrencyNameTable.cs index 01c6b57fa..97e748722 100644 --- a/BTCPayServer/Services/Rates/CurrencyNameTable.cs +++ b/BTCPayServer/Services/Rates/CurrencyNameTable.cs @@ -40,6 +40,14 @@ namespace BTCPayServer.Services.Rates } static Dictionary _CurrencyProviders = new Dictionary(); + + public NumberFormatInfo GetNumberFormatInfo(string currency) + { + var data = GetCurrencyProvider(currency); + if (data is NumberFormatInfo nfi) + return nfi; + return ((CultureInfo)data).NumberFormat; + } public IFormatProvider GetCurrencyProvider(string currency) { lock (_CurrencyProviders) @@ -54,7 +62,11 @@ namespace BTCPayServer.Services.Rates } catch { } } - AddCurrency(_CurrencyProviders, "BTC", 8, "BTC"); + + foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll()) + { + AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode); + } } return _CurrencyProviders.TryGet(currency); } @@ -106,6 +118,17 @@ namespace BTCPayServer.Services.Rates info.Symbol = splitted[3]; } } + + foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll()) + { + dico.TryAdd(network.CryptoCode, new CurrencyData() + { + Code = network.CryptoCode, + Divisibility = 8, + Name = network.CryptoCode + }); + } + return dico.Values.ToArray(); } diff --git a/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs b/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs new file mode 100644 index 000000000..9aeaf7fb1 --- /dev/null +++ b/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using ExchangeSharp; + +namespace BTCPayServer.Services.Rates +{ + public class ExchangeSharpRateProvider : IRateProvider + { + readonly ExchangeAPI _ExchangeAPI; + readonly string _ExchangeName; + public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false) + { + if (exchangeAPI == null) + throw new ArgumentNullException(nameof(exchangeAPI)); + exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0); + _ExchangeAPI = exchangeAPI; + _ExchangeName = exchangeName; + ReverseCurrencyPair = reverseCurrencyPair; + } + + public bool ReverseCurrencyPair + { + get; set; + } + + public async Task GetRatesAsync() + { + await new SynchronizationContextRemover(); + var rates = await _ExchangeAPI.GetTickersAsync(); + lock (notFoundSymbols) + { + var exchangeRates = + rates.Select(t => CreateExchangeRate(t)) + .Where(t => t != null) + .ToArray(); + return new ExchangeRates(exchangeRates); + } + } + + // ExchangeSymbolToGlobalSymbol throws exception which would kill perf + HashSet notFoundSymbols = new HashSet(); + private ExchangeRate CreateExchangeRate(KeyValuePair ticker) + { + if (notFoundSymbols.Contains(ticker.Key)) + return null; + try + { + var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key); + if (!CurrencyPair.TryParse(tickerName, out var pair)) + { + notFoundSymbols.Add(ticker.Key); + return null; + } + if(ReverseCurrencyPair) + pair = new CurrencyPair(pair.Right, pair.Left); + var rate = new ExchangeRate(); + rate.CurrencyPair = pair; + rate.Exchange = _ExchangeName; + rate.Value = ticker.Value.Bid; + return rate; + } + catch (ArgumentException) + { + notFoundSymbols.Add(ticker.Key); + return null; + } + } + } +} diff --git a/BTCPayServer/Services/Rates/FallbackRateProvider.cs b/BTCPayServer/Services/Rates/FallbackRateProvider.cs index 18f31dfc0..2e618cb3a 100644 --- a/BTCPayServer/Services/Rates/FallbackRateProvider.cs +++ b/BTCPayServer/Services/Rates/FallbackRateProvider.cs @@ -2,58 +2,35 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { - public class FallbackRateProviderDescription : RateProviderDescription - { - public FallbackRateProviderDescription(RateProviderDescription[] rateProviders) - { - RateProviders = rateProviders; - } - - public RateProviderDescription[] RateProviders { get; set; } - - public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) - { - return new FallbackRateProvider(RateProviders.Select(r => r.CreateRateProvider(serviceProvider)).ToArray()); - } - } - public class FallbackRateProvider : IRateProvider { - IRateProvider[] _Providers; + public bool Used { get; set; } public FallbackRateProvider(IRateProvider[] providers) { if (providers == null) throw new ArgumentNullException(nameof(providers)); _Providers = providers; } - public async Task GetRateAsync(string currency) - { - foreach(var p in _Providers) - { - try - { - return await p.GetRateAsync(currency).ConfigureAwait(false); - } - catch { } - } - throw new RateUnavailableException(currency); - } - public async Task> GetRatesAsync() + public async Task GetRatesAsync() { + Used = true; foreach (var p in _Providers) { try { return await p.GetRatesAsync().ConfigureAwait(false); } - catch { } + catch(Exception ex) { Exceptions.Add(ex); } } - throw new RateUnavailableException("ALL"); + return new ExchangeRates(); } + + public List Exceptions { get; set; } = new List(); } } diff --git a/BTCPayServer/Services/Rates/IRateProvider.cs b/BTCPayServer/Services/Rates/IRateProvider.cs index 19a33ae45..00b26c5bd 100644 --- a/BTCPayServer/Services/Rates/IRateProvider.cs +++ b/BTCPayServer/Services/Rates/IRateProvider.cs @@ -2,32 +2,12 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { - public class Rate - { - public Rate() - { - - } - public Rate(string currency, decimal value) - { - Value = value; - Currency = currency; - } - public string Currency - { - get; set; - } - public decimal Value - { - get; set; - } - } public interface IRateProvider { - Task GetRateAsync(string currency); - Task> GetRatesAsync(); + Task GetRatesAsync(); } } diff --git a/BTCPayServer/Services/Rates/IRateProviderFactory.cs b/BTCPayServer/Services/Rates/IRateProviderFactory.cs deleted file mode 100644 index 5c3b76a77..000000000 --- a/BTCPayServer/Services/Rates/IRateProviderFactory.cs +++ /dev/null @@ -1,40 +0,0 @@ -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, RateRules rules); - TimeSpan CacheSpan { get; set; } - void InvalidateCache(); - } -} diff --git a/BTCPayServer/Services/Rates/MockRateProvider.cs b/BTCPayServer/Services/Rates/MockRateProvider.cs deleted file mode 100644 index 28d8298d1..000000000 --- a/BTCPayServer/Services/Rates/MockRateProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Services.Rates -{ - public class MockRateProviderFactory : IRateProviderFactory - { - List _Mocks = new List(); - public MockRateProviderFactory() - { - - } - - public TimeSpan CacheSpan { get; set; } - - public void AddMock(MockRateProvider mock) - { - _Mocks.Add(mock); - } - public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) - { - return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode); - } - - public void InvalidateCache() - { - - } - } - public class MockRateProvider : IRateProvider - { - List _Rates; - - public string CryptoCode { get; } - - public MockRateProvider(string cryptoCode, params Rate[] rates) - { - _Rates = new List(rates); - CryptoCode = cryptoCode; - } - public MockRateProvider(string cryptoCode, List rates) - { - _Rates = rates; - CryptoCode = cryptoCode; - } - public Task GetRateAsync(string currency) - { - var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase)); - if (rate == null) - throw new RateUnavailableException(currency); - return Task.FromResult(rate.Value); - } - - public Task> GetRatesAsync() - { - ICollection rates = _Rates; - return Task.FromResult(rates); - } - } -} diff --git a/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs b/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs index 34ae2b12d..10fb75189 100644 --- a/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs +++ b/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs @@ -4,32 +4,15 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using BTCPayServer.Rating; using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { public class QuadrigacxRateProvider : IRateProvider { - public QuadrigacxRateProvider(string crypto) - { - CryptoCode = crypto; - } - public string CryptoCode { get; set; } + public const string QuadrigacxName = "quadrigacx"; static HttpClient _Client = new HttpClient(); - public async Task GetRateAsync(string currency) - { - return await GetRatesAsyncCore(CryptoCode, currency); - } - - private async Task 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) { @@ -40,26 +23,26 @@ namespace BTCPayServer.Services.Rates return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); } - public async Task> GetRatesAsync() + public async Task 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 result = new List(); + var exchangeRates = new ExchangeRates(); foreach (var prop in rates.Properties()) { - var rate = new Rate(); - var splitted = prop.Name.Split('_'); - var crypto = splitted[0].ToUpperInvariant(); - if (crypto != CryptoCode) + var rate = new ExchangeRate(); + if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair)) + continue; + rate.CurrencyPair = pair; + rate.Exchange = QuadrigacxName; + if (!TryToDecimal((JObject)prop.Value, out var v)) continue; - rate.Currency = splitted[1].ToUpperInvariant(); - TryToDecimal((JObject)prop.Value, out var v); rate.Value = v; - result.Add(rate); + exchangeRates.Add(rate); } - return result; + return exchangeRates; } } } diff --git a/BTCPayServer/Services/Rates/RateProviderDescription.cs b/BTCPayServer/Services/Rates/RateProviderDescription.cs deleted file mode 100644 index bffac1b37..000000000 --- a/BTCPayServer/Services/Rates/RateProviderDescription.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Services.Rates -{ - public interface RateProviderDescription - { - IRateProvider CreateRateProvider(IServiceProvider serviceProvider); - } -} diff --git a/BTCPayServer/Services/Rates/RateUnavailableException.cs b/BTCPayServer/Services/Rates/RateUnavailableException.cs deleted file mode 100644 index a21cbf71f..000000000 --- a/BTCPayServer/Services/Rates/RateUnavailableException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace BTCPayServer.Services.Rates -{ - public class RateUnavailableException : Exception - { - public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency) - { - if (currency == null) - throw new ArgumentNullException(nameof(currency)); - Currency = currency; - } - - public string Currency - { - get; set; - } - } -} diff --git a/BTCPayServer/Services/Rates/TweakRateProvider.cs b/BTCPayServer/Services/Rates/TweakRateProvider.cs deleted file mode 100644 index dcca887cd..000000000 --- a/BTCPayServer/Services/Rates/TweakRateProvider.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Data; - -namespace BTCPayServer.Services.Rates -{ - public class TweakRateProvider : IRateProvider - { - private BTCPayNetwork network; - private IRateProvider rateProvider; - private RateRules rateRules; - - public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (rateProvider == null) - throw new ArgumentNullException(nameof(rateProvider)); - if (rateRules == null) - throw new ArgumentNullException(nameof(rateRules)); - this.network = network; - this.rateProvider = rateProvider; - this.rateRules = rateRules; - } - - public async Task GetRateAsync(string currency) - { - var rate = await rateProvider.GetRateAsync(currency); - foreach(var rule in rateRules) - { - rate = rule.Apply(network, rate); - } - return rate; - } - - public async Task> GetRatesAsync() - { - List rates = new List(); - foreach (var rate in await rateProvider.GetRatesAsync()) - { - var localRate = rate.Value; - foreach (var rule in rateRules) - { - localRate = rule.Apply(network, localRate); - } - rates.Add(new Rate(rate.Currency, localRate)); - } - return rates; - } - } -} diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index f938f4bef..9f4e0fbee 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -44,7 +44,9 @@ namespace BTCPayServer.Services.Stores }).ToArrayAsync()) .Select(us => { +#pragma warning disable CS0612 // Type or member is obsolete us.Store.Role = us.Role; +#pragma warning restore CS0612 // Type or member is obsolete return us.Store; }).FirstOrDefault(); } @@ -84,7 +86,9 @@ namespace BTCPayServer.Services.Stores .ToArrayAsync()) .Select(u => { +#pragma warning disable CS0612 // Type or member is obsolete u.StoreData.Role = u.Role; +#pragma warning restore CS0612 // Type or member is obsolete return u.StoreData; }).ToArray(); } diff --git a/BTCPayServer/StorePolicies.cs b/BTCPayServer/StorePolicies.cs index 876e58700..76957920e 100644 --- a/BTCPayServer/StorePolicies.cs +++ b/BTCPayServer/StorePolicies.cs @@ -5,11 +5,6 @@ using System.Threading.Tasks; namespace BTCPayServer { - public class StorePolicies - { - public const string CanAccessStores = "CanAccessStore"; - public const string OwnStore = "OwnStore"; - } public class StoreRoles { public const string Owner = "Owner"; diff --git a/BTCPayServer/Validation/UriAttribute.cs b/BTCPayServer/Validation/UriAttribute.cs new file mode 100644 index 000000000..0e9c75e08 --- /dev/null +++ b/BTCPayServer/Validation/UriAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace BTCPayServer.Validation +{ + //from https://stackoverflow.com/a/47196738/275504 + public class UriAttribute : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var str = value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture); + Uri uri; + bool valid = string.IsNullOrWhiteSpace(str) || Uri.TryCreate(str, UriKind.Absolute, out uri); + + if (!valid) + { + return new ValidationResult(ErrorMessage); + } + return ValidationResult.Success; + } + } +} diff --git a/BTCPayServer/Views/Account/ConfirmEmail.cshtml b/BTCPayServer/Views/Account/ConfirmEmail.cshtml index 55ceaee89..0f7158654 100644 --- a/BTCPayServer/Views/Account/ConfirmEmail.cshtml +++ b/BTCPayServer/Views/Account/ConfirmEmail.cshtml @@ -7,7 +7,7 @@
- @Html.Partial("_StatusMessage", "Thank you for confirming your email.") +
diff --git a/BTCPayServer/Views/Account/ForgotPassword.cshtml b/BTCPayServer/Views/Account/ForgotPassword.cshtml index fcc7538a9..0747d28b1 100644 --- a/BTCPayServer/Views/Account/ForgotPassword.cshtml +++ b/BTCPayServer/Views/Account/ForgotPassword.cshtml @@ -7,7 +7,7 @@
- @Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
diff --git a/BTCPayServer/Views/Account/Register.cshtml b/BTCPayServer/Views/Account/Register.cshtml index 1c9175918..129ac0d7d 100644 --- a/BTCPayServer/Views/Account/Register.cshtml +++ b/BTCPayServer/Views/Account/Register.cshtml @@ -7,7 +7,7 @@
- @Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
diff --git a/BTCPayServer/Views/Apps/ListApps.cshtml b/BTCPayServer/Views/Apps/ListApps.cshtml index f61af878b..df549a520 100644 --- a/BTCPayServer/Views/Apps/ListApps.cshtml +++ b/BTCPayServer/Views/Apps/ListApps.cshtml @@ -8,7 +8,7 @@
- @Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index c8099fdf2..1e9304634 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -12,7 +12,7 @@
- @Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
diff --git a/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml b/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml index 18d8fe9d9..a4445ca83 100644 --- a/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/ViewPointOfSale.cshtml @@ -1,4 +1,6 @@ -@model ViewPointOfSaleViewModel +@inject BTCPayServer.HostedServices.CssThemeManager themeManager + +@model ViewPointOfSaleViewModel @{ ViewData["Title"] = Model.Title; Layout = null; @@ -11,13 +13,13 @@ - +

@Model.Title

-
+
@for(int i = 0; i < Model.Items.Length; i++) { @@ -34,7 +36,7 @@ {
- +
diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index fff5ade47..e00aa4547 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -16,7 +16,7 @@
- @Html.Partial("Checkout-Spinner") +
@@ -51,7 +51,7 @@ }
- @Html.Partial("Checkout-Spinner") +
@@ -66,10 +66,14 @@
-
+
+ {{ srvModel.btcPaid }} {{ srvModel.cryptoCode }} +
+
{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}
-
+ +
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
@@ -83,6 +87,12 @@
{{$t("Order Amount")}}
{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}
+
+
 
+
+ {{srvModel.orderAmountFiat}} +
+
{{$t("Network Cost")}} @@ -129,7 +139,7 @@
-
+
@@ -147,7 +157,7 @@ @@ -319,7 +329,7 @@ @@ -426,7 +436,7 @@ diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index 8a4484ba1..26268045f 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -11,12 +11,12 @@ - BTCPay Invoice + @Model.HtmlTitle @@ -57,7 +57,7 @@ @@ -114,7 +114,8 @@ 'pt-BR': { translation: locales_pt_br }, 'nl': { translation: locales_nl }, 'cs-CZ': { translation: locales_cs }, - 'is-IS': { translation: locales_is } + 'is-IS': { translation: locales_is }, + 'hr-HR': { translation: locales_hr } }, }); diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 18446e0ab..343d3c613 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -11,6 +11,7 @@ .smMaxWidth { max-width: 200px; } + @@media (min-width: 768px) { .smMaxWidth { max-width: 400px; @@ -27,7 +28,7 @@
- @Html.Partial("_StatusMessage", Model.StatusMessage) +
@@ -166,10 +167,14 @@ Rate Paid Due + @if(Model.StatusException == "paidOver") + { + Overpaid + } - @foreach (var payment in Model.CryptoPayments) + @foreach(var payment in Model.CryptoPayments) { @payment.PaymentMethod @@ -177,13 +182,17 @@ @payment.Rate @payment.Paid @payment.Due + @if(Model.StatusException == "paidOver") + { + @payment.Overpaid + } }
- @if (Model.OnChainPayments.Count > 0) + @if(Model.OnChainPayments.Count > 0) {
@@ -198,7 +207,7 @@ - @foreach (var payment in Model.OnChainPayments) + @foreach(var payment in Model.OnChainPayments) { var replaced = payment.Replaced ? "class='linethrough'" : ""; @@ -217,7 +226,7 @@
} - @if (Model.OffChainPayments.Count > 0) + @if(Model.OffChainPayments.Count > 0) {
@@ -230,7 +239,7 @@ - @foreach (var payment in Model.OffChainPayments) + @foreach(var payment in Model.OffChainPayments) { @payment.Crypto @@ -253,7 +262,7 @@ - @foreach (var address in Model.Addresses) + @foreach(var address in Model.Addresses) { var current = address.Current ? "font-weight-bold" : ""; @@ -277,7 +286,7 @@ - @foreach (var evt in Model.Events) + @foreach(var evt in Model.Events) { @evt.Timestamp diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 250d0e475..d6e6915f8 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -8,7 +8,7 @@
- @Html.Partial("_StatusMessage", Model.StatusMessage) +
@@ -19,15 +19,17 @@

Create, search or pay an invoice. (Help)

- You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
- You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters + You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
+ You can also apply filters to your search by searching for filtername:value, here is a list of supported filters

    -
  • storeid:id for filtering a specific store
  • -
  • status:(expired|invalid|complete|confirmed|paid|new) for filtering a specific status
  • +
  • storeid:id for filtering a specific store
  • +
  • status:(expired|invalid|complete|confirmed|paid|new) for filtering a specific status
  • +
  • exceptionstatus:(paidover|paidlate|paidpartial) for filtering a specific exception state
  • +
  • unusual:(true|false) for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)

- If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`. + If you want all confirmed and complete invoices, you can duplicate a filter status:confirmed status:complete.

@@ -95,7 +97,7 @@ } @invoice.AmountCurrency - @if(invoice.Status == "new") + @if(invoice.ShowCheckout) { Checkout - }Details diff --git a/BTCPayServer/Views/Manage/ChangePassword.cshtml b/BTCPayServer/Views/Manage/ChangePassword.cshtml index 8c3a4dcef..37b70dd98 100644 --- a/BTCPayServer/Views/Manage/ChangePassword.cshtml +++ b/BTCPayServer/Views/Manage/ChangePassword.cshtml @@ -4,7 +4,7 @@ }

@ViewData["Title"]

-@Html.Partial("_StatusMessage", Model.StatusMessage) +
diff --git a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml index ab0187380..8eeb75748 100644 --- a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml +++ b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml @@ -22,6 +22,7 @@

Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

+
  • diff --git a/BTCPayServer/Views/Manage/ExternalLogins.cshtml b/BTCPayServer/Views/Manage/ExternalLogins.cshtml index 488f7542f..c1ec86ddc 100644 --- a/BTCPayServer/Views/Manage/ExternalLogins.cshtml +++ b/BTCPayServer/Views/Manage/ExternalLogins.cshtml @@ -3,7 +3,7 @@ ViewData.SetActivePageAndTitle(ManageNavPages.ExternalLogins, "Manage your external logins"); } -@Html.Partial("_StatusMessage", Model.StatusMessage) + @if (Model.CurrentLogins?.Count > 0) {

    Registered Logins

    diff --git a/BTCPayServer/Views/Manage/Index.cshtml b/BTCPayServer/Views/Manage/Index.cshtml index c94ed5b22..83ff61360 100644 --- a/BTCPayServer/Views/Manage/Index.cshtml +++ b/BTCPayServer/Views/Manage/Index.cshtml @@ -4,7 +4,7 @@ }

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", Model.StatusMessage) +
    diff --git a/BTCPayServer/Views/Manage/SetPassword.cshtml b/BTCPayServer/Views/Manage/SetPassword.cshtml index 6cd28ef16..a53bd7961 100644 --- a/BTCPayServer/Views/Manage/SetPassword.cshtml +++ b/BTCPayServer/Views/Manage/SetPassword.cshtml @@ -4,7 +4,7 @@ }

    Set your password

    -@Html.Partial("_StatusMessage", Model.StatusMessage) +

    You do not have a local username/password for this site. Add a local account so you can log in without an external login. diff --git a/BTCPayServer/Views/Server/Emails.cshtml b/BTCPayServer/Views/Server/Emails.cshtml index f52bda25c..19d5a81b0 100644 --- a/BTCPayServer/Views/Server/Emails.cshtml +++ b/BTCPayServer/Views/Server/Emails.cshtml @@ -5,7 +5,7 @@

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", Model.StatusMessage) +
    @@ -53,6 +53,7 @@
    +
    diff --git a/BTCPayServer/Views/Server/ListUsers.cshtml b/BTCPayServer/Views/Server/ListUsers.cshtml index 5326eefff..3f0a1619e 100644 --- a/BTCPayServer/Views/Server/ListUsers.cshtml +++ b/BTCPayServer/Views/Server/ListUsers.cshtml @@ -5,7 +5,7 @@

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", Model.StatusMessage) + diff --git a/BTCPayServer/Views/Server/Policies.cshtml b/BTCPayServer/Views/Server/Policies.cshtml index 69767f659..69f072da2 100644 --- a/BTCPayServer/Views/Server/Policies.cshtml +++ b/BTCPayServer/Views/Server/Policies.cshtml @@ -5,7 +5,7 @@

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
    diff --git a/BTCPayServer/Views/Server/Rates.cshtml b/BTCPayServer/Views/Server/Rates.cshtml index 61641a7c4..bbf407366 100644 --- a/BTCPayServer/Views/Server/Rates.cshtml +++ b/BTCPayServer/Views/Server/Rates.cshtml @@ -1,11 +1,11 @@ -@model RatesViewModel +@model BTCPayServer.Models.ServerViewModels.RatesViewModel @{ ViewData.SetActivePageAndTitle(ServerNavPages.Rates); }

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
    diff --git a/BTCPayServer/Views/Server/Theme.cshtml b/BTCPayServer/Views/Server/Theme.cshtml index b9f71fae4..2580e0812 100644 --- a/BTCPayServer/Views/Server/Theme.cshtml +++ b/BTCPayServer/Views/Server/Theme.cshtml @@ -5,7 +5,7 @@

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", TempData["StatusMessage"]) +
    diff --git a/BTCPayServer/Views/Server/User.cshtml b/BTCPayServer/Views/Server/User.cshtml index 47f4225ac..93039fe77 100644 --- a/BTCPayServer/Views/Server/User.cshtml +++ b/BTCPayServer/Views/Server/User.cshtml @@ -5,7 +5,7 @@

    Modify User - @Model.Email

    -@Html.Partial("_StatusMessage", Model.StatusMessage) +
    diff --git a/BTCPayServer/Views/Shared/Confirm.cshtml b/BTCPayServer/Views/Shared/Confirm.cshtml index eb0c54855..4a44c7cce 100644 --- a/BTCPayServer/Views/Shared/Confirm.cshtml +++ b/BTCPayServer/Views/Shared/Confirm.cshtml @@ -15,7 +15,7 @@
    - +
    diff --git a/BTCPayServer/Views/Shared/Error.cshtml b/BTCPayServer/Views/Shared/Error.cshtml deleted file mode 100644 index 086796acf..000000000 --- a/BTCPayServer/Views/Shared/Error.cshtml +++ /dev/null @@ -1,22 +0,0 @@ -@model ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

    Error.

    -

    An error occurred while processing your request.

    - -@if(Model.ShowRequestId) -{ -

    - Request ID: @Model.RequestId -

    -} - -

    Development Mode

    -

    - Swapping to Development environment will display more detailed information about the error that occurred. -

    -

    - Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. -

    diff --git a/BTCPayServer/Views/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml index 69ae060bf..2af9e84cd 100644 --- a/BTCPayServer/Views/Shared/_Layout.cshtml +++ b/BTCPayServer/Views/Shared/_Layout.cshtml @@ -4,6 +4,7 @@ @inject BTCPayServer.Services.BTCPayServerEnvironment env @inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard @inject BTCPayServer.HostedServices.CssThemeManager themeManager + @addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers @@ -18,8 +19,8 @@ BTCPay Server @* CSS *@ - - + + @@ -30,7 +31,7 @@ @{ - if (ViewBag.AlwaysShrinkNavBar == null) + if(ViewBag.AlwaysShrinkNavBar == null) { ViewBag.AlwaysShrinkNavBar = true; } @@ -42,7 +43,7 @@
    - @if (env.NetworkType != NBitcoin.NetworkType.Mainnet) + @if(env.NetworkType != NBitcoin.NetworkType.Mainnet) { @env.NetworkType.ToString() } @@ -52,9 +53,9 @@
    - - - - - - - - - - @foreach (var token in Model.Tokens) - { - - - - - - } - -
    LabelSINFacadeActions
    @token.Label@token.SIN@token.Facade -
    - - -
    -
    + +

    Access token

    +
    +
    +

    Authorize a public key to access Bitpay compatible Invoice API (More information)

    +
    +
    +
    +
    + Create a new token + + + + + + + + + + + @foreach(var token in Model.Tokens) + { + + + + + + + } + +
    LabelSINFacadeActions
    @token.Label@token.SIN@token.Facade +
    + + +
    +
    +
    +
    + +

    Legacy API Keys

    +
    +
    +

    Alternatively, you can use the invoice API by including the following HTTP Header in your requests:
    Authorization: Basic @Model.EncodedApiKey

    +
    +
    + +
    +
    +
    +
    + + +
    + +
    +
    +
    diff --git a/BTCPayServer/Views/Stores/Rates.cshtml b/BTCPayServer/Views/Stores/Rates.cshtml new file mode 100644 index 000000000..b7fd908d4 --- /dev/null +++ b/BTCPayServer/Views/Stores/Rates.cshtml @@ -0,0 +1,161 @@ +@model BTCPayServer.Models.StoreViewModels.RatesViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["Title"] = "Rates"; + ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Rates); +} + +

    @ViewData["Title"]

    + + +
    +
    +
    +
    +
    +
    +
    +
    + @if(Model.ShowScripting) + { +
    +
    Scripting
    + Rate script allows you to express precisely how you want to calculate rates for currency pairs. +

    + Supported exchanges are: + @for(int i = 0; i < Model.AvailableExchanges.Length; i++) + { + @Model.AvailableExchanges[i].Name@(i == Model.AvailableExchanges.Length - 1 ? "" : ",") + } +

    +

    Click here for more information

    +
    + } + @if(Model.TestRateRules != null) + { +
    +
    Test results:
    + + + @foreach(var result in Model.TestRateRules) + { + + @if(result.Error) + { + + } + else + { + + } + + + } + +
    @result.CurrencyPair @result.CurrencyPair@result.Rule
    +
    + } + @if(Model.ShowScripting) + { +
    +

    + The script language is composed of several rules composed of a currency pair and a mathematic expression. + The example below will use gdax for both LTC_USD and BTC_USD pairs. +

    +
    +                    
    +                            LTC_USD = gdax(LTC_USD);
    +                            BTC_USD = gdax(BTC_USD);
    +                        
    +                    
    +

    However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule X_X which will match any currency pair. The following example will use gdax for getting the rate of any currency pair.

    +
    +                    
    +                            X_X = gdax(X_X);
    +                        
    +                    
    +

    However, gdax does not support the BTC_CAD pair. For this reason you can add a rule mapping all X_CAD to quadrigacx, a Canadian exchange.

    +
    +                    
    +                            X_CAD = quadrigacx(X_CAD);
    +                            X_X = gdax(X_X);
    +                        
    +                    
    +

    A given currency pair match the most specific rule. If two rules are matching and are as specific, the first rule will be chosen.

    +

    + But now, what if you want to support DOGE? The problem with DOGE is that most exchange do not have any pair for it. But bittrex has a DOGE_BTC pair.
    + Luckily, the rule engine allow you to reference rules: +

    +
    +                    
    +                            DOGE_X = bittrex(DOGE_BTC) * BTC_X
    +                            X_CAD = quadrigacx(X_CAD);
    +                            X_X = gdax(X_X);
    +                        
    +                    
    +

    With DOGE_USD will be expanded to bittrex(DOGE_BTC) * gdax(BTC_USD). And DOGE_CAD will be expanded to bittrex(DOGE_BTC) * quadrigacx(BTC_CAD).
    + However, we advise you to write it that way to increase coverage so that DOGE_BTC is also supported:

    +
    +                        
    +                            DOGE_X = DOGE_BTC * BTC_X
    +                            DOGE_BTC = bittrex(DOGE_BTC)
    +                            X_CAD = quadrigacx(X_CAD);
    +                            X_X = gdax(X_X);
    +                        
    +                    
    +

    It is worth noting that the inverses of those pairs are automatically supported as well.
    + It means that the rule USD_DOGE = 1 / DOGE_USD implicitely exists.

    + +
    + + + } + else + { +
    + + + +

    + Current price source is @Model.PreferredExchange. +

    +
    + + } +
    + + + +
    +
    +
    Testing
    + Enter currency pairs which you want to test against your rule (eg. DOGE_USD,DOGE_CAD,BTC_CAD,BTC_USD) +
    + + + + +
    + +
    + + +
    +
    +
    + +@section Scripts { + + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Stores/StoreNavPages.cs b/BTCPayServer/Views/Stores/StoreNavPages.cs index 87c3cb516..14f54c253 100644 --- a/BTCPayServer/Views/Stores/StoreNavPages.cs +++ b/BTCPayServer/Views/Stores/StoreNavPages.cs @@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Stores { public static string ActivePageKey => "ActivePage"; public static string Index => "Index"; + public static string Rates => "Rates"; public static string Checkout => "Checkout experience"; public static string Tokens => "Tokens"; @@ -20,6 +21,7 @@ namespace BTCPayServer.Views.Stores public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout); public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates); public static string PageNavClass(ViewContext viewContext, string page) { diff --git a/BTCPayServer/Views/Stores/StoreUsers.cshtml b/BTCPayServer/Views/Stores/StoreUsers.cshtml index 6c0f04906..6432d95b8 100644 --- a/BTCPayServer/Views/Stores/StoreUsers.cshtml +++ b/BTCPayServer/Views/Stores/StoreUsers.cshtml @@ -6,7 +6,7 @@ }

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
    diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index e35466c79..9041cbe68 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -6,7 +6,7 @@ }

    @ViewData["Title"]

    -@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"]) +
    @@ -34,19 +34,6 @@
    -
    - - - -

    - Current price source is @Model.PreferredExchange. -

    -
    -
    - - - -
    @@ -57,11 +44,17 @@
    +
    + + + +
    diff --git a/BTCPayServer/Views/Stores/Wallet.cshtml b/BTCPayServer/Views/Stores/Wallet.cshtml index 70fbeeee3..60265ca0f 100644 --- a/BTCPayServer/Views/Stores/Wallet.cshtml +++ b/BTCPayServer/Views/Stores/Wallet.cshtml @@ -65,7 +65,7 @@ @section Scripts { diff --git a/BTCPayServer/Views/Stores/_Nav.cshtml b/BTCPayServer/Views/Stores/_Nav.cshtml index 255dcb30d..369f388f3 100644 --- a/BTCPayServer/Views/Stores/_Nav.cshtml +++ b/BTCPayServer/Views/Stores/_Nav.cshtml @@ -3,6 +3,7 @@