Migrate Payouts to new format (#5989)

* Migrate Payouts to new format

* Rename PayoutData column to PayoutMethodId
This commit is contained in:
Nicolas Dorier 2024-06-28 20:07:53 +09:00 committed by GitHub
parent c56b660c92
commit a295e123bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 303 additions and 151 deletions

View file

@ -37,6 +37,10 @@ namespace BTCPayServer.Data
{ {
paymentData.Migrate(); paymentData.Migrate();
} }
else if (entity is PayoutData payoutData && payoutData.Currency is null)
{
payoutData.Migrate();
}
return entity; return entity;
} }
} }

View file

@ -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];
}
}
}

View file

@ -10,7 +10,7 @@ using NBitcoin;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class PayoutData public partial class PayoutData
{ {
[Key] [Key]
[MaxLength(30)] [MaxLength(30)]
@ -18,12 +18,13 @@ namespace BTCPayServer.Data
public DateTimeOffset Date { get; set; } public DateTimeOffset Date { get; set; }
public string PullPaymentDataId { get; set; } public string PullPaymentDataId { get; set; }
public string StoreDataId { get; set; } public string StoreDataId { get; set; }
public string Currency { get; set; }
public PullPaymentData PullPaymentData { get; set; } public PullPaymentData PullPaymentData { get; set; }
[MaxLength(20)] [MaxLength(20)]
public PayoutState State { get; set; } public PayoutState State { get; set; }
[MaxLength(20)] [MaxLength(20)]
[Required] [Required]
public string PaymentMethodId { get; set; } public string PayoutMethodId { get; set; }
public string Blob { get; set; } public string Blob { get; set; }
public string Proof { get; set; } public string Proof { get; set; }
#nullable enable #nullable enable

View file

@ -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
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Payouts",
type: "text",
nullable: true);
migrationBuilder.RenameColumn("PaymentMethodId", "Payouts", "PayoutMethodId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View file

@ -1,4 +1,4 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Data; using BTCPayServer.Data;
@ -567,13 +567,16 @@ namespace BTCPayServer.Migrations
b.Property<string>("Blob") b.Property<string>("Blob")
.HasColumnType("JSONB"); .HasColumnType("JSONB");
b.Property<string>("Currency")
.HasColumnType("text");
b.Property<DateTimeOffset>("Date") b.Property<DateTimeOffset>("Date")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("Destination") b.Property<string>("Destination")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("PaymentMethodId") b.Property<string>("PayoutMethodId")
.IsRequired() .IsRequired()
.HasMaxLength(20) .HasMaxLength(20)
.HasColumnType("character varying(20)"); .HasColumnType("character varying(20)");

View file

@ -372,9 +372,8 @@ namespace BTCPayServer.Controllers.Greenfield
Metadata = blob.Metadata?? new JObject(), Metadata = blob.Metadata?? new JObject(),
}; };
model.Destination = blob.Destination; model.Destination = blob.Destination;
model.PaymentMethod = p.PaymentMethodId; model.PaymentMethod = p.PayoutMethodId;
var currency = this._payoutHandlers.TryGet(p.GetPayoutMethodId())?.Currency; model.CryptoCode = p.Currency;
model.CryptoCode = currency;
model.PaymentProof = p.GetProofBlobJson(); model.PaymentProof = p.GetProofBlobJson();
return model; return model;
} }

View file

@ -179,7 +179,7 @@ namespace BTCPayServer
lightningHandler.CreateLightningClient(pm); lightningHandler.CreateLightningClient(pm);
var payResult = await UILightningLikePayoutController.TrypayBolt(client, var payResult = await UILightningLikePayoutController.TrypayBolt(client,
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings), claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
claimResponse.PayoutData, result, payoutHandler.Currency, cancellationToken); claimResponse.PayoutData, result, cancellationToken);
switch (payResult.Result) switch (payResult.Result)
{ {

View file

@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers
Currency = blob.Currency, Currency = blob.Currency,
Status = entity.Entity.State, Status = entity.Entity.State,
Destination = entity.Blob.Destination, Destination = entity.Blob.Destination,
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId), PaymentMethod = PaymentMethodId.Parse(entity.Entity.PayoutMethodId),
Link = entity.ProofBlob?.Link, Link = entity.ProofBlob?.Link,
TransactionId = entity.ProofBlob?.Id TransactionId = entity.ProofBlob?.Id
}).ToList() }).ToList()

View file

@ -509,14 +509,14 @@ namespace BTCPayServer.Controllers
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name; 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()) .Select(datas => new { datas.Key, Count = datas.Count() }).ToListAsync())
.ToDictionary(datas => datas.Key, arg => arg.Count); .ToDictionary(datas => datas.Key, arg => arg.Count);
if (vm.PayoutMethodId != null) if (vm.PayoutMethodId != null)
{ {
var pmiStr = vm.PayoutMethodId; 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) vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State)
.Select(e => new { e.Key, Count = e.Count() }) .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 }); 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 var m = new PayoutsModel.PayoutModel
{ {
@ -572,7 +571,7 @@ namespace BTCPayServer.Controllers
SourceLink = payoutSourceLink, SourceLink = payoutSourceLink,
Date = item.Payout.Date, Date = item.Payout.Date,
PayoutId = item.Payout.Id, 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 Destination = payoutBlob.Destination
}; };
var handler = _payoutHandlers var handler = _payoutHandlers

View file

@ -227,7 +227,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
Stores = new[] { storeId }, Stores = new[] { storeId },
PayoutIds = payoutIds PayoutIds = payoutIds
}, context)).Where(data => }, context)).Where(data =>
PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) && PayoutMethodId.TryParse(data.PayoutMethodId, out var payoutMethodId) &&
payoutMethodId == PayoutMethodId) payoutMethodId == PayoutMethodId)
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false); .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) foreach (var valueTuple in payouts)
@ -252,7 +252,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
Stores = new[] { storeId }, Stores = new[] { storeId },
PayoutIds = payoutIds PayoutIds = payoutIds
}, context)).Where(data => }, context)).Where(data =>
PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) && PayoutMethodId.TryParse(data.PayoutMethodId, out var payoutMethodId) &&
payoutMethodId == PayoutMethodId) payoutMethodId == PayoutMethodId)
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true); .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) foreach (var valueTuple in payouts)
@ -285,7 +285,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = await ctx.Payouts.Include(data => data.PullPaymentData) var payouts = await ctx.Payouts.Include(data => data.PullPaymentData)
.Where(data => payoutIds.Contains(data.Id) .Where(data => payoutIds.Contains(data.Id)
&& PayoutMethodId.ToString() == data.PaymentMethodId && PayoutMethodId.ToString() == data.PayoutMethodId
&& data.State == PayoutState.AwaitingPayment) && data.State == PayoutState.AwaitingPayment)
.ToListAsync(); .ToListAsync();
@ -428,7 +428,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
.Include(o => o.StoreData) .Include(o => o.StoreData)
.Include(o => o.PullPaymentData) .Include(o => o.PullPaymentData)
.Where(p => p.State == PayoutState.AwaitingPayment) .Where(p => p.State == PayoutState.AwaitingPayment)
.Where(p => p.PaymentMethodId == paymentMethodId.ToString()) .Where(p => p.PayoutMethodId == paymentMethodId.ToString())
#pragma warning disable CA1307 // Specify StringComparison #pragma warning disable CA1307 // Specify StringComparison
.Where(p => destination.Equals(p.Destination)) .Where(p => destination.Equals(p.Destination))
#pragma warning restore CA1307 // Specify StringComparison #pragma warning restore CA1307 // Specify StringComparison
@ -474,7 +474,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId), await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
new ExternalPayoutTransactionNotification() new ExternalPayoutTransactionNotification()
{ {
PaymentMethod = payout.PaymentMethodId, PaymentMethod = payout.PayoutMethodId,
PayoutId = payout.Id, PayoutId = payout.Id,
StoreId = payout.StoreDataId StoreId = payout.StoreDataId
}); });

View file

@ -86,7 +86,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
.Where(data => .Where(data =>
payoutIds.Contains(data.Id) && payoutIds.Contains(data.Id) &&
data.State == PayoutState.AwaitingPayment && data.State == PayoutState.AwaitingPayment &&
data.PaymentMethodId == pmiStr) data.PayoutMethodId == pmiStr)
.ToListAsync()) .ToListAsync())
.Where(payout => .Where(payout =>
{ {
@ -185,13 +185,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
} }
else else
{ {
result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, payoutHandler.Currency, cancellationToken); result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, cancellationToken);
} }
break; break;
case BoltInvoiceClaimDestination item1: case BoltInvoiceClaimDestination item1:
result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, payoutHandler.Currency, cancellationToken); result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, cancellationToken);
break; break;
default: default:
@ -276,8 +276,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
} }
public static async Task<ResultVM> TrypayBolt( public static async Task<ResultVM> TrypayBolt(
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, CancellationToken cancellationToken)
string payoutCurrency, CancellationToken cancellationToken)
{ {
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC); var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount > payoutBlob.CryptoAmount) if (boltAmount > payoutBlob.CryptoAmount)
@ -287,7 +286,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
Result = PayResult.Error, 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 Destination = payoutBlob.Destination
}; };
} }

View file

@ -31,7 +31,7 @@ namespace BTCPayServer.Data
public static PayoutMethodId GetPayoutMethodId(this PayoutData 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) public static string GetPayoutSource(this PayoutData data, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)

View file

@ -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<TEntity> : 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<Settings>(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<TEntity> 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<Settings>(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>(settings, SettingsKey);
}
}
protected abstract IQueryable<TEntity> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress);
protected abstract DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<TEntity> entities);
public async Task ResetMigration()
{
await SettingsRepository.UpdateSetting<Settings>(new Settings(), SettingsKey);
}
public async Task<bool> IsComplete()
{
return (await SettingsRepository.GetSettingAsync<Settings>(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");
});
}
}

View file

@ -20,136 +20,62 @@ using static BTCPayServer.Controllers.UIInvoiceController;
namespace BTCPayServer.HostedServices; namespace BTCPayServer.HostedServices;
public class InvoiceBlobMigratorHostedService : IHostedService public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService<InvoiceData>
{ {
const string SettingsKey = "InvoiceBlobMigratorHostedService.Settings";
private readonly PaymentMethodHandlerDictionary _handlers; 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( public InvoiceBlobMigratorHostedService(
ILogger<InvoiceBlobMigratorHostedService> logs, ILogger<InvoiceBlobMigratorHostedService> logs,
ISettingsRepository settingsRepository, ISettingsRepository settingsRepository,
ApplicationDbContextFactory applicationDbContextFactory, ApplicationDbContextFactory applicationDbContextFactory,
PaymentMethodHandlerDictionary handlers) PaymentMethodHandlerDictionary handlers) : base(logs, settingsRepository, applicationDbContextFactory)
{ {
Logs = logs;
SettingsRepository = settingsRepository;
ApplicationDbContextFactory = applicationDbContextFactory;
_handlers = handlers; _handlers = handlers;
} }
public ILogger<InvoiceBlobMigratorHostedService> Logs { get; } public override string SettingsKey => "InvoicesMigration";
public ISettingsRepository SettingsRepository { get; } protected override IQueryable<InvoiceData> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress)
public ApplicationDbContextFactory ApplicationDbContextFactory { get; }
public Task StartAsync(CancellationToken cancellationToken)
{ {
_Migrating = Migrate(cancellationToken); var query = progress is DateTimeOffset last2 ?
return Task.CompletedTask; 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; protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<InvoiceData> invoices)
private async Task Migrate(CancellationToken cancellationToken)
{ {
var settings = await SettingsRepository.GetSettingAsync<Settings>(SettingsKey) ?? new Settings(); // Those clean up the JSON blobs, and mark entities as modified
if (settings.Complete is true) foreach (var inv in invoices)
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)
{ {
retry: var blob = inv.GetBlob();
List<InvoiceData> invoices; var prompts = blob.GetPaymentPrompts();
await using (var ctx = ApplicationDbContextFactory.CreateContext()) foreach (var p in prompts)
{ {
var query = settings.Progress is DateTimeOffset last2 ? if (_handlers.TryGetValue(p.PaymentMethodId, out var handler) && p.Details is not (null or { Type: JTokenType.Null }))
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)
{ {
await SettingsRepository.UpdateSetting<Settings>(new Settings() { Complete = true }, SettingsKey); p.Details = JToken.FromObject(handler.ParsePaymentPromptDetails(p.Details), handler.Serializer);
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<InvoiceData>())
{
entry.State = EntityState.Modified;
}
foreach (var entry in ctx.ChangeTracker.Entries<PaymentData>())
{
entry.State = EntityState.Modified;
}
await ctx.SaveChangesAsync();
batchSize = BatchSize;
}
catch (DbUpdateConcurrencyException)
{
batchSize /= 2;
batchSize = Math.Max(1, batchSize);
goto retry;
} }
} }
settings = new Settings() { Progress = invoices[^1].Created }; blob.SetPaymentPrompts(prompts);
await SettingsRepository.UpdateSetting<Settings>(settings, SettingsKey); 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<InvoiceData>())
public async Task ResetMigration()
{
await SettingsRepository.UpdateSetting<Settings>(new Settings(), SettingsKey);
}
public async Task<bool> IsComplete()
{
return (await SettingsRepository.GetSettingAsync<Settings>(SettingsKey)) is { Complete: true };
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.TrySetCanceled();
return (_Migrating ?? Task.CompletedTask).ContinueWith(t =>
{ {
if (t.IsFaulted) entry.State = EntityState.Modified;
Logs.LogError(t.Exception, "Error while migrating invoices JSON Blobs"); }
}); foreach (var entry in ctx.ChangeTracker.Entries<PaymentData>())
{
entry.State = EntityState.Modified;
}
return invoices[^1].Created;
} }
} }

View file

@ -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<PayoutData>
{
private readonly PaymentMethodHandlerDictionary _handlers;
public PayoutBlobMigratorHostedService(
ILogger<PayoutBlobMigratorHostedService> logs,
ISettingsRepository settingsRepository,
ApplicationDbContextFactory applicationDbContextFactory,
PaymentMethodHandlerDictionary handlers) : base(logs, settingsRepository, applicationDbContextFactory)
{
_handlers = handlers;
}
public override string SettingsKey => "PayoutsMigration";
protected override IQueryable<PayoutData> 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<PayoutData> payouts)
{
foreach (var entry in ctx.ChangeTracker.Entries<PayoutData>())
{
entry.State = EntityState.Modified;
}
return payouts[^1].Date;
}
}

View file

@ -217,11 +217,11 @@ namespace BTCPayServer.HostedServices
if (payoutQuery.PayoutMethods.Length == 1) if (payoutQuery.PayoutMethods.Length == 1)
{ {
var pm = payoutQuery.PayoutMethods[0]; var pm = payoutQuery.PayoutMethods[0];
query = query.Where(data => pm == data.PaymentMethodId); query = query.Where(data => pm == data.PayoutMethodId);
} }
else 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; 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)); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
return; return;
@ -644,9 +644,10 @@ namespace BTCPayServer.HostedServices
Date = now, Date = now,
State = PayoutState.AwaitingApproval, State = PayoutState.AwaitingApproval,
PullPaymentDataId = req.ClaimRequest.PullPaymentId, PullPaymentDataId = req.ClaimRequest.PullPaymentId,
PaymentMethodId = req.ClaimRequest.PayoutMethodId.ToString(), PayoutMethodId = req.ClaimRequest.PayoutMethodId.ToString(),
Destination = req.ClaimRequest.Destination.Id, Destination = req.ClaimRequest.Destination.Id,
StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId,
Currency = payoutHandler.Currency
}; };
var payoutBlob = new PayoutBlob() var payoutBlob = new PayoutBlob()
{ {
@ -693,7 +694,7 @@ namespace BTCPayServer.HostedServices
StoreId = payout.StoreDataId, StoreId = payout.StoreDataId,
Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode, Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode,
Status = payout.State, Status = payout.State,
PaymentMethod = payout.PaymentMethodId, PaymentMethod = payout.PayoutMethodId,
PayoutId = payout.Id PayoutId = payout.Id
}); });
} }

View file

@ -577,6 +577,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<InvoiceBlobMigratorHostedService>(); services.AddSingleton<InvoiceBlobMigratorHostedService>();
services.AddSingleton<IHostedService, InvoiceBlobMigratorHostedService>(o => o.GetRequiredService<InvoiceBlobMigratorHostedService>()); services.AddSingleton<IHostedService, InvoiceBlobMigratorHostedService>(o => o.GetRequiredService<InvoiceBlobMigratorHostedService>());
services.AddSingleton<PayoutBlobMigratorHostedService>();
services.AddSingleton<IHostedService, PayoutBlobMigratorHostedService>(o => o.GetRequiredService<PayoutBlobMigratorHostedService>());
// Broken // Broken
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM"))); // Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));

View file

@ -85,7 +85,7 @@ public class LightningPendingPayoutListener : BaseAsyncService
} }
foreach (IGrouping<string, PayoutData> payoutByStoreByPaymentMethod in payoutByStore.GroupBy(data => foreach (IGrouping<string, PayoutData> payoutByStoreByPaymentMethod in payoutByStore.GroupBy(data =>
data.PaymentMethodId)) data.PayoutMethodId))
{ {
var pmi = PaymentMethodId.Parse(payoutByStoreByPaymentMethod.Key); var pmi = PaymentMethodId.Parse(payoutByStoreByPaymentMethod.Key);
var pm = store.GetPaymentMethodConfigs(_handlers) var pm = store.GetPaymentMethodConfigs(_handlers)

View file

@ -146,6 +146,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
{ {
return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData, return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData,
bolt11PaymentRequest, bolt11PaymentRequest,
_payoutHandler.Currency, CancellationToken)).Result is PayResult.Ok ; CancellationToken)).Result is PayResult.Ok ;
} }
} }

View file

@ -54,8 +54,7 @@ public class PayoutsReportProvider : ReportProvider
data.Add(payout.Date); data.Add(payout.Date);
data.Add(payout.GetPayoutSource(_btcPayNetworkJsonSerializerSettings)); data.Add(payout.GetPayoutSource(_btcPayNetworkJsonSerializerSettings));
data.Add(payout.State.ToString()); data.Add(payout.State.ToString());
string? payoutCurrency; if (PayoutMethodId.TryParse(payout.PayoutMethodId, out var pmi))
if (PayoutMethodId.TryParse(payout.PaymentMethodId, out var pmi))
{ {
var handler = _handlers.TryGet(pmi); var handler = _handlers.TryGet(pmi);
if (handler is LightningLikePayoutHandler) if (handler is LightningLikePayoutHandler)
@ -64,17 +63,16 @@ public class PayoutsReportProvider : ReportProvider
data.Add("On-Chain"); data.Add("On-Chain");
else else
data.Add(pmi.ToString()); data.Add(pmi.ToString());
payoutCurrency = handler?.Currency;
} }
else else
continue; continue;
var ppBlob = payout.PullPaymentData?.GetBlob(); var ppBlob = payout.PullPaymentData?.GetBlob();
var currency = ppBlob?.Currency ?? payoutCurrency; var currency = ppBlob?.Currency ?? payout.Currency;
if (currency is null) if (currency is null)
continue; continue;
data.Add(payoutCurrency); data.Add(payout.Currency);
data.Add(blob.CryptoAmount.HasValue && payoutCurrency is not null ? _displayFormatter.ToFormattedAmount(blob.CryptoAmount.Value, payoutCurrency) : null); data.Add(blob.CryptoAmount is decimal v ? _displayFormatter.ToFormattedAmount(v, payout.Currency) : null);
data.Add(currency); data.Add(currency);
data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency)); data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency));
data.Add(blob.Destination); data.Add(blob.Destination);

View file

@ -67,7 +67,7 @@ namespace BTCPayServer.Services.Reporting
var conn = ctx.Database.GetDbConnection(); var conn = ctx.Database.GetDbConnection();
var rows = await conn.QueryAsync( var rows = await conn.QueryAsync(
""" """
SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PaymentMethodId", pp."Id" AS "PullPaymentId", pp."Blob" AS "ppBlob", p."Blob" AS "pBlob" FROM "Invoices" i SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PayoutMethodId", p."Currency" AS "PayoutCurrency", pp."Id" AS "PullPaymentId", pp."Blob" AS "ppBlob", p."Blob" AS "pBlob" FROM "Invoices" i
JOIN "Refunds" r ON r."InvoiceDataId"= i."Id" JOIN "Refunds" r ON r."InvoiceDataId"= i."Id"
JOIN "PullPayments" pp ON r."PullPaymentDataId"=pp."Id" JOIN "PullPayments" pp ON r."PullPaymentDataId"=pp."Id"
LEFT JOIN "Payouts" p ON p."PullPaymentDataId"=pp."Id" LEFT JOIN "Payouts" p ON p."PullPaymentDataId"=pp."Id"
@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Reporting
if (r.pBlob is null) if (r.pBlob is null)
return null; return null;
Data.PayoutData p = new Data.PayoutData(); Data.PayoutData p = new Data.PayoutData();
p.PaymentMethodId = r.PaymentMethodId; p.PayoutMethodId = r.PayoutMethodId;
p.Currency = (string)r.PayoutCurrency;
p.Blob = (string)r.pBlob; p.Blob = (string)r.pBlob;
return p.GetBlob(_serializerSettings); return p.GetBlob(_serializerSettings);
} }