diff --git a/.circleci/config.yml b/.circleci/config.yml index 5df734e32..429591bbf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: fast_tests: machine: - image: ubuntu-2004:202111-02 + image: ubuntu-2004:2024.11.1 steps: - checkout - run: @@ -10,7 +10,7 @@ jobs: cd .circleci && ./run-tests.sh "Fast=Fast|ThirdParty=ThirdParty" && ./can-build.sh selenium_tests: machine: - image: ubuntu-2004:202111-02 + image: ubuntu-2004:2024.11.1 steps: - checkout - run: @@ -18,7 +18,7 @@ jobs: cd .circleci && ./run-tests.sh "Selenium=Selenium" integration_tests: machine: - image: ubuntu-2004:202111-02 + image: ubuntu-2004:2024.11.1 steps: - checkout - run: @@ -26,7 +26,7 @@ jobs: cd .circleci && ./run-tests.sh "Integration=Integration" trigger_docs_build: machine: - image: ubuntu-2004:202111-02 + image: ubuntu-2004:2024.11.1 steps: - run: command: | diff --git a/.circleci/run-tests.sh b/.circleci/run-tests.sh index 8f37a38cd..acd577e84 100755 --- a/.circleci/run-tests.sh +++ b/.circleci/run-tests.sh @@ -2,8 +2,8 @@ set -e cd ../BTCPayServer.Tests -docker-compose -v -docker-compose -f "docker-compose.altcoins.yml" down --v +docker-compose --version +docker-compose -f "docker-compose.altcoins.yml" down -v # For some reason, docker-compose pull fails time to time, so we try several times n=0 diff --git a/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj b/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj index e1ecd781e..030613b3a 100644 --- a/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj +++ b/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj @@ -32,8 +32,8 @@ - - + + diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index fbcd65a27..30c047653 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/BTCPayServer.Client/BTCPayServerClient.StoreUsers.cs b/BTCPayServer.Client/BTCPayServerClient.StoreUsers.cs index 116cd6be6..2311418c7 100644 --- a/BTCPayServer.Client/BTCPayServerClient.StoreUsers.cs +++ b/BTCPayServer.Client/BTCPayServerClient.StoreUsers.cs @@ -29,4 +29,10 @@ public partial class BTCPayServerClient if (request == null) throw new ArgumentNullException(nameof(request)); await SendHttpRequest($"api/v1/stores/{storeId}/users", request, HttpMethod.Post, token); } + + public virtual async Task UpdateStoreUser(string storeId, string userId, StoreUserData request, CancellationToken token = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + await SendHttpRequest($"api/v1/stores/{storeId}/users/{userId}", request, HttpMethod.Put, token); + } } diff --git a/BTCPayServer.Client/Models/StoreData.cs b/BTCPayServer.Client/Models/StoreData.cs index ca6459e69..9f2e2b728 100644 --- a/BTCPayServer.Client/Models/StoreData.cs +++ b/BTCPayServer.Client/Models/StoreData.cs @@ -17,7 +17,25 @@ namespace BTCPayServer.Client.Models /// public string UserId { get; set; } + /// + /// the store role of the user + /// public string Role { get; set; } + + /// + /// the email AND username of the user + /// + public string Email { get; set; } + + /// + /// the name of the user + /// + public string Name { get; set; } + + /// + /// the image url of the user + /// + public string ImageUrl { get; set; } } public class RoleData diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index 023f30477..ac003c2de 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -2,7 +2,7 @@ - + diff --git a/BTCPayServer.Common/CustomThreadPool.cs b/BTCPayServer.Common/CustomThreadPool.cs deleted file mode 100644 index 8117663f1..000000000 --- a/BTCPayServer.Common/CustomThreadPool.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace BTCPayServer -{ - public class CustomThreadPool : IDisposable - { - readonly CancellationTokenSource _Cancel = new CancellationTokenSource(); - readonly TaskCompletionSource _Exited; - int _ExitedCount = 0; - readonly Thread[] _Threads; - Exception _UnhandledException; - readonly BlockingCollection<(Action, TaskCompletionSource)> _Actions = new BlockingCollection<(Action, TaskCompletionSource)>(new ConcurrentQueue<(Action, TaskCompletionSource)>()); - - public CustomThreadPool(int threadCount, string threadName) - { - if (threadCount <= 0) - throw new ArgumentOutOfRangeException(nameof(threadCount)); - _Exited = new TaskCompletionSource(); - _Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray(); - foreach (var t in _Threads) - t.Start(); - } - - public void Do(Action act) - { - DoAsync(act).GetAwaiter().GetResult(); - } - - public T Do(Func act) - { - return DoAsync(act).GetAwaiter().GetResult(); - } - - public async Task DoAsync(Func act) - { - TaskCompletionSource done = new TaskCompletionSource(); - _Actions.Add((() => - { - try - { - done.TrySetResult(act()); - } - catch (Exception ex) { done.TrySetException(ex); } - } - , done)); - return (T)(await done.Task.ConfigureAwait(false)); - } - - public Task DoAsync(Action act) - { - return DoAsync(() => - { - act(); - return null; - }); - } - - void RunLoop() - { - try - { - foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token)) - { - act.Item1(); - } - } - catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { } - catch (Exception ex) - { - _Cancel.Cancel(); - _UnhandledException = ex; - } - if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length) - { - foreach (var action in _Actions) - { - try - { - action.Item2.TrySetCanceled(); - } - catch { } - } - _Exited.TrySetResult(true); - } - } - - public void Dispose() - { - _Cancel.Cancel(); - _Exited.Task.GetAwaiter().GetResult(); - } - } -} diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 254de870f..22f3a30f2 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -3,11 +3,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 77e7ca7f9..475d2114f 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -6,9 +6,10 @@ - + - + + diff --git a/BTCPayServer.Rating/Providers/BareBitcoinRateProvider.cs b/BTCPayServer.Rating/Providers/BareBitcoinRateProvider.cs new file mode 100644 index 000000000..6ca0d5239 --- /dev/null +++ b/BTCPayServer.Rating/Providers/BareBitcoinRateProvider.cs @@ -0,0 +1,39 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Rates +{ + public class BareBitcoinRateProvider : IRateProvider + { + private readonly HttpClient _httpClient; + + public RateSourceInfo RateSourceInfo => new("barebitcoin", "Bare Bitcoin", "https://api.bb.no/price"); + + public BareBitcoinRateProvider(HttpClient httpClient) + { + _httpClient = httpClient ?? new HttpClient(); + } + + public async Task GetRatesAsync(CancellationToken cancellationToken) + { + using var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken); + response.EnsureSuccessStatusCode(); + + var jobj = await response.Content.ReadAsAsync(cancellationToken); + + // Extract market and otc prices + var market = jobj["market"].Value(); + var buy = jobj["buy"].Value(); + var sell = jobj["sell"].Value(); + + // Create currency pair for BTC/NOK + var pair = new CurrencyPair("BTC", "NOK"); + + // Return single pair rate with sell/buy as bid/ask + return new[] { new PairRate(pair, new BidAsk(sell, buy)) }; + } + } +} \ No newline at end of file diff --git a/BTCPayServer.Rating/Providers/BitmyntRateProvider.cs b/BTCPayServer.Rating/Providers/BitmyntRateProvider.cs new file mode 100644 index 000000000..fec24f906 --- /dev/null +++ b/BTCPayServer.Rating/Providers/BitmyntRateProvider.cs @@ -0,0 +1,39 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Rates +{ + public class BitmyntRateProvider : IRateProvider + { + private readonly HttpClient _httpClient; + + public RateSourceInfo RateSourceInfo => new("bitmynt", "Bitmynt", "https://ny.bitmynt.no/data/rates.json"); + + public BitmyntRateProvider(HttpClient httpClient) + { + _httpClient = httpClient ?? new HttpClient(); + } + + public async Task GetRatesAsync(CancellationToken cancellationToken) + { + using var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken); + response.EnsureSuccessStatusCode(); + + var jobj = await response.Content.ReadAsAsync(cancellationToken); + + // Extract bid and ask prices from current_rate object + var currentRate = jobj["current_rate"]; + var bid = currentRate["bid"].Value(); + var ask = currentRate["ask"].Value(); + + // Create currency pair for BTC/NOK + var pair = new CurrencyPair("BTC", "NOK"); + + // Return single pair rate with bid/ask + return new[] { new PairRate(pair, new BidAsk(bid, ask)) }; + } + } +} \ No newline at end of file diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index 0938bcc0e..02e25ac6d 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -93,16 +93,16 @@ namespace BTCPayServer.Tests Assert.IsType(response); // Get enabled state from settings - response = controller.WalletSettings(user.StoreId, cryptoCode).GetAwaiter().GetResult(); + response = await controller.WalletSettings(user.StoreId, cryptoCode); var onchainSettingsModel = (WalletSettingsViewModel)Assert.IsType(response).Model; Assert.NotNull(onchainSettingsModel?.DerivationScheme); Assert.True(onchainSettingsModel.Enabled); // Disable wallet onchainSettingsModel.Enabled = false; - response = controller.UpdateWalletSettings(onchainSettingsModel).GetAwaiter().GetResult(); + response = await controller.UpdateWalletSettings(onchainSettingsModel); Assert.IsType(response); - response = controller.WalletSettings(user.StoreId, cryptoCode).GetAwaiter().GetResult(); + response = await controller.WalletSettings(user.StoreId, cryptoCode); onchainSettingsModel = (WalletSettingsViewModel)Assert.IsType(response).Model; Assert.NotNull(onchainSettingsModel?.DerivationScheme); Assert.False(onchainSettingsModel.Enabled); @@ -124,7 +124,7 @@ namespace BTCPayServer.Tests Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); // Removing the derivation scheme, should redirect to store page - response = controller.ConfirmDeleteWallet(user.StoreId, cryptoCode).GetAwaiter().GetResult(); + response = await controller.ConfirmDeleteWallet(user.StoreId, cryptoCode); Assert.IsType(response); // Setting it again should show the confirmation page @@ -174,7 +174,7 @@ namespace BTCPayServer.Tests Assert.Equal("ElectrumFile", settingsVm.Source); // Now let's check that no data has been lost in the process - var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult(); + var store = await tester.PayTester.StoreRepository.FindStore(storeId); var handlers = tester.PayTester.GetService(); var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC"); var onchainBTC = store.GetPaymentMethodConfig(pmi, handlers); @@ -206,7 +206,7 @@ namespace BTCPayServer.Tests Assert.Equal("paid", invoice.Status); }); var wallet = tester.PayTester.GetController(); - var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, + var psbt = await wallet.CreatePSBT(btcNetwork, onchainBTC, new WalletSendModel() { Outputs = new List @@ -219,7 +219,7 @@ namespace BTCPayServer.Tests } }, FeeSatoshiPerByte = 1 - }, default).GetAwaiter().GetResult(); + }, default); Assert.NotNull(psbt); @@ -440,136 +440,135 @@ namespace BTCPayServer.Tests [Trait("Altcoins", "Altcoins")] public async Task CanPayWithTwoCurrencies() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + + tester.ActivateLTC(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + // First we try payment with a merchant having only BTC + var invoice = await user.BitPay.CreateInvoiceAsync( + new Invoice + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + var cashCow = tester.ExplorerNode; + await cashCow.GenerateAsync(2); // get some money in case + var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + var firstPayment = Money.Coins(0.04m); + await cashCow.SendToAddressAsync(invoiceAddress, firstPayment); + TestUtils.Eventually(() => { - tester.ActivateLTC(); - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - // First we try payment with a merchant having only BTC - var invoice = await user.BitPay.CreateInvoiceAsync( - new Invoice - { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.True(invoice.BtcPaid == firstPayment); + }); - var cashCow = tester.ExplorerNode; - await cashCow.GenerateAsync(2); // get some money in case - var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - var firstPayment = Money.Coins(0.04m); - await cashCow.SendToAddressAsync(invoiceAddress, firstPayment); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.True(invoice.BtcPaid == firstPayment); - }); + Assert.Single(invoice.CryptoInfo); // Only BTC should be presented - Assert.Single(invoice.CryptoInfo); // Only BTC should be presented - - var controller = tester.PayTester.GetController(null); - var checkout = - (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null) - .GetAwaiter().GetResult()).Value; - Assert.Single(checkout.AvailablePaymentMethods); - Assert.Equal("BTC", checkout.PaymentMethodCurrency); - - 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 = await user.BitPay.CreateInvoiceAsync( - new Invoice - { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - cashCow = tester.ExplorerNode; - invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - firstPayment = Money.Coins(0.04m); - await cashCow.SendToAddressAsync(invoiceAddress, firstPayment); - TestLogs.LogInformation("First payment sent to " + invoiceAddress); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.True(invoice.BtcPaid == firstPayment); - }); - - cashCow = tester.LTCExplorerNode; - var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC"); - Assert.NotNull(ltcCryptoInfo); - invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); - var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture)); - await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money... - await cashCow.SendToAddressAsync(invoiceAddress, secondPayment); - TestLogs.LogInformation("Second payment sent to " + invoiceAddress); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal(Money.Zero, invoice.BtcDue); - var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC"); - Assert.Equal(Money.Zero, ltcPaid.Due); - Assert.Equal(secondPayment, ltcPaid.CryptoPaid); - Assert.Equal("paid", invoice.Status); - Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); - }); - - controller = tester.PayTester.GetController(null); - checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC") + var controller = tester.PayTester.GetController(null); + var checkout = + (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null) .GetAwaiter().GetResult()).Value; - Assert.Equal(2, checkout.AvailablePaymentMethods.Count); - Assert.Equal("LTC", checkout.PaymentMethodCurrency); + Assert.Single(checkout.AvailablePaymentMethods); + Assert.Equal("BTC", checkout.PaymentMethodCurrency); - 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")); + 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")); + ////////////////////// - // Check if we can disable LTC - invoice = await user.BitPay.CreateInvoiceAsync( - new Invoice + // Retry now with LTC enabled + user.RegisterDerivationScheme("LTC"); + invoice = await user.BitPay.CreateInvoiceAsync( + new Invoice + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + cashCow = tester.ExplorerNode; + invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + firstPayment = Money.Coins(0.04m); + await cashCow.SendToAddressAsync(invoiceAddress, firstPayment); + TestLogs.LogInformation("First payment sent to " + invoiceAddress); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.True(invoice.BtcPaid == firstPayment); + }); + + cashCow = tester.LTCExplorerNode; + var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC"); + Assert.NotNull(ltcCryptoInfo); + invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); + var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture)); + await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money... + await cashCow.SendToAddressAsync(invoiceAddress, secondPayment); + TestLogs.LogInformation("Second payment sent to " + invoiceAddress); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(Money.Zero, invoice.BtcDue); + var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC"); + Assert.Equal(Money.Zero, ltcPaid.Due); + Assert.Equal(secondPayment, ltcPaid.CryptoPaid); + Assert.Equal("paid", invoice.Status); + Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); + }); + + controller = tester.PayTester.GetController(null); + checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC") + .GetAwaiter().GetResult()).Value; + Assert.Equal(2, checkout.AvailablePaymentMethods.Count); + Assert.Equal("LTC", checkout.PaymentMethodCurrency); + + 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")); + + // Check if we can disable LTC + invoice = await user.BitPay.CreateInvoiceAsync( + new Invoice + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true, + SupportedTransactionCurrencies = new Dictionary() { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true, - SupportedTransactionCurrencies = new Dictionary() - { - {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} - } - }, Facade.Merchant); + {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} + } + }, Facade.Merchant); - Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC")); - Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC")); - } + Assert.Single(invoice.CryptoInfo, c => c.CryptoCode == "BTC"); + Assert.DoesNotContain(invoice.CryptoInfo, c => c.CryptoCode == "LTC"); } [Fact] @@ -744,7 +743,7 @@ noninventoryitem: invoices = user.BitPay.GetInvoices(); Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem"))); var inventoryItemInvoice = - Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem"))); + Assert.Single(invoices, invoice => invoice.ItemCode.Equals("inventoryitem")); Assert.NotNull(inventoryItemInvoice); //let's mark the inventoryitem invoice as invalid, this should return the item to back in stock diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index f35846a09..29249d126 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -39,13 +39,14 @@ - - + + + - - + + all runtime; build; native; contentfiles; analyzers diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs index a0b7c764e..b3dc858aa 100644 --- a/BTCPayServer.Tests/CrowdfundTests.cs +++ b/BTCPayServer.Tests/CrowdfundTests.cs @@ -263,7 +263,7 @@ namespace BTCPayServer.Tests }); TestLogs.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged"); - var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult(); + var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version); Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags); @@ -281,7 +281,7 @@ namespace BTCPayServer.Tests TransactionSpeed = "high", FullNotifications = true }, Facade.Merchant); - invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult(); + invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); Assert.DoesNotContain(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags); TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted"); diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index 52fa208c6..7cc24cccb 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0.101-bookworm-slim AS builder +FROM mcr.microsoft.com/dotnet/sdk:8.0.404-bookworm-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \ && rm -rf /var/lib/apt/lists/* diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 36505cf63..09dcf1baf 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -174,10 +174,10 @@ namespace BTCPayServer.Tests public void CanRandomizeByPercentage() { var generated = Enumerable.Range(0, 1000).Select(_ => MempoolSpaceFeeProvider.RandomizeByPercentage(100.0m, 10.0m)).ToArray(); - Assert.Empty(generated.Where(g => g < 90m)); - Assert.Empty(generated.Where(g => g > 110m)); - Assert.NotEmpty(generated.Where(g => g < 91m)); - Assert.NotEmpty(generated.Where(g => g > 109m)); + Assert.DoesNotContain(generated, g => g < 90m); + Assert.DoesNotContain(generated, g => g > 110m); + Assert.Contains(generated, g => g < 91m); + Assert.Contains(generated, g => g > 109m); } private void CanParseDecimalsCore(string str, decimal expected) @@ -793,9 +793,9 @@ namespace BTCPayServer.Tests }), BTCPayLogs); await tor.Refresh(); - Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer)); - Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P)); - Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC)); + Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.BTCPayServer); + Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.P2P); + Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.RPC); Assert.True(tor.Services.Count(t => t.ServiceType == TorServiceType.Other) > 1); tor = new TorServices(CreateNetworkProvider(ChainName.Regtest), @@ -806,24 +806,24 @@ namespace BTCPayServer.Tests }), BTCPayLogs); await Task.WhenAll(tor.StartAsync(CancellationToken.None)); - var btcpayS = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer)); + var btcpayS = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.BTCPayServer); Assert.Null(btcpayS.Network); Assert.Equal("host.onion", btcpayS.OnionHost); Assert.Equal(80, btcpayS.VirtualPort); - var p2p = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P)); + var p2p = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.P2P); Assert.NotNull(p2p.Network); Assert.Equal("BTC", p2p.Network.CryptoCode); Assert.Equal("host2.onion", p2p.OnionHost); Assert.Equal(81, p2p.VirtualPort); - var rpc = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC)); + var rpc = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.RPC); Assert.NotNull(p2p.Network); Assert.Equal("BTC", rpc.Network.CryptoCode); Assert.Equal("host3.onion", rpc.OnionHost); Assert.Equal(82, rpc.VirtualPort); - var unknown = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.Other)); + var unknown = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.Other); Assert.Null(unknown.Network); Assert.Equal("host4.onion", unknown.OnionHost); Assert.Equal(83, unknown.VirtualPort); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index bfebb99b7..ce12b8e57 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -3867,7 +3867,7 @@ namespace BTCPayServer.Tests void VerifyLightning(GenericPaymentMethodData[] methods) { - var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-LN")); + var m = Assert.Single(methods, m => m.PaymentMethodId == "BTC-LN"); Assert.Equal("Internal Node", m.Config["internalNodeRef"].Value()); } @@ -3879,7 +3879,7 @@ namespace BTCPayServer.Tests void VerifyOnChain(GenericPaymentMethodData[] dictionary) { - var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-CHAIN")); + var m = Assert.Single(methods, m => m.PaymentMethodId == "BTC-CHAIN"); var paymentMethodBaseData = Assert.IsType(m.Config); Assert.Equal(wallet.Config.AccountDerivation, paymentMethodBaseData["accountDerivation"].Value()); } @@ -3985,7 +3985,12 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); await user.GrantAccessAsync(true); - var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings); + var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings, Policies.CanModifyProfile); + await client.UpdateCurrentUser(new UpdateApplicationUserRequest + { + Name = "The Admin", + ImageUrl = "avatar.jpg" + }); var roles = await client.GetServerRoles(); Assert.Equal(4, roles.Count); @@ -3999,6 +4004,9 @@ namespace BTCPayServer.Tests var storeUser = Assert.Single(users); Assert.Equal(user.UserId, storeUser.UserId); Assert.Equal(ownerRole.Id, storeUser.Role); + Assert.Equal(user.Email, storeUser.Email); + Assert.Equal("The Admin", storeUser.Name); + Assert.Equal("avatar.jpg", storeUser.ImageUrl); var manager = tester.NewAccount(); await manager.GrantAccessAsync(); var employee = tester.NewAccount(); @@ -4029,7 +4037,14 @@ namespace BTCPayServer.Tests // add users to store await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = manager.UserId }); await client.AddStoreUser(user.StoreId, new StoreUserData { Role = employeeRole.Id, UserId = employee.UserId }); - await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.UserId }); + + // add with email + await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.Email }); + + // test unknown user + await AssertAPIError("user-not-found", async () => await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = "unknown" })); + await AssertAPIError("user-not-found", async () => await client.UpdateStoreUser(user.StoreId, "unknown", new StoreUserData { Role = ownerRole.Id })); + await AssertAPIError("user-not-found", async () => await client.RemoveStoreUser(user.StoreId, "unknown")); //test no access to api for employee await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId)); @@ -4050,9 +4065,14 @@ namespace BTCPayServer.Tests await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId)); // updates + await client.UpdateStoreUser(user.StoreId, employee.UserId, new StoreUserData { Role = ownerRole.Id }); + await employeeClient.GetStore(user.StoreId); + + // remove await client.RemoveStoreUser(user.StoreId, employee.UserId); await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId)); + // test duplicate add await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }); await AssertAPIError("duplicate-store-user-role", async () => await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId })); @@ -4412,8 +4432,8 @@ namespace BTCPayServer.Tests payouts = await adminClient.GetStorePayouts(admin.StoreId); Assert.Equal(3, payouts.Length); - Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval)); - Assert.Empty(payouts.Where(data => data.PayoutAmount is null)); + Assert.DoesNotContain(payouts, data => data.State == PayoutState.AwaitingApproval); + Assert.DoesNotContain(payouts, data => data.PayoutAmount is null); Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")); @@ -4456,7 +4476,7 @@ namespace BTCPayServer.Tests { Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count()); payouts = await adminClient.GetStorePayouts(admin.StoreId); - Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress)); + Assert.Single(payouts, data => data.State == PayoutState.InProgress); }); uint256 txid = null; @@ -4470,7 +4490,7 @@ namespace BTCPayServer.Tests { Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count()); payouts = await adminClient.GetStorePayouts(admin.StoreId); - Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress)); + Assert.DoesNotContain(payouts, data => data.State != PayoutState.InProgress); }); // settings that were added later @@ -4536,7 +4556,7 @@ namespace BTCPayServer.Tests payouts = await adminClient.GetStorePayouts(admin.StoreId); try { - Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id)); + Assert.Single(payouts, data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id); } catch (SingleException) { @@ -4580,7 +4600,7 @@ namespace BTCPayServer.Tests await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); payouts = await adminClient.GetStorePayouts(admin.StoreId); - Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id)); + Assert.Single(payouts, data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id); beforeHookTcs = new TaskCompletionSource(); afterHookTcs = new TaskCompletionSource(); @@ -4613,7 +4633,7 @@ namespace BTCPayServer.Tests await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); payouts = await adminClient.GetStorePayouts(admin.StoreId); - Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress)); + Assert.DoesNotContain(payouts, data => data.State != PayoutState.InProgress); } @@ -4690,11 +4710,11 @@ namespace BTCPayServer.Tests await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = "newtype", Id = test1.Id }); testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test); - Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value() == "lol")); - Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData["testData"]?.Value() == "test1")); + Assert.Single(testObj.Links, l => l.Id == "test1" && l.LinkData["testData"]?.Value() == "lol"); + Assert.Single(testObj.Links, l => l.Id == "test1" && l.ObjectData["testData"]?.Value() == "test1"); testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test, false); - Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value() == "lol")); - Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData is null)); + Assert.Single(testObj.Links, l => l.Id == "test1" && l.LinkData["testData"]?.Value() == "lol"); + Assert.Single(testObj.Links, l => l.Id == "test1" && l.ObjectData is null); async Task TestWalletRepository() { diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index fad621877..29ef3b362 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -385,10 +385,6 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456"); s.ClickPagePrimary(); Assert.Contains("Account successfully created.", s.FindAlertMessage().Text); - - s.Driver.FindElement(By.Id("Email")).SendKeys(usr); - s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); - s.Driver.FindElement(By.Id("LoginButton")).Click(); // We should be logged in now s.GoToHome(); diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index 4c7c1881b..948073a98 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -208,6 +208,18 @@ namespace BTCPayServer.Tests e => e.CurrencyPair == new CurrencyPair("BTC", "LBP") && e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 LBP (I hope) } + else if (name == "bitmynt") + { + Assert.Contains(exchangeRates.ByExchange[name], + e => e.CurrencyPair == new CurrencyPair("BTC", "NOK") && + e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NOK + } + else if (name == "barebitcoin") + { + Assert.Contains(exchangeRates.ByExchange[name], + e => e.CurrencyPair == new CurrencyPair("BTC", "NOK") && + e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NOK + } else { if (name == "kraken") @@ -234,7 +246,7 @@ namespace BTCPayServer.Tests } // Kraken emit one request only after first GetRates - factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult(); + await factory.Providers["kraken"].GetRatesAsync(default); var p = new KrakenExchangeRateProvider(); var rates = await p.GetRatesAsync(default); @@ -339,7 +351,7 @@ retry: } [Fact] - public void CanSolveTheDogesRatesOnKraken() + public async Task CanSolveTheDogesRatesOnKraken() { var factory = FastTests.CreateBTCPayRateFactory(); var fetcher = new RateFetcher(factory); @@ -347,7 +359,7 @@ retry: Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule)); foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD" }) { - var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default).GetAwaiter().GetResult(); + var result = await fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default); Assert.NotNull(result.BidAsk); Assert.Empty(result.Errors); } @@ -601,7 +613,7 @@ retry: foreach (var rate in rates) { - Assert.Single(rates.Where(r => r == rate)); + Assert.Single(rates, r => r == rate); } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6fbe898be..3fa4ffcb6 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -194,7 +194,7 @@ namespace BTCPayServer.Tests [Fact] [Trait("Integration", "Integration")] - public async void CanStoreArbitrarySettingsWithStore() + public async Task CanStoreArbitrarySettingsWithStore() { using var tester = CreateServerTester(); await tester.StartAsync(); @@ -446,25 +446,25 @@ namespace BTCPayServer.Tests Assert.IsType(storeResponse); Assert.IsType(storeController.SetupLightningNode(user.StoreId, "BTC")); - storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel + await storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel { ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true", SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :( - }, "test", "BTC").GetAwaiter().GetResult(); + }, "test", "BTC"); Assert.False(storeController.TempData.ContainsKey(WellKnownTempData.ErrorMessage)); storeController.TempData.Clear(); Assert.True(storeController.ModelState.IsValid); - Assert.IsType(storeController.SetupLightningNode(user.StoreId, + Assert.IsType(await storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel { ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true" - }, "save", "BTC").GetAwaiter().GetResult()); + }, "save", "BTC")); // Make sure old connection string format does not work - Assert.IsType(storeController.SetupLightningNode(user.StoreId, + Assert.IsType(await storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri }, - "save", "BTC").GetAwaiter().GetResult()); + "save", "BTC")); storeResponse = storeController.LightningSettings(user.StoreId, "BTC"); var storeVm = @@ -1099,7 +1099,7 @@ namespace BTCPayServer.Tests [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] - public async void CheckCORSSetOnBitpayAPI() + public async Task CheckCORSSetOnBitpayAPI() { using var tester = CreateServerTester(); await tester.StartAsync(); @@ -1143,9 +1143,8 @@ namespace BTCPayServer.Tests // Test request pairing code client side var storeController = user.GetController(); - storeController - .CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId }) - .GetAwaiter().GetResult(); + await storeController + .CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId }); Assert.NotNull(storeController.GeneratedPairingCode); @@ -1169,17 +1168,17 @@ namespace BTCPayServer.Tests // Can generate API Key var repo = tester.PayTester.GetService(); - Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); - Assert.IsType(user.GetController() - .GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); + Assert.Empty(await repo.GetLegacyAPIKeys(user.StoreId)); + Assert.IsType(await user.GetController() + .GenerateAPIKey(user.StoreId)); - var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + var apiKey = Assert.Single(await repo.GetLegacyAPIKeys(user.StoreId)); /////// // Generating a new one remove the previous - Assert.IsType(user.GetController() - .GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); - var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.IsType(await user.GetController() + .GenerateAPIKey(user.StoreId)); + var apiKey2 = Assert.Single(await repo.GetLegacyAPIKeys(user.StoreId)); Assert.NotEqual(apiKey, apiKey2); //////// @@ -1193,7 +1192,7 @@ namespace BTCPayServer.Tests 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(); + var result = await client.SendAsync(message); result.EnsureSuccessStatusCode(); ///////////////////// @@ -1207,7 +1206,7 @@ namespace BTCPayServer.Tests mess.Headers.Add("x-identity", "04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99"); mess.Method = HttpMethod.Get; - result = client.SendAsync(mess).GetAwaiter().GetResult(); + result = await client.SendAsync(mess); Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode); // @@ -1539,7 +1538,7 @@ namespace BTCPayServer.Tests // We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice var vm = await user.GetController().CheckoutAppearance().AssertViewModelAsync(); Assert.Equal(2, vm.PaymentMethodCriteria.Count); - var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); + var criteria = Assert.Single(vm.PaymentMethodCriteria, m => m.PaymentMethod == btcMethod.ToString()); Assert.Equal(btcMethod.ToString(), criteria.PaymentMethod); criteria.Value = "5 USD"; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; @@ -2175,19 +2174,19 @@ namespace BTCPayServer.Tests Assert.Equal(0, invoice.CryptoInfo[0].TxCount); Assert.True(invoice.MinerFees.ContainsKey("BTC")); Assert.Contains(Math.Round(invoice.MinerFees["BTC"].SatoshiPerBytes), new[] { 100.0m, 20.0m }); - TestUtils.Eventually(() => + await TestUtils.EventuallyAsync(async () => { - var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() + var textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() { StoreId = new[] { user.StoreId }, TextSearch = invoice.OrderId - }).GetAwaiter().GetResult(); + }); Assert.Single(textSearchResult); - textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() + textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() { StoreId = new[] { user.StoreId }, TextSearch = invoice.Id - }).GetAwaiter().GetResult(); + }); Assert.Single(textSearchResult); }); @@ -2215,11 +2214,11 @@ namespace BTCPayServer.Tests Assert.True(IsMapped(invoice, ctx)); cashCow.SendToAddress(invoiceAddress, firstPayment); - var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); + var invoiceEntity = await repo.GetInvoice(invoice.Id, true); Money secondPayment = Money.Zero; - TestUtils.Eventually(() => + await TestUtils.EventuallyAsync(async () => { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("new", localInvoice.Status); @@ -2231,7 +2230,7 @@ namespace BTCPayServer.Tests Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(localInvoice, ctx)); - invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); + invoiceEntity = await repo.GetInvoice(invoice.Id, true); invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network); secondPayment = localInvoice.BtcDue; }); @@ -2274,18 +2273,18 @@ namespace BTCPayServer.Tests var txId = await cashCow.SendToAddressAsync(invoiceAddress, invoice.BtcDue + Money.Coins(1)); - TestUtils.Eventually(() => + await TestUtils.EventuallyAsync(async () => { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paid", localInvoice.Status); Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); - var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() + var textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() { StoreId = new[] { user.StoreId }, TextSearch = txId.ToString() - }).GetAwaiter().GetResult(); + }); Assert.Single(textSearchResult); }); @@ -2421,7 +2420,7 @@ namespace BTCPayServer.Tests [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] - public async void CheckOnionlocationForNonOnionHtmlRequests() + public async Task CheckOnionlocationForNonOnionHtmlRequests() { using var tester = CreateServerTester(); await tester.StartAsync(); @@ -3048,7 +3047,7 @@ namespace BTCPayServer.Tests [Fact] [Trait("Integration", "Integration")] - public async void CanUseLocalProviderFiles() + public async Task CanUseLocalProviderFiles() { using var tester = CreateServerTester(); await tester.StartAsync(); @@ -3273,7 +3272,7 @@ namespace BTCPayServer.Tests var fullyPaidIndex = report.GetIndex("FullyPaid"); var completedIndex = report.GetIndex("Completed"); var limitIndex = report.GetIndex("Limit"); - var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value() == inv.Id)); + var d = Assert.Single(report.Data, d => d[report.GetIndex("InvoiceId")].Value() == inv.Id); Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]); Assert.Equal(currency, d[currencyIndex].Value()); Assert.Equal(completed, GetAmount(completedIndex, d)); diff --git a/BTCPayServer.Tests/docker-compose.altcoins.yml b/BTCPayServer.Tests/docker-compose.altcoins.yml index a14e4d9fa..cd6792de7 100644 --- a/BTCPayServer.Tests/docker-compose.altcoins.yml +++ b/BTCPayServer.Tests/docker-compose.altcoins.yml @@ -98,7 +98,7 @@ services: custom: nbxplorer: - image: nicolasdorier/nbxplorer:2.5.12 + image: nicolasdorier/nbxplorer:2.5.14 restart: unless-stopped ports: - "32838:32838" diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 9025379fb..57116f468 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -95,7 +95,7 @@ services: custom: nbxplorer: - image: nicolasdorier/nbxplorer:2.5.12 + image: nicolasdorier/nbxplorer:2.5.14 restart: unless-stopped ports: - "32838:32838" diff --git a/BTCPayServer.Tests/xunit.runner.json b/BTCPayServer.Tests/xunit.runner.json index d58dae577..e4f38ad43 100644 --- a/BTCPayServer.Tests/xunit.runner.json +++ b/BTCPayServer.Tests/xunit.runner.json @@ -1,6 +1,5 @@ { "maxParallelThreads": 4, "longRunningTestSeconds": 60, - "diagnosticMessages": true, "methodDisplay": "method" } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 7d4ef66c5..d423e4bc6 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -8,6 +8,19 @@ False False + + + + + false + + + true + + + $(DefineConstants);RAZOR_COMPILE_ON_BUILD + + <_Parameter1>$(GitCommit) @@ -37,17 +50,17 @@ - + - + - - - - + + + + @@ -60,13 +73,14 @@ - - - - - - - + + + + + + + + diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs index 1839d7071..f54da7301 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers.Greenfield { @@ -30,10 +31,10 @@ namespace BTCPayServer.Controllers.Greenfield [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/users")] - public IActionResult GetStoreUsers() + public async Task GetStoreUsers() { var store = HttpContext.GetStoreData(); - return store == null ? StoreNotFound() : Ok(FromModel(store)); + return store == null ? StoreNotFound() : Ok(await FromModel(store)); } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] @@ -41,31 +42,28 @@ namespace BTCPayServer.Controllers.Greenfield public async Task RemoveStoreUser(string storeId, string idOrEmail) { var store = HttpContext.GetStoreData(); - if (store == null) - { - return StoreNotFound(); - } + if (store == null) return StoreNotFound(); - var userId = await _userManager.FindByIdOrEmail(idOrEmail); - if (userId != null && await _storeRepository.RemoveStoreUser(storeId, idOrEmail)) - { - return Ok(); - } - - return this.CreateAPIError(409, "store-user-role-orphaned", "Removing this user would result in the store having no owner."); + var user = await _userManager.FindByIdOrEmail(idOrEmail); + if (user == null) return UserNotFound(); + + return await _storeRepository.RemoveStoreUser(storeId, user.Id) + ? Ok() + : this.CreateAPIError(409, "store-user-role-orphaned", "Removing this user would result in the store having no owner."); } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/stores/{storeId}/users")] - public async Task AddStoreUser(string storeId, StoreUserData request) + [HttpPut("~/api/v1/stores/{storeId}/users/{idOrEmail?}")] + public async Task AddOrUpdateStoreUser(string storeId, StoreUserData request, string idOrEmail = null) { var store = HttpContext.GetStoreData(); - if (store == null) - { - return StoreNotFound(); - } - StoreRoleId roleId = null; + if (store == null) return StoreNotFound(); + var user = await _userManager.FindByIdOrEmail(idOrEmail ?? request.UserId); + if (user == null) return UserNotFound(); + + StoreRoleId roleId = null; if (request.Role is not null) { roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.Role); @@ -76,21 +74,42 @@ namespace BTCPayServer.Controllers.Greenfield if (!ModelState.IsValid) return this.CreateValidationError(ModelState); - if (await _storeRepository.AddStoreUser(storeId, request.UserId, roleId)) - { - return Ok(); - } - - return this.CreateAPIError(409, "duplicate-store-user-role", "The user is already added to the store"); + var result = string.IsNullOrEmpty(idOrEmail) + ? await _storeRepository.AddStoreUser(storeId, user.Id, roleId) + : await _storeRepository.AddOrUpdateStoreUser(storeId, user.Id, roleId); + return result + ? Ok() + : this.CreateAPIError(409, "duplicate-store-user-role", "The user is already added to the store"); } - private IEnumerable FromModel(Data.StoreData data) + private async Task> FromModel(StoreData data) { - return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.StoreRoleId }); + var storeUsers = new List(); + foreach (var storeUser in data.UserStores) + { + var user = await _userManager.FindByIdOrEmail(storeUser.ApplicationUserId); + var blob = user?.GetBlob(); + storeUsers.Add(new StoreUserData + { + UserId = storeUser.ApplicationUserId, + Role = storeUser.StoreRoleId, + Email = user?.Email, + Name = blob?.Name, + ImageUrl = blob?.ImageUrl, + + }); + } + return storeUsers; } + private IActionResult StoreNotFound() { return this.CreateAPIError(404, "store-not-found", "The store was not found"); } + + private IActionResult UserNotFound() + { + return this.CreateAPIError(404, "user-not-found", "The user was not found"); + } } } diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 2a4b898f3..95e2c30b2 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -985,17 +985,22 @@ namespace BTCPayServer.Controllers.Greenfield return GetFromActionResult(await GetController().GetUsers()); } - public override Task> GetStoreUsers(string storeId, + public override async Task> GetStoreUsers(string storeId, CancellationToken token = default) { - return Task.FromResult( - GetFromActionResult>(GetController().GetStoreUsers())); + return GetFromActionResult>(await GetController().GetStoreUsers()); } public override async Task AddStoreUser(string storeId, StoreUserData request, CancellationToken token = default) { - HandleActionResult(await GetController().AddStoreUser(storeId, request)); + HandleActionResult(await GetController().AddOrUpdateStoreUser(storeId, request)); + } + + public override async Task UpdateStoreUser(string storeId, string userId, StoreUserData request, + CancellationToken token = default) + { + HandleActionResult(await GetController().AddOrUpdateStoreUser(storeId, request, userId)); } public override async Task RemoveStoreUser(string storeId, string userId, CancellationToken token = default) diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs index 5e43e201a..cac0fa637 100644 --- a/BTCPayServer/Controllers/UIAccountController.cs +++ b/BTCPayServer/Controllers/UIAccountController.cs @@ -273,7 +273,7 @@ namespace BTCPayServer.Controllers } return new LoginWithFido2ViewModel { - Data = r, + Data = System.Text.Json.JsonSerializer.Serialize(r, r.GetType()), UserId = user.Id, RememberMe = rememberMe }; @@ -385,7 +385,7 @@ namespace BTCPayServer.Controllers try { - if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject())) + if (await _fido2Service.CompleteLogin(viewModel.UserId, System.Text.Json.JsonSerializer.Deserialize(viewModel.Response))) { await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2"); _logger.LogInformation("User {Email} logged in with FIDO2", user.Email); @@ -650,6 +650,7 @@ namespace BTCPayServer.Controllers if (logon) { await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User {Email} logged in", user.Email); return RedirectToLocal(returnUrl); } } @@ -793,7 +794,7 @@ namespace BTCPayServer.Controllers [HttpPost("/login/set-password")] [AllowAnonymous] [ValidateAntiForgeryToken] - public async Task SetPassword(SetPasswordViewModel model) + public async Task SetPassword(SetPasswordViewModel model, string returnUrl = null) { if (!ModelState.IsValid) { @@ -802,9 +803,11 @@ namespace BTCPayServer.Controllers var user = await _userManager.FindByEmailAsync(model.Email); var hasPassword = user != null && await _userManager.HasPasswordAsync(user); - if (!UserService.TryCanLogin(user, out _)) + var needsInitialPassword = user != null && !await _userManager.HasPasswordAsync(user); + // Let unapproved users set a password. Otherwise, don't reveal that the user does not exist. + if (!UserService.TryCanLogin(user, out var message) && !needsInitialPassword || user == null) { - // Don't reveal that the user does not exist + _logger.LogWarning("User {Email} tried to reset password, but failed: {Message}", user?.Email ?? "(NO EMAIL)", message); return RedirectToAction(nameof(Login)); } @@ -818,7 +821,19 @@ namespace BTCPayServer.Controllers ? StringLocalizer["Password successfully set."].Value : StringLocalizer["Account successfully created."].Value }); + if (!hasPassword) await FinalizeInvitationIfApplicable(user); + + // see if we can sign in user after accepting an invitation and setting the password + if (needsInitialPassword && UserService.TryCanLogin(user, out _)) + { + var signInResult = await _signInManager.PasswordSignInAsync(user.Email!, model.Password, true, true); + if (signInResult.Succeeded) + { + _logger.LogInformation("User {Email} logged in", user.Email); + return RedirectToLocal(returnUrl); + } + } return RedirectToAction(nameof(Login)); } diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 905b4f81d..97b56bed9 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -33,7 +33,6 @@ using NBitcoin; using Newtonsoft.Json.Linq; using StoreData = BTCPayServer.Data.StoreData; using Serilog.Filters; -using PeterO.Numbers; using BTCPayServer.Payouts; using Microsoft.Extensions.Localization; diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index f15d3511c..8cf49db72 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -13,7 +13,6 @@ using BTCPayServer.Services.Rates; using NBitcoin; using NBXplorer; using Newtonsoft.Json.Linq; -using static Org.BouncyCastle.Math.EC.ECCurve; namespace BTCPayServer.Data { diff --git a/BTCPayServer/Fido2/Fido2Service.cs b/BTCPayServer/Fido2/Fido2Service.cs index e6e9cd6ae..2a2d64596 100644 --- a/BTCPayServer/Fido2/Fido2Service.cs +++ b/BTCPayServer/Fido2/Fido2Service.cs @@ -11,6 +11,7 @@ using Fido2NetLib.Objects; using Microsoft.EntityFrameworkCore; using NBitcoin; using Newtonsoft.Json.Linq; +using static BTCPayServer.Fido2.Models.Fido2CredentialBlob; namespace BTCPayServer.Fido2 { @@ -45,7 +46,7 @@ namespace BTCPayServer.Fido2 var existingKeys = user.Fido2Credentials .Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2) - .Select(c => c.GetFido2Blob().Descriptor).ToList(); + .Select(c => c.GetFido2Blob().Descriptor?.ToFido2()).ToList(); // 3. Create options var authenticatorSelection = new AuthenticatorSelection @@ -57,14 +58,7 @@ namespace BTCPayServer.Fido2 var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, - UserVerificationIndex = true, - Location = true, - UserVerificationMethod = true, - BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds - { - FAR = float.MaxValue, - FRR = float.MaxValue - }, + UserVerificationMethod = true }; var options = _fido2.RequestNewCredential( @@ -81,7 +75,7 @@ namespace BTCPayServer.Fido2 try { - var attestationResponse = JObject.Parse(data).ToObject(); + var attestationResponse = System.Text.Json.JsonSerializer.Deserialize(data); await using var dbContext = _contextFactory.CreateContext(); var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials) .FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId); @@ -92,14 +86,14 @@ namespace BTCPayServer.Fido2 // 2. Verify and make the credentials var success = - await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true)); + await _fido2.MakeNewCredentialAsync(attestationResponse, options, (args, cancellation) => Task.FromResult(true)); // 3. Store the credentials in db var newCredential = new Fido2Credential() { Name = name, ApplicationUserId = userId }; newCredential.SetBlob(new Fido2CredentialBlob() { - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), + Descriptor = new DescriptorClass(success.Result.CredentialId), PublicKey = success.Result.PublicKey, UserHandle = success.Result.User.Id, SignatureCounter = success.Result.Counter, @@ -158,21 +152,13 @@ namespace BTCPayServer.Fido2 } var existingCredentials = user.Fido2Credentials .Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2) - .Select(c => c.GetFido2Blob().Descriptor) + .Select(c => c.GetFido2Blob().Descriptor?.ToFido2()) .ToList(); var exts = new AuthenticationExtensionsClientInputs() { - SimpleTransactionAuthorization = "FIDO", - GenericTransactionAuthorization = new TxAuthGenericArg - { - ContentType = "text/plain", - Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } - }, - UserVerificationIndex = true, - Location = true, UserVerificationMethod = true, Extensions = true, - AppID = _fido2Configuration.Origin + AppID = _fido2Configuration.Origins.First() }; // 3. Create options @@ -206,7 +192,7 @@ namespace BTCPayServer.Fido2 // 5. Make the assertion var res = await _fido2.MakeAssertionAsync(response, options, credential.Item2.PublicKey, - credential.Item2.SignatureCounter, x => Task.FromResult(true)); + credential.Item2.SignatureCounter, (x, cancellationToken) => Task.FromResult(true)); // 6. Store the updated counter credential.Item2.SignatureCounter = res.Counter; diff --git a/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs b/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs index da15df1a9..6f5344a5f 100644 --- a/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs +++ b/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs @@ -1,3 +1,4 @@ +using System; using Fido2NetLib; using Fido2NetLib.Objects; using Newtonsoft.Json; @@ -6,7 +7,84 @@ namespace BTCPayServer.Fido2.Models { public class Fido2CredentialBlob { - public PublicKeyCredentialDescriptor Descriptor { get; set; } + public class Base64UrlConverter : JsonConverter + { + private readonly Required _requirement = Required.DisallowNull; + + public Base64UrlConverter() + { + } + + public Base64UrlConverter(Required required = Required.DisallowNull) + { + _requirement = required; + } + + public override void WriteJson(JsonWriter writer, byte[] value, JsonSerializer serializer) + { + writer.WriteValue(Base64Url.Encode(value)); + } + + public override byte[] ReadJson(JsonReader reader, Type objectType, byte[] existingValue, bool hasExistingValue, JsonSerializer serializer) + { + byte[] ret = null; + + if (null == reader.Value && _requirement == Required.AllowNull) + return ret; + + if (null == reader.Value) + throw new Fido2VerificationException("json value must not be null"); + if (Type.GetType("System.String") != reader.ValueType) + throw new Fido2VerificationException("json valuetype must be string"); + try + { + ret = Base64Url.Decode((string)reader.Value); + } + catch (FormatException ex) + { + throw new Fido2VerificationException("json value must be valid base64 encoded string", ex); + } + return ret; + } + } + public class DescriptorClass + { + public DescriptorClass(byte[] credentialId) + { + Id = credentialId; + } + + public DescriptorClass() + { + + } + + /// + /// This member contains the type of the public key credential the caller is referring to. + /// + [JsonProperty("type")] + public string Type { get; set; } = "public-key"; + + /// + /// This member contains the credential ID of the public key credential the caller is referring to. + /// + [JsonConverter(typeof(Base64UrlConverter))] + [JsonProperty("id")] + public byte[] Id { get; set; } + + /// + /// This OPTIONAL member contains a hint as to how the client might communicate with the managing authenticator of the public key credential the caller is referring to. + /// + [JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)] + public string[] Transports { get; set; } + + public PublicKeyCredentialDescriptor ToFido2() + { + var str = JsonConvert.SerializeObject(this); + return System.Text.Json.JsonSerializer.Deserialize(str); + } + } + public DescriptorClass Descriptor { get; set; } [JsonConverter(typeof(Base64UrlConverter))] public byte[] PublicKey { get; set; } [JsonConverter(typeof(Base64UrlConverter))] diff --git a/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs b/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs index 23eb1a61a..9142f0261 100644 --- a/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs +++ b/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs @@ -1,4 +1,5 @@ using Fido2NetLib; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Fido2.Models { @@ -7,7 +8,7 @@ namespace BTCPayServer.Fido2.Models public string UserId { get; set; } public bool RememberMe { get; set; } - public AssertionOptions Data { get; set; } + public string Data { get; set; } public string Response { get; set; } } } diff --git a/BTCPayServer/HostedServices/BlobMigratorHostedService.cs b/BTCPayServer/HostedServices/BlobMigratorHostedService.cs index 8a62bed94..5228abd76 100644 --- a/BTCPayServer/HostedServices/BlobMigratorHostedService.cs +++ b/BTCPayServer/HostedServices/BlobMigratorHostedService.cs @@ -11,7 +11,6 @@ using BTCPayServer.Services.Invoices; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage.Table; using NBitcoin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 36949f1b3..f0bcb870c 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -532,7 +532,8 @@ o.GetRequiredService>().ToDictionary(o => o.P { "TRY", "btcturk" }, { "UGX", "yadio"}, { "RSD", "bitpay"}, - { "NGN", "bitnob"} + { "NGN", "bitnob"}, + { "NOK", "barebitcoin"} }) { var r = new DefaultRules.Recommendation(rule.Key, rule.Value); @@ -587,6 +588,8 @@ o.GetRequiredService>().ToDictionary(o => o.P services.AddRateProvider(); services.AddRateProvider(); services.AddRateProvider(); + services.AddRateProvider(); + services.AddRateProvider(); services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index a45b10b0d..7092b76b8 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; @@ -22,14 +23,15 @@ using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Stores; using BTCPayServer.Storage.Models; using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using Fido2NetLib.Cbor; using Fido2NetLib.Objects; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using PeterO.Cbor; using YamlDotNet.RepresentationModel; +using static BTCPayServer.Fido2.Models.Fido2CredentialBlob; using LightningAddressData = BTCPayServer.Data.LightningAddressData; namespace BTCPayServer.Hosting @@ -738,9 +740,9 @@ WHERE cte.""Id""=p.""Id"" fido2.SetBlob(new Fido2CredentialBlob() { SignatureCounter = (uint)u2FDevice.Counter, - PublicKey = CreatePublicKeyFromU2fRegistrationData(u2FDevice.PublicKey).EncodeToBytes(), + PublicKey = CreatePublicKeyFromU2fRegistrationData(u2FDevice.PublicKey).Encode(), UserHandle = u2FDevice.KeyHandle, - Descriptor = new PublicKeyCredentialDescriptor(u2FDevice.KeyHandle), + Descriptor = new DescriptorClass(u2FDevice.KeyHandle), CredType = "u2f" }); @@ -751,27 +753,29 @@ WHERE cte.""Id""=p.""Id"" await ctx.SaveChangesAsync(); } //from https://github.com/abergs/fido2-net-lib/blob/0fa7bb4b4a1f33f46c5f7ca4ee489b47680d579b/Test/ExistingU2fRegistrationDataTests.cs#L70 - private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData) + private static CborMap CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData) { - if (publicKeyData.Length != 65) - { - throw new ArgumentException("u2f public key must be 65 bytes", nameof(publicKeyData)); - } var x = new byte[32]; var y = new byte[32]; Buffer.BlockCopy(publicKeyData, 1, x, 0, 32); Buffer.BlockCopy(publicKeyData, 33, y, 0, 32); + var point = new ECPoint + { + X = x, + Y = y, + }; - var coseKey = CBORObject.NewMap(); + var coseKey = new CborMap + { + { (long)COSE.KeyCommonParameter.KeyType, (long)COSE.KeyType.EC2 }, + { (long)COSE.KeyCommonParameter.Alg, -7L }, - coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2); - coseKey.Add(COSE.KeyCommonParameter.Alg, -7); + { (long)COSE.KeyTypeParameter.Crv, (long)COSE.EllipticCurve.P256 }, - coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256); - - coseKey.Add(COSE.KeyTypeParameter.X, x); - coseKey.Add(COSE.KeyTypeParameter.Y, y); + { (long)COSE.KeyTypeParameter.X, point.X }, + { (long)COSE.KeyTypeParameter.Y, point.Y } + }; return coseKey; } diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index de294a8ec..51b59a00d 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -122,8 +122,7 @@ namespace BTCPayServer.Hosting }) .AddCachedMetadataService(config => { - //They'll be used in a "first match wins" way in the order registered - config.AddStaticMetadataRepository(); + config.AddFidoMetadataRepository(); }); var descriptor = services.Single(descriptor => descriptor.ServiceType == typeof(Fido2Configuration)); services.Remove(descriptor); @@ -133,7 +132,7 @@ namespace BTCPayServer.Hosting return new Fido2Configuration() { ServerName = "BTCPay Server", - Origin = $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}", + Origins = new[] { $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}" }.ToHashSet(), ServerDomain = httpContext.HttpContext.Request.Host.Host }; }); @@ -141,7 +140,7 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddMvc(o => + var mvcBuilder = services.AddMvc(o => { o.Filters.Add(new XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.Deny)); o.Filters.Add(new XContentTypeOptionsAttribute("nosniff")); @@ -167,11 +166,14 @@ namespace BTCPayServer.Hosting o.PageViewLocationFormats.Add("/{0}.cshtml"); }) .AddNewtonsoftJson() - .AddRazorRuntimeCompilation() .AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider) .AddDataAnnotationsLocalization() .AddControllersAsServices(); +#if !RAZOR_COMPILE_ON_BUILD + mvcBuilder.AddRazorRuntimeCompilation(); +#endif + services.AddServerSideBlazor(); LowercaseTransformer.Register(services); diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index ec985b575..4b6d18377 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -67,16 +67,14 @@ namespace BTCPayServer.PaymentRequest var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id); var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true); - if (contributions.Total >= blob.Amount) - { - currentStatus = contributions.TotalSettled >= blob.Amount - ? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed - : Client.Models.PaymentRequestData.PaymentRequestStatus.Processing; - } - else - { - currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending; - } + currentStatus = + (PaidEnough: contributions.Total >= blob.Amount, + SettledEnough: contributions.TotalSettled >= blob.Amount) switch + { + { SettledEnough: true } => Client.Models.PaymentRequestData.PaymentRequestStatus.Completed, + { PaidEnough: true } => Client.Models.PaymentRequestData.PaymentRequestStatus.Processing, + _ => Client.Models.PaymentRequestData.PaymentRequestStatus.Pending + }; } if (currentStatus != pr.Status) @@ -100,7 +98,7 @@ namespace BTCPayServer.PaymentRequest var amountDue = blob.Amount - paymentStats.Total; var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime) .FirstOrDefault(entity => entity.Status == InvoiceStatus.New); - + return new ViewPaymentRequestViewModel(pr) { Archived = pr.Archived, @@ -121,8 +119,7 @@ namespace BTCPayServer.PaymentRequest var state = entity.GetInvoiceState(); var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(entity, _displayFormatter, _transactionLinkProviders, _handlers); - if (state.Status == InvoiceStatus.Invalid || - state.Status == InvoiceStatus.Expired && !payments.Any()) + if (state.Status is InvoiceStatus.Invalid or InvoiceStatus.Expired && payments.Count is 0) return null; return new ViewPaymentRequestViewModel.PaymentRequestInvoice diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index 2f02292cf..33dfd314f 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -23,7 +23,6 @@ using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using static Org.BouncyCastle.Math.EC.ECCurve; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Payments.Bitcoin diff --git a/BTCPayServer/Payments/IPaymentMethodHandler.cs b/BTCPayServer/Payments/IPaymentMethodHandler.cs index 83a6c4309..f3fd39dc2 100644 --- a/BTCPayServer/Payments/IPaymentMethodHandler.cs +++ b/BTCPayServer/Payments/IPaymentMethodHandler.cs @@ -17,7 +17,6 @@ using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; -using crypto; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ModelBinding; using NBitcoin; diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index 15efa0fc7..759e42bfd 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -144,6 +144,8 @@ retry: return _memoryCache.GetOrCreateAsync(GetCacheKey(invoiceId), async (cacheEntry) => { var invoice = await _InvoiceRepository.GetInvoice(invoiceId); + if (invoice is null) + return null; cacheEntry.AbsoluteExpiration = GetExpiration(invoice); return invoice; })!; diff --git a/BTCPayServer/Payments/PayJoin/PayJoinRepository.cs b/BTCPayServer/Payments/PayJoin/PayJoinRepository.cs index 629a8bdf1..d43c8492f 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinRepository.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinRepository.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; +using Dapper; using Microsoft.EntityFrameworkCore; using NBitcoin; +using Npgsql; namespace BTCPayServer.Payments.PayJoin { @@ -16,20 +18,6 @@ namespace BTCPayServer.Payments.PayJoin _dbContextFactory = dbContextFactory; } - public async Task TryLock(OutPoint outpoint) - { - using var ctx = _dbContextFactory.CreateContext(); - ctx.PayjoinLocks.Add(new PayjoinLock() { Id = outpoint.ToString() }); - try - { - return await ctx.SaveChangesAsync() == 1; - } - catch (DbUpdateException) - { - return false; - } - } - public async Task TryUnlock(params OutPoint[] outPoints) { using var ctx = _dbContextFactory.CreateContext(); @@ -48,29 +36,33 @@ namespace BTCPayServer.Payments.PayJoin } } - public async Task TryLockInputs(OutPoint[] outPoints) + + + + + private async Task TryLockInputs(string[] ids) { using var ctx = _dbContextFactory.CreateContext(); - foreach (OutPoint outPoint in outPoints) - { - ctx.PayjoinLocks.Add(new PayjoinLock() - { - // Random flag so it does not lock same id - // as the lock utxo - Id = "K-" + outPoint.ToString() - }); - } - + var connection = ctx.Database.GetDbConnection(); try { - return await ctx.SaveChangesAsync() == outPoints.Length; + await connection.ExecuteAsync(""" + INSERT INTO "PayjoinLocks"("Id") + SELECT * FROM unnest(@ids) + """, new { ids }); + return true; } - catch (DbUpdateException) + catch (Npgsql.PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UniqueViolation) { return false; } } + public Task TryLock(OutPoint outpoint) + => TryLockInputs([outpoint.ToString()]); + public Task TryLockInputs(OutPoint[] outPoints) + => TryLockInputs(outPoints.Select(o => "K-" + o.ToString()).ToArray()); + public async Task> FindLocks(OutPoint[] outpoints) { var outPointsStr = outpoints.Select(o => o.ToString()); diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 71640e37c..7393aa17d 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -43,6 +43,10 @@ namespace BTCPayServer.Services.Apps private readonly DisplayFormatter _displayFormatter; private readonly StoreRepository _storeRepository; public CurrencyNameTable Currencies => _Currencies; + private readonly string[] _paidStatuses = [ + InvoiceStatus.Processing.ToString(), + InvoiceStatus.Settled.ToString() + ]; public AppService( IEnumerable apps, @@ -86,11 +90,7 @@ namespace BTCPayServer.Services.Apps { if (GetAppType(appData.AppType) is not IHasItemStatsAppType salesType) throw new InvalidOperationException("This app isn't a SalesAppBaseType"); - var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData, null, - [ - InvoiceStatus.Processing.ToString(), - InvoiceStatus.Settled.ToString() - ]); + var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData, null, _paidStatuses); return await salesType.GetItemStats(appData, paidInvoices); } @@ -132,8 +132,7 @@ namespace BTCPayServer.Services.Apps { if (GetAppType(app.AppType) is not IHasSaleStatsAppType salesType) throw new InvalidOperationException("This app isn't a SalesAppBaseType"); - var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays)); - + var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays), _paidStatuses); return await salesType.GetSalesStats(app, paidInvoices, numberOfDays); } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index fcf5f76c0..f6fae9432 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -267,9 +267,9 @@ namespace BTCPayServer.Services.Invoices } public const int InternalTagSupport_Version = 1; public const int GreenfieldInvoices_Version = 2; - public const int LeanInvoices_Version = 3; - public const int Lastest_Version = 3; - public int Version { get; set; } + public const int LeanInvoices_Version = 3; + public const int Lastest_Version = 3; + public int Version { get; set; } [JsonIgnore] public string Id { get; set; } [JsonIgnore] @@ -349,7 +349,7 @@ namespace BTCPayServer.Services.Invoices ArgumentNullException.ThrowIfNull(pair); #pragma warning disable CS0618 // Type or member is obsolete if (pair.Right == Currency && Rates.TryGetValue(pair.Left, out var rate)) // Fast lane - return rate; + return rate; #pragma warning restore CS0618 // Type or member is obsolete var rule = GetRateRules().GetRuleFor(pair); rule.Reevaluate(); @@ -802,33 +802,40 @@ namespace BTCPayServer.Services.Invoices } public record InvoiceState(InvoiceStatus Status, InvoiceExceptionStatus ExceptionStatus) { - public InvoiceState(string status, string exceptionStatus): + public InvoiceState(string status, string exceptionStatus) : this(Enum.Parse(status), exceptionStatus switch { "None" or "" or null => InvoiceExceptionStatus.None, _ => Enum.Parse(exceptionStatus) }) { } - public bool CanMarkComplete() + public bool CanMarkComplete() => (Status, ExceptionStatus) is { - return Status is InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired or InvoiceStatus.Invalid || - (Status != InvoiceStatus.Settled && ExceptionStatus == InvoiceExceptionStatus.Marked); + Status: InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired or InvoiceStatus.Invalid } + or + { + Status: not InvoiceStatus.Settled, + ExceptionStatus: InvoiceExceptionStatus.Marked + }; - public bool CanMarkInvalid() + public bool CanMarkInvalid() => (Status, ExceptionStatus) is { - return Status is InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired || - (Status != InvoiceStatus.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked); + Status: InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired } + or + { + Status: not InvoiceStatus.Invalid, + ExceptionStatus: InvoiceExceptionStatus.Marked + }; - public bool CanRefund() + public bool CanRefund() => (Status, ExceptionStatus) is { - return - Status == InvoiceStatus.Settled || - (Status == InvoiceStatus.Expired && - (ExceptionStatus == InvoiceExceptionStatus.PaidLate || - ExceptionStatus == InvoiceExceptionStatus.PaidOver || - ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) || - Status == InvoiceStatus.Invalid; + Status: InvoiceStatus.Settled or InvoiceStatus.Invalid } + or + { + Status: InvoiceStatus.Expired, + ExceptionStatus: InvoiceExceptionStatus.PaidLate or InvoiceExceptionStatus.PaidOver or InvoiceExceptionStatus.PaidPartial + }; public override string ToString() { diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConnectionStringValidator.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConnectionStringValidator.cs index 38d9785f7..343f71e4e 100644 --- a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConnectionStringValidator.cs +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConnectionStringValidator.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using Microsoft.WindowsAzure.Storage; namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration { @@ -10,7 +9,7 @@ namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration { try { - CloudStorageAccount.Parse(value as string); + new Azure.Storage.Blobs.BlobClient(value as string, "unusedcontainer", "unusedblob"); return ValidationResult.Success; } catch (Exception e) diff --git a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml index 74e1c25d5..41d6176fd 100644 --- a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml @@ -32,7 +32,8 @@