diff --git a/BTCPayServer.Data/Data/InvoiceData.Migration.cs b/BTCPayServer.Data/Data/InvoiceData.Migration.cs index 32ada35c3..f26e4a26e 100644 --- a/BTCPayServer.Data/Data/InvoiceData.Migration.cs +++ b/BTCPayServer.Data/Data/InvoiceData.Migration.cs @@ -37,6 +37,10 @@ namespace BTCPayServer.Data { paymentData.Migrate(); } + else if (entity is PayoutData payoutData && payoutData.Currency is null) + { + payoutData.Migrate(); + } return entity; } } diff --git a/BTCPayServer.Data/Data/PayoutData.Migration.cs b/BTCPayServer.Data/Data/PayoutData.Migration.cs new file mode 100644 index 000000000..bafdf8286 --- /dev/null +++ b/BTCPayServer.Data/Data/PayoutData.Migration.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BTCPayServer.Data +{ + public partial class PayoutData + { + public void Migrate() + { + PayoutMethodId = MigrationExtensions.MigratePaymentMethodId(PayoutMethodId); + // Could only be BTC-LN or BTC-CHAIN, so we extract the crypto currency + Currency = PayoutMethodId.Split('-')[0]; + } + } +} diff --git a/BTCPayServer.Data/Data/PayoutData.cs b/BTCPayServer.Data/Data/PayoutData.cs index c79cc9109..4d86b75f3 100644 --- a/BTCPayServer.Data/Data/PayoutData.cs +++ b/BTCPayServer.Data/Data/PayoutData.cs @@ -10,7 +10,7 @@ using NBitcoin; namespace BTCPayServer.Data { - public class PayoutData + public partial class PayoutData { [Key] [MaxLength(30)] @@ -18,12 +18,13 @@ namespace BTCPayServer.Data public DateTimeOffset Date { get; set; } public string PullPaymentDataId { get; set; } public string StoreDataId { get; set; } + public string Currency { get; set; } public PullPaymentData PullPaymentData { get; set; } [MaxLength(20)] public PayoutState State { get; set; } [MaxLength(20)] [Required] - public string PaymentMethodId { get; set; } + public string PayoutMethodId { get; set; } public string Blob { get; set; } public string Proof { get; set; } #nullable enable diff --git a/BTCPayServer.Data/Migrations/20240520042729_payoutsmigration.cs b/BTCPayServer.Data/Migrations/20240520042729_payoutsmigration.cs new file mode 100644 index 000000000..ee1067289 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20240520042729_payoutsmigration.cs @@ -0,0 +1,29 @@ +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240520042729_payoutsmigration")] + public partial class payoutsmigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Currency", + table: "Payouts", + type: "text", + nullable: true); + migrationBuilder.RenameColumn("PaymentMethodId", "Payouts", "PayoutMethodId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index e7a6d6c68..758587f2a 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Collections.Generic; using BTCPayServer.Data; @@ -567,13 +567,16 @@ namespace BTCPayServer.Migrations b.Property("Blob") .HasColumnType("JSONB"); + b.Property("Currency") + .HasColumnType("text"); + b.Property("Date") .HasColumnType("timestamp with time zone"); b.Property("Destination") .HasColumnType("text"); - b.Property("PaymentMethodId") + b.Property("PayoutMethodId") .IsRequired() .HasMaxLength(20) .HasColumnType("character varying(20)"); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs index a6d2e5298..3734250a7 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs @@ -372,9 +372,8 @@ namespace BTCPayServer.Controllers.Greenfield Metadata = blob.Metadata?? new JObject(), }; model.Destination = blob.Destination; - model.PaymentMethod = p.PaymentMethodId; - var currency = this._payoutHandlers.TryGet(p.GetPayoutMethodId())?.Currency; - model.CryptoCode = currency; + model.PaymentMethod = p.PayoutMethodId; + model.CryptoCode = p.Currency; model.PaymentProof = p.GetProofBlobJson(); return model; } diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 66a52d088..fbb557771 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -179,7 +179,7 @@ namespace BTCPayServer lightningHandler.CreateLightningClient(pm); var payResult = await UILightningLikePayoutController.TrypayBolt(client, claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings), - claimResponse.PayoutData, result, payoutHandler.Currency, cancellationToken); + claimResponse.PayoutData, result, cancellationToken); switch (payResult.Result) { diff --git a/BTCPayServer/Controllers/UIPullPaymentController.cs b/BTCPayServer/Controllers/UIPullPaymentController.cs index 0d8d220e0..ecc4bcc1f 100644 --- a/BTCPayServer/Controllers/UIPullPaymentController.cs +++ b/BTCPayServer/Controllers/UIPullPaymentController.cs @@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers Currency = blob.Currency, Status = entity.Entity.State, Destination = entity.Blob.Destination, - PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId), + PaymentMethod = PaymentMethodId.Parse(entity.Entity.PayoutMethodId), Link = entity.ProofBlob?.Link, TransactionId = entity.ProofBlob?.Id }).ToList() diff --git a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs index e67b31348..f3a34dcbb 100644 --- a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs +++ b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs @@ -509,14 +509,14 @@ namespace BTCPayServer.Controllers vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name; } - vm.PayoutMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId) + vm.PayoutMethodCount = (await payoutRequest.GroupBy(data => data.PayoutMethodId) .Select(datas => new { datas.Key, Count = datas.Count() }).ToListAsync()) .ToDictionary(datas => datas.Key, arg => arg.Count); if (vm.PayoutMethodId != null) { var pmiStr = vm.PayoutMethodId; - payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr); + payoutRequest = payoutRequest.Where(p => p.PayoutMethodId == pmiStr); } vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State) .Select(e => new { e.Key, Count = e.Count() }) @@ -563,7 +563,6 @@ namespace BTCPayServer.Controllers { payoutSourceLink = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = item.PullPayment?.Id }); } - var pCurrency = _payoutHandlers.TryGet(PayoutMethodId.Parse(item.Payout.PaymentMethodId))?.Currency; var m = new PayoutsModel.PayoutModel { @@ -572,7 +571,7 @@ namespace BTCPayServer.Controllers SourceLink = payoutSourceLink, Date = item.Payout.Date, PayoutId = item.Payout.Id, - Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? pCurrency), + Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? item.Payout.Currency), Destination = payoutBlob.Destination }; var handler = _payoutHandlers diff --git a/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs b/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs index 3c82ca1ec..f954b0bf4 100644 --- a/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs +++ b/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs @@ -227,7 +227,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork Stores = new[] { storeId }, PayoutIds = payoutIds }, context)).Where(data => - PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) && + PayoutMethodId.TryParse(data.PayoutMethodId, out var payoutMethodId) && payoutMethodId == PayoutMethodId) .Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false); foreach (var valueTuple in payouts) @@ -252,7 +252,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork Stores = new[] { storeId }, PayoutIds = payoutIds }, context)).Where(data => - PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) && + PayoutMethodId.TryParse(data.PayoutMethodId, out var payoutMethodId) && payoutMethodId == PayoutMethodId) .Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true); foreach (var valueTuple in payouts) @@ -285,7 +285,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var payouts = await ctx.Payouts.Include(data => data.PullPaymentData) .Where(data => payoutIds.Contains(data.Id) - && PayoutMethodId.ToString() == data.PaymentMethodId + && PayoutMethodId.ToString() == data.PayoutMethodId && data.State == PayoutState.AwaitingPayment) .ToListAsync(); @@ -428,7 +428,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork .Include(o => o.StoreData) .Include(o => o.PullPaymentData) .Where(p => p.State == PayoutState.AwaitingPayment) - .Where(p => p.PaymentMethodId == paymentMethodId.ToString()) + .Where(p => p.PayoutMethodId == paymentMethodId.ToString()) #pragma warning disable CA1307 // Specify StringComparison .Where(p => destination.Equals(p.Destination)) #pragma warning restore CA1307 // Specify StringComparison @@ -474,7 +474,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId), new ExternalPayoutTransactionNotification() { - PaymentMethod = payout.PaymentMethodId, + PaymentMethod = payout.PayoutMethodId, PayoutId = payout.Id, StoreId = payout.StoreDataId }); diff --git a/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs b/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs index 00894dc42..a5b471253 100644 --- a/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs +++ b/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs @@ -86,7 +86,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike .Where(data => payoutIds.Contains(data.Id) && data.State == PayoutState.AwaitingPayment && - data.PaymentMethodId == pmiStr) + data.PayoutMethodId == pmiStr) .ToListAsync()) .Where(payout => { @@ -185,13 +185,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike } else { - result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, payoutHandler.Currency, cancellationToken); + result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, cancellationToken); } break; case BoltInvoiceClaimDestination item1: - result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, payoutHandler.Currency, cancellationToken); + result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, cancellationToken); break; default: @@ -276,8 +276,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike } public static async Task TrypayBolt( - ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, - string payoutCurrency, CancellationToken cancellationToken) + ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, CancellationToken cancellationToken) { var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC); if (boltAmount > payoutBlob.CryptoAmount) @@ -287,7 +286,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike { PayoutId = payoutData.Id, Result = PayResult.Error, - Message = $"The BOLT11 invoice amount ({boltAmount} {payoutCurrency}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {payoutCurrency})", + Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {payoutData.Currency})", Destination = payoutBlob.Destination }; } diff --git a/BTCPayServer/Data/Payouts/PayoutExtensions.cs b/BTCPayServer/Data/Payouts/PayoutExtensions.cs index a209eebda..257eb5684 100644 --- a/BTCPayServer/Data/Payouts/PayoutExtensions.cs +++ b/BTCPayServer/Data/Payouts/PayoutExtensions.cs @@ -31,7 +31,7 @@ namespace BTCPayServer.Data public static PayoutMethodId GetPayoutMethodId(this PayoutData data) { - return PayoutMethodId.TryParse(data.PaymentMethodId, out var pmi) ? pmi : null; + return PayoutMethodId.TryParse(data.PayoutMethodId, out var pmi) ? pmi : null; } public static string GetPayoutSource(this PayoutData data, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings) diff --git a/BTCPayServer/HostedServices/BlobMigratorHostedService.cs b/BTCPayServer/HostedServices/BlobMigratorHostedService.cs new file mode 100644 index 000000000..ae170d8bf --- /dev/null +++ b/BTCPayServer/HostedServices/BlobMigratorHostedService.cs @@ -0,0 +1,118 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Data; +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; +using static BTCPayServer.Controllers.UIInvoiceController; + +namespace BTCPayServer.HostedServices; + +public abstract class BlobMigratorHostedService : IHostedService +{ + public abstract string SettingsKey { get; } + internal class Settings + { + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset? Progress { get; set; } + public bool Complete { get; set; } + } + Task? _Migrating; + TaskCompletionSource _Cts = new TaskCompletionSource(); + public BlobMigratorHostedService( + ILogger logs, + ISettingsRepository settingsRepository, + ApplicationDbContextFactory applicationDbContextFactory) + { + Logs = logs; + SettingsRepository = settingsRepository; + ApplicationDbContextFactory = applicationDbContextFactory; + } + + public ILogger Logs { get; } + public ISettingsRepository SettingsRepository { get; } + public ApplicationDbContextFactory ApplicationDbContextFactory { get; } + + public Task StartAsync(CancellationToken cancellationToken) + { + _Migrating = Migrate(cancellationToken); + return Task.CompletedTask; + } + public int BatchSize { get; set; } = 1000; + + private async Task Migrate(CancellationToken cancellationToken) + { + var settings = await SettingsRepository.GetSettingAsync(SettingsKey) ?? new Settings(); + if (settings.Complete is true) + return; + if (settings.Progress is DateTimeOffset last) + Logs.LogInformation($"Migrating from {last}"); + else + Logs.LogInformation("Migrating from the beginning"); + + int batchSize = BatchSize; + while (!cancellationToken.IsCancellationRequested) + { +retry: + List entities; + DateTimeOffset progress; + await using (var ctx = ApplicationDbContextFactory.CreateContext()) + { + var query = GetQuery(ctx, settings?.Progress).Take(batchSize); + entities = await query.ToListAsync(cancellationToken); + if (entities.Count == 0) + { + await SettingsRepository.UpdateSetting(new Settings() { Complete = true }, SettingsKey); + Logs.LogInformation("Migration completed"); + return; + } + + try + { + progress = ProcessEntities(ctx, entities); + await ctx.SaveChangesAsync(); + batchSize = BatchSize; + } + catch (DbUpdateConcurrencyException) + { + batchSize /= 2; + batchSize = Math.Max(1, batchSize); + goto retry; + } + } + settings = new Settings() { Progress = progress }; + await SettingsRepository.UpdateSetting(settings, SettingsKey); + } + } + protected abstract IQueryable GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress); + protected abstract DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List entities); + public async Task ResetMigration() + { + await SettingsRepository.UpdateSetting(new Settings(), SettingsKey); + } + public async Task IsComplete() + { + return (await SettingsRepository.GetSettingAsync(SettingsKey)) is { Complete: true }; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _Cts.TrySetCanceled(); + return (_Migrating ?? Task.CompletedTask).ContinueWith(t => + { + if (t.IsFaulted) + Logs.LogError(t.Exception, "Error while migrating"); + }); + } +} diff --git a/BTCPayServer/HostedServices/InvoiceBlobMigratorHostedService.cs b/BTCPayServer/HostedServices/InvoiceBlobMigratorHostedService.cs index 93a607697..1833d3522 100644 --- a/BTCPayServer/HostedServices/InvoiceBlobMigratorHostedService.cs +++ b/BTCPayServer/HostedServices/InvoiceBlobMigratorHostedService.cs @@ -20,136 +20,62 @@ using static BTCPayServer.Controllers.UIInvoiceController; namespace BTCPayServer.HostedServices; -public class InvoiceBlobMigratorHostedService : IHostedService +public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService { - const string SettingsKey = "InvoiceBlobMigratorHostedService.Settings"; + private readonly PaymentMethodHandlerDictionary _handlers; - internal class Settings - { - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public DateTimeOffset? Progress { get; set; } - public bool Complete { get; set; } - } - Task? _Migrating; - TaskCompletionSource _Cts = new TaskCompletionSource(); public InvoiceBlobMigratorHostedService( ILogger logs, ISettingsRepository settingsRepository, ApplicationDbContextFactory applicationDbContextFactory, - PaymentMethodHandlerDictionary handlers) + PaymentMethodHandlerDictionary handlers) : base(logs, settingsRepository, applicationDbContextFactory) { - Logs = logs; - SettingsRepository = settingsRepository; - ApplicationDbContextFactory = applicationDbContextFactory; _handlers = handlers; } - public ILogger Logs { get; } - public ISettingsRepository SettingsRepository { get; } - public ApplicationDbContextFactory ApplicationDbContextFactory { get; } - - public Task StartAsync(CancellationToken cancellationToken) + public override string SettingsKey => "InvoicesMigration"; + protected override IQueryable GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress) { - _Migrating = Migrate(cancellationToken); - return Task.CompletedTask; + var query = progress is DateTimeOffset last2 ? + ctx.Invoices.Include(o => o.Payments).Where(i => i.Created < last2 && i.Currency == null) : + ctx.Invoices.Include(o => o.Payments).Where(i => i.Currency == null); + return query.OrderByDescending(i => i.Created); } - public int BatchSize { get; set; } = 1000; - - private async Task Migrate(CancellationToken cancellationToken) + protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List invoices) { - var settings = await SettingsRepository.GetSettingAsync(SettingsKey) ?? new Settings(); - if (settings.Complete is true) - return; - if (settings.Progress is DateTimeOffset last) - Logs.LogInformation($"Migrating invoices JSON Blobs from {last}"); - else - Logs.LogInformation("Migrating invoices JSON Blobs from the beginning"); - - int batchSize = BatchSize; - while (!cancellationToken.IsCancellationRequested) + // Those clean up the JSON blobs, and mark entities as modified + foreach (var inv in invoices) { -retry: - List invoices; - await using (var ctx = ApplicationDbContextFactory.CreateContext()) + var blob = inv.GetBlob(); + var prompts = blob.GetPaymentPrompts(); + foreach (var p in prompts) { - var query = settings.Progress is DateTimeOffset last2 ? - ctx.Invoices.Include(o => o.Payments).Where(i => i.Created < last2 && i.Currency == null) : - ctx.Invoices.Include(o => o.Payments).Where(i => i.Currency == null); - query = query.OrderByDescending(i => i.Created).Take(batchSize); - invoices = await query.ToListAsync(cancellationToken); - if (invoices.Count == 0) + if (_handlers.TryGetValue(p.PaymentMethodId, out var handler) && p.Details is not (null or { Type: JTokenType.Null })) { - await SettingsRepository.UpdateSetting(new Settings() { Complete = true }, SettingsKey); - Logs.LogInformation("Migration of invoices JSON Blobs completed"); - return; - } - - try - { - // Those clean up the JSON blobs, and mark entities as modified - foreach (var inv in invoices) - { - var blob = inv.GetBlob(); - var prompts = blob.GetPaymentPrompts(); - foreach (var p in prompts) - { - if (_handlers.TryGetValue(p.PaymentMethodId, out var handler) && p.Details is not (null or { Type: JTokenType.Null })) - { - p.Details = JToken.FromObject(handler.ParsePaymentPromptDetails(p.Details), handler.Serializer); - } - } - blob.SetPaymentPrompts(prompts); - inv.SetBlob(blob); - foreach (var pay in inv.Payments) - { - var paymentEntity = pay.GetBlob(); - if (_handlers.TryGetValue(paymentEntity.PaymentMethodId, out var handler) && paymentEntity.Details is not (null or { Type: JTokenType.Null })) - { - paymentEntity.Details = JToken.FromObject(handler.ParsePaymentDetails(paymentEntity.Details), handler.Serializer); - } - pay.SetBlob(paymentEntity); - } - } - foreach (var entry in ctx.ChangeTracker.Entries()) - { - entry.State = EntityState.Modified; - } - foreach (var entry in ctx.ChangeTracker.Entries()) - { - entry.State = EntityState.Modified; - } - await ctx.SaveChangesAsync(); - batchSize = BatchSize; - } - catch (DbUpdateConcurrencyException) - { - batchSize /= 2; - batchSize = Math.Max(1, batchSize); - goto retry; + p.Details = JToken.FromObject(handler.ParsePaymentPromptDetails(p.Details), handler.Serializer); } } - settings = new Settings() { Progress = invoices[^1].Created }; - await SettingsRepository.UpdateSetting(settings, SettingsKey); + blob.SetPaymentPrompts(prompts); + inv.SetBlob(blob); + foreach (var pay in inv.Payments) + { + var paymentEntity = pay.GetBlob(); + if (_handlers.TryGetValue(paymentEntity.PaymentMethodId, out var handler) && paymentEntity.Details is not (null or { Type: JTokenType.Null })) + { + paymentEntity.Details = JToken.FromObject(handler.ParsePaymentDetails(paymentEntity.Details), handler.Serializer); + } + pay.SetBlob(paymentEntity); + } } - } - - public async Task ResetMigration() - { - await SettingsRepository.UpdateSetting(new Settings(), SettingsKey); - } - public async Task IsComplete() - { - return (await SettingsRepository.GetSettingAsync(SettingsKey)) is { Complete: true }; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _Cts.TrySetCanceled(); - return (_Migrating ?? Task.CompletedTask).ContinueWith(t => + foreach (var entry in ctx.ChangeTracker.Entries()) { - if (t.IsFaulted) - Logs.LogError(t.Exception, "Error while migrating invoices JSON Blobs"); - }); + entry.State = EntityState.Modified; + } + foreach (var entry in ctx.ChangeTracker.Entries()) + { + entry.State = EntityState.Modified; + } + return invoices[^1].Created; } } diff --git a/BTCPayServer/HostedServices/PayoutBlobMigratorHostedService.cs b/BTCPayServer/HostedServices/PayoutBlobMigratorHostedService.cs new file mode 100644 index 000000000..229ea1a25 --- /dev/null +++ b/BTCPayServer/HostedServices/PayoutBlobMigratorHostedService.cs @@ -0,0 +1,53 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Threading; +using System.Threading.Tasks; +using AngleSharp.Dom; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Data; +using BTCPayServer.Services.Invoices; +using Google.Apis.Logging; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using static BTCPayServer.Controllers.UIInvoiceController; + +namespace BTCPayServer.HostedServices; + +public class PayoutBlobMigratorHostedService : BlobMigratorHostedService +{ + + private readonly PaymentMethodHandlerDictionary _handlers; + + public PayoutBlobMigratorHostedService( + ILogger logs, + ISettingsRepository settingsRepository, + ApplicationDbContextFactory applicationDbContextFactory, + PaymentMethodHandlerDictionary handlers) : base(logs, settingsRepository, applicationDbContextFactory) + { + _handlers = handlers; + } + + public override string SettingsKey => "PayoutsMigration"; + protected override IQueryable GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress) + { + var query = progress is DateTimeOffset last2 ? + ctx.Payouts.Where(i => i.Date < last2 && i.Currency == null) : + ctx.Payouts.Where(i => i.Currency == null); + return query.OrderByDescending(i => i); + } + protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List payouts) + { + foreach (var entry in ctx.ChangeTracker.Entries()) + { + entry.State = EntityState.Modified; + } + return payouts[^1].Date; + } +} diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index efd93f14c..fe508c551 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -217,11 +217,11 @@ namespace BTCPayServer.HostedServices if (payoutQuery.PayoutMethods.Length == 1) { var pm = payoutQuery.PayoutMethods[0]; - query = query.Where(data => pm == data.PaymentMethodId); + query = query.Where(data => pm == data.PayoutMethodId); } else { - query = query.Where(data => payoutQuery.PayoutMethods.Contains(data.PaymentMethodId)); + query = query.Where(data => payoutQuery.PayoutMethods.Contains(data.PayoutMethodId)); } } @@ -459,7 +459,7 @@ namespace BTCPayServer.HostedServices return; } - if (!PayoutMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod)) + if (!PayoutMethodId.TryParse(payout.PayoutMethodId, out var paymentMethod)) { req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null)); return; @@ -644,9 +644,10 @@ namespace BTCPayServer.HostedServices Date = now, State = PayoutState.AwaitingApproval, PullPaymentDataId = req.ClaimRequest.PullPaymentId, - PaymentMethodId = req.ClaimRequest.PayoutMethodId.ToString(), + PayoutMethodId = req.ClaimRequest.PayoutMethodId.ToString(), Destination = req.ClaimRequest.Destination.Id, - StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId + StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId, + Currency = payoutHandler.Currency }; var payoutBlob = new PayoutBlob() { @@ -693,7 +694,7 @@ namespace BTCPayServer.HostedServices StoreId = payout.StoreDataId, Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode, Status = payout.State, - PaymentMethod = payout.PaymentMethodId, + PaymentMethod = payout.PayoutMethodId, PayoutId = payout.Id }); } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index a0770ae6b..fab607e2f 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -577,6 +577,9 @@ o.GetRequiredService>().ToDictionary(o => o.P services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(o => o.GetRequiredService()); + // Broken // Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM"))); diff --git a/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs b/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs index 39b74f77d..0829f197b 100644 --- a/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs @@ -85,7 +85,7 @@ public class LightningPendingPayoutListener : BaseAsyncService } foreach (IGrouping payoutByStoreByPaymentMethod in payoutByStore.GroupBy(data => - data.PaymentMethodId)) + data.PayoutMethodId)) { var pmi = PaymentMethodId.Parse(payoutByStoreByPaymentMethod.Key); var pm = store.GetPaymentMethodConfigs(_handlers) diff --git a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs index 695e9c363..3cbe97a70 100644 --- a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs @@ -146,6 +146,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor