diff --git a/BTCPayServer.Data/Data/InvoiceData.cs b/BTCPayServer.Data/Data/InvoiceData.cs index 554a1827d..cafafdd6e 100644 --- a/BTCPayServer.Data/Data/InvoiceData.cs +++ b/BTCPayServer.Data/Data/InvoiceData.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -31,7 +32,9 @@ namespace BTCPayServer.Data public List InvoiceSearchData { get; set; } public List Refunds { get; set; } - + [Timestamp] + // With this, update of InvoiceData will fail if the row was modified by another process + public uint XMin { get; set; } internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) { builder.Entity() diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index 3d5f484c7..bcc6343b0 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; @@ -16,25 +16,7 @@ namespace BTCPayServer.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); - - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.Property("Address") - .HasColumnType("TEXT"); - - b.Property("CreatedTime") - .HasColumnType("TEXT"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Address"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("AddressInvoices"); - }); + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => { @@ -71,6 +53,24 @@ namespace BTCPayServer.Migrations b.ToTable("ApiKeys"); }); + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("CreatedTime") + .HasColumnType("TEXT"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + modelBuilder.Entity("BTCPayServer.Data.AppData", b => { b.Property("Id") @@ -89,7 +89,7 @@ namespace BTCPayServer.Migrations .HasColumnType("TEXT"); b.Property("Settings") - .HasColumnType("JSONB"); + .HasColumnType("TEXT"); b.Property("StoreDataId") .HasColumnType("TEXT"); @@ -305,6 +305,11 @@ namespace BTCPayServer.Migrations b.Property("StoreDataId") .HasColumnType("TEXT"); + b.Property("XMin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.HasIndex("Created"); @@ -781,31 +786,6 @@ namespace BTCPayServer.Migrations b.ToTable("Stores"); }); - modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("StorageFileName") - .HasColumnType("TEXT"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId"); - - b.ToTable("Files"); - }); - modelBuilder.Entity("BTCPayServer.Data.StoreRole", b => { b.Property("Id") @@ -863,6 +843,31 @@ namespace BTCPayServer.Migrations b.ToTable("StoreWebhooks"); }); + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("StorageFileName") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => { b.Property("Id") @@ -1171,16 +1176,6 @@ namespace BTCPayServer.Migrations b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("AddressInvoices") - .HasForeignKey("InvoiceDataId") - .OnDelete(DeleteBehavior.Cascade); - - b.Navigation("InvoiceData"); - }); - modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => { b.HasOne("BTCPayServer.Data.StoreData", "StoreData") @@ -1198,6 +1193,16 @@ namespace BTCPayServer.Migrations b.Navigation("User"); }); + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("InvoiceData"); + }); + modelBuilder.Entity("BTCPayServer.Data.AppData", b => { b.HasOne("BTCPayServer.Data.StoreData", "StoreData") @@ -1408,15 +1413,6 @@ namespace BTCPayServer.Migrations b.Navigation("PullPaymentData"); }); - modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("StoredFiles") - .HasForeignKey("ApplicationUserId"); - - b.Navigation("ApplicationUser"); - }); - modelBuilder.Entity("BTCPayServer.Data.StoreRole", b => { b.HasOne("BTCPayServer.Data.StoreData", "StoreData") @@ -1457,6 +1453,15 @@ namespace BTCPayServer.Migrations b.Navigation("Webhook"); }); + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("ApplicationUser"); + }); + modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => { b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index b64bf835c..e11f3b98b 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -105,8 +105,15 @@ namespace BTCPayServer.Tests public void MineBlockOnInvoiceCheckout() { - Driver.FindElement(By.CssSelector("#mine-block button")).Click(); - + retry: + try + { + Driver.FindElement(By.CssSelector("#mine-block button")).Click(); + } + catch (StaleElementReferenceException) + { + goto retry; + } } /// diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index cb02a2fc1..657dbbedf 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1149,7 +1149,6 @@ namespace BTCPayServer.Tests // Contribute s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click(); - Thread.Sleep(1000); s.Driver.WaitUntilAvailable(By.Name("btcpay")); var frameElement = s.Driver.FindElement(By.Name("btcpay")); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index f8ce7ed93..c836946cb 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -395,7 +395,7 @@ namespace BTCPayServer.Tests BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC)); }, 40000); - TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning"); + TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork)tester.DefaultNetwork)} via lightning"); var evt = await tester.WaitForEvent(async () => { await tester.SendLightningPaymentAsync(newInvoice); @@ -1301,11 +1301,8 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); await user.GrantAccessAsync(); user.RegisterDerivationScheme("BTC"); - + await tester.ExplorerNode.EnsureGenerateAsync(1); var rng = new Random(); - var seed = rng.Next(); - rng = new Random(seed); - TestLogs.LogInformation("Seed: " + seed); foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast()) { await user.SetNetworkFeeMode(networkFeeMode); @@ -1318,7 +1315,7 @@ namespace BTCPayServer.Tests } } - private static async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode) + private async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode) { var cashCow = tester.ExplorerNode; // First we try payment with a merchant having only BTC @@ -1343,7 +1340,6 @@ namespace BTCPayServer.Tests { networkFee = 0.0m; } - await cashCow.SendToAddressAsync(invoiceAddress, paid); await TestUtils.EventuallyAsync(async () => { @@ -1822,7 +1818,7 @@ namespace BTCPayServer.Tests Assert.Empty(appList2.Apps); Assert.Equal("test", appList.Apps[0].AppName); Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); - + Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId)); Assert.Equal(user.StoreId, appList.Apps[0].StoreId); Assert.IsType(apps2.DeleteApp(appList.Apps[0].Id)); @@ -2399,11 +2395,11 @@ namespace BTCPayServer.Tests var url = lnMethod.GetExternalLightningUrl(); var kv = LightningConnectionStringHelper.ExtractValues(url, out var connType); - Assert.Equal(LightningConnectionType.Charge,connType); + Assert.Equal(LightningConnectionType.Charge, connType); var client = Assert.IsType(tester.PayTester.GetService() .Create(url, tester.NetworkProvider.GetNetwork("BTC"))); var auth = Assert.IsType(client.ChargeAuthentication); - + Assert.Equal("pass", auth.NetworkCredential.Password); Assert.Equal("usr", auth.NetworkCredential.UserName); @@ -2829,7 +2825,7 @@ namespace BTCPayServer.Tests var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest() { AppName = "Static", - DefaultView = Client.Models.PosViewType.Static, + DefaultView = Client.Models.PosViewType.Static, Template = new PointOfSaleSettings().Template }); var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea"); @@ -2839,7 +2835,7 @@ namespace BTCPayServer.Tests app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest() { AppName = "Cart", - DefaultView = Client.Models.PosViewType.Cart, + DefaultView = Client.Models.PosViewType.Cart, Template = new PointOfSaleSettings().Template }); resp = await posController.ViewPointOfSale(app.Id, posData: new JObject() diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index d11def182..0eabf630b 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -50,6 +50,7 @@ namespace BTCPayServer.Data public static StoreBlob GetStoreBlob(this StoreData storeData) { + ArgumentNullException.ThrowIfNull(storeData); var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject(storeData.StoreBlob); if (result.PreferredExchange == null) result.PreferredExchange = result.GetRecommendedExchange(); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 04711c308..7649edc3a 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -38,11 +38,10 @@ namespace BTCPayServer.HostedServices public bool Dirty => _dirty; - bool _isBlobUpdated; - public bool IsBlobUpdated => _isBlobUpdated; - public void BlobUpdated() + public bool IsPriceUpdated { get; private set; } + public void PriceUpdated() { - _isBlobUpdated = true; + IsPriceUpdated = true; } } @@ -104,7 +103,7 @@ namespace BTCPayServer.HostedServices var payment = invoice.GetPayments(true).First(); invoice.Price = payment.InvoicePaidAmount.Net; invoice.UpdateTotals(); - context.BlobUpdated(); + context.PriceUpdated(); } else { @@ -291,9 +290,9 @@ namespace BTCPayServer.HostedServices await _invoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } - if (updateContext.IsBlobUpdated) + if (updateContext.IsPriceUpdated) { - await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice); + await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice.Price); } foreach (var evt in updateContext.Events) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 8501d0a95..96c6d8202 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -79,13 +79,13 @@ namespace BTCPayServer.Services.Apps public async Task GetInfo(string appId) { - var appData = await GetApp(appId, null); + var appData = await GetApp(appId, null, includeStore: true); if (appData is null) return null; var appType = GetAppType(appData.AppType); if (appType is null) return null; - return appType.GetInfo(appData); + return await appType.GetInfo(appData); } public async Task> GetItemStats(AppData appData) diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 0253658ce..ece763785 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -11,6 +11,7 @@ using BTCPayServer.Events; using BTCPayServer.Logging; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; +using Dapper; using Microsoft.EntityFrameworkCore; using NBitcoin; using Newtonsoft.Json; @@ -130,32 +131,50 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data) { - using var ctx = _applicationDbContextFactory.CreateContext(); - var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - if (invoiceData.CustomerEmail == null && data.Email != null) + retry: + using (var ctx = _applicationDbContextFactory.CreateContext()) { - invoiceData.CustomerEmail = data.Email; - AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail); + var invoiceData = await ctx.Invoices.FindAsync(invoiceId); + if (invoiceData == null) + return; + if (invoiceData.CustomerEmail == null && data.Email != null) + { + invoiceData.CustomerEmail = data.Email; + AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail); + } + try + { + await ctx.SaveChangesAsync().ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException) + { + goto retry; + } } - await ctx.SaveChangesAsync().ConfigureAwait(false); } public async Task UpdateInvoiceExpiry(string invoiceId, TimeSpan seconds) { - await using var ctx = _applicationDbContextFactory.CreateContext(); - var invoiceData = await ctx.Invoices.FindAsync(invoiceId); - var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); - var expiry = DateTimeOffset.Now + seconds; - invoice.ExpirationTime = expiry; - invoice.MonitoringExpiration = expiry.AddHours(1); - invoiceData.SetBlob(invoice); - - await ctx.SaveChangesAsync(); - - _eventAggregator.Publish(new InvoiceDataChangedEvent(invoice)); - _ = InvoiceNeedUpdateEventLater(invoiceId, seconds); + retry: + await using (var ctx = _applicationDbContextFactory.CreateContext()) + { + var invoiceData = await ctx.Invoices.FindAsync(invoiceId); + var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); + var expiry = DateTimeOffset.Now + seconds; + invoice.ExpirationTime = expiry; + invoice.MonitoringExpiration = expiry.AddHours(1); + invoiceData.SetBlob(invoice); + try + { + await ctx.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + goto retry; + } + _eventAggregator.Publish(new InvoiceDataChangedEvent(invoice)); + _ = InvoiceNeedUpdateEventLater(invoiceId, seconds); + } } async Task InvoiceNeedUpdateEventLater(string invoiceId, TimeSpan expirationIn) @@ -166,13 +185,23 @@ namespace BTCPayServer.Services.Invoices public async Task ExtendInvoiceMonitor(string invoiceId) { - using var ctx = _applicationDbContextFactory.CreateContext(); - var invoiceData = await ctx.Invoices.FindAsync(invoiceId); + retry: + using (var ctx = _applicationDbContextFactory.CreateContext()) + { + var invoiceData = await ctx.Invoices.FindAsync(invoiceId); - var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); - invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); - invoiceData.SetBlob(invoice); - await ctx.SaveChangesAsync(); + var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); + invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); + invoiceData.SetBlob(invoice); + try + { + await ctx.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + goto retry; + } + } } public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null) @@ -279,62 +308,81 @@ namespace BTCPayServer.Services.Invoices public async Task NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network) { - await using var context = _applicationDbContextFactory.CreateContext(); - var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault(); - if (invoice == null) - return false; + retry: + await using (var context = _applicationDbContextFactory.CreateContext()) + { + var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault(); + if (invoice == null) + return false; - var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); - var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType()); - if (paymentMethod == null) - return false; + var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); + var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType()); + if (paymentMethod == null) + return false; - var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails(); - paymentMethod.SetPaymentMethodDetails(paymentMethodDetails); + var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails(); + paymentMethod.SetPaymentMethodDetails(paymentMethodDetails); #pragma warning disable CS0618 - if (network.IsBTC) - { - invoiceEntity.DepositAddress = paymentMethod.DepositAddress; - } + if (network.IsBTC) + { + invoiceEntity.DepositAddress = paymentMethod.DepositAddress; + } #pragma warning restore CS0618 - invoiceEntity.SetPaymentMethod(paymentMethod); - invoice.SetBlob(invoiceEntity); - - await context.AddressInvoices.AddAsync(new AddressInvoiceData() - { - InvoiceDataId = invoiceId, - CreatedTime = DateTimeOffset.UtcNow - } - .Set(GetDestination(paymentMethod), paymentMethod.GetId())); - - AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination()); - await context.SaveChangesAsync(); - return true; - } - - public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) - { - using var context = _applicationDbContextFactory.CreateContext(); - var invoice = await context.Invoices.FindAsync(invoiceId); - if (invoice == null) - return; - var network = paymentMethod.Network; - var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); - var newDetails = paymentMethod.GetPaymentMethodDetails(); - var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); - if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) - { + invoiceEntity.SetPaymentMethod(paymentMethod); + invoice.SetBlob(invoiceEntity); await context.AddressInvoices.AddAsync(new AddressInvoiceData() { InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow } - .Set(GetDestination(paymentMethod), paymentMethod.GetId())); + .Set(GetDestination(paymentMethod), paymentMethod.GetId())); + + AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination()); + try + { + await context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + goto retry; + } + return true; + } + } + + public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) + { + retry: + using (var context = _applicationDbContextFactory.CreateContext()) + { + var invoice = await context.Invoices.FindAsync(invoiceId); + if (invoice == null) + return; + var network = paymentMethod.Network; + var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); + var newDetails = paymentMethod.GetPaymentMethodDetails(); + var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); + if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) + { + await context.AddressInvoices.AddAsync(new AddressInvoiceData() + { + InvoiceDataId = invoiceId, + CreatedTime = DateTimeOffset.UtcNow + } + .Set(GetDestination(paymentMethod), paymentMethod.GetId())); + } + invoiceEntity.SetPaymentMethod(paymentMethod); + invoice.SetBlob(invoiceEntity); + AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination()); + try + { + await context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + goto retry; + } } - invoiceEntity.SetPaymentMethod(paymentMethod); - invoice.SetBlob(invoiceEntity); - AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination()); - await context.SaveChangesAsync(); } public async Task AddPendingInvoiceIfNotPresent(string invoiceId) @@ -389,26 +437,38 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState) { using var context = _applicationDbContextFactory.CreateContext(); - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - invoiceData.Status = InvoiceState.ToString(invoiceState.Status); - invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus); - await context.SaveChangesAsync().ConfigureAwait(false); + await context.Database.GetDbConnection() + .ExecuteAsync("UPDATE \"Invoices\" SET \"Status\"=@status, \"ExceptionStatus\"=@exstatus WHERE \"Id\"=@id", + new + { + id = invoiceId, + status = InvoiceState.ToString(invoiceState.Status), + exstatus = InvoiceState.ToString(invoiceState.ExceptionStatus) + }); } - internal async Task UpdateInvoicePrice(string invoiceId, InvoiceEntity invoice) + internal async Task UpdateInvoicePrice(string invoiceId, decimal price) { - if (invoice.Type != InvoiceType.TopUp) - throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice)); - using var context = _applicationDbContextFactory.CreateContext(); - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - var blob = invoiceData.GetBlob(_btcPayNetworkProvider); - blob.Price = invoice.Price; - AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) }); - invoiceData.SetBlob(blob); - await context.SaveChangesAsync().ConfigureAwait(false); + retry: + using (var context = _applicationDbContextFactory.CreateContext()) + { + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null) + return; + var blob = invoiceData.GetBlob(_btcPayNetworkProvider); + if (blob.Type != InvoiceType.TopUp) + throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoiceId)); + blob.Price = price; + AddToTextSearch(context, invoiceData, new[] { price.ToString(CultureInfo.InvariantCulture) }); + invoiceData.SetBlob(blob); + try + { + await context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + goto retry; + } + } } public async Task MassArchive(string[] invoiceIds, bool archive = true) @@ -436,37 +496,47 @@ namespace BTCPayServer.Services.Invoices } public async Task UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata) { - using var context = _applicationDbContextFactory.CreateContext(); - var invoiceData = await GetInvoiceRaw(invoiceId, context); - if (invoiceData == null || (storeId != null && - !invoiceData.StoreDataId.Equals(storeId, - StringComparison.InvariantCultureIgnoreCase))) - return null; - var blob = invoiceData.GetBlob(_btcPayNetworkProvider); - - var newMetadata = InvoiceMetadata.FromJObject(metadata); - var oldOrderId = blob.Metadata.OrderId; - var newOrderId = newMetadata.OrderId; - - if (newOrderId != oldOrderId) +retry: + using (var context = _applicationDbContextFactory.CreateContext()) { - // OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency. - invoiceData.OrderId = newOrderId; + var invoiceData = await GetInvoiceRaw(invoiceId, context); + if (invoiceData == null || (storeId != null && + !invoiceData.StoreDataId.Equals(storeId, + StringComparison.InvariantCultureIgnoreCase))) + return null; + var blob = invoiceData.GetBlob(_btcPayNetworkProvider); - if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture))) + var newMetadata = InvoiceMetadata.FromJObject(metadata); + var oldOrderId = blob.Metadata.OrderId; + var newOrderId = newMetadata.OrderId; + + if (newOrderId != oldOrderId) { - RemoveFromTextSearch(context, invoiceData, oldOrderId); + // OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency. + invoiceData.OrderId = newOrderId; + + if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture))) + { + RemoveFromTextSearch(context, invoiceData, oldOrderId); + } + if (newOrderId != null) + { + AddToTextSearch(context, invoiceData, new[] { newOrderId }); + } } - if (newOrderId != null) + + blob.Metadata = newMetadata; + invoiceData.SetBlob(blob); + try { - AddToTextSearch(context, invoiceData, new[] { newOrderId }); + await context.SaveChangesAsync(); } + catch (DbUpdateConcurrencyException) + { + goto retry; + } + return ToEntity(invoiceData); } - - blob.Metadata = newMetadata; - invoiceData.SetBlob(blob); - await context.SaveChangesAsync().ConfigureAwait(false); - return ToEntity(invoiceData); } public async Task MarkInvoiceStatus(string invoiceId, InvoiceStatus status) {