Refactor Payouts and PullPayments DB models (#6173)

This commit is contained in:
Nicolas Dorier 2024-08-28 18:52:08 +09:00 committed by GitHub
parent 3c40dc1f49
commit 1dd37c5020
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 389 additions and 261 deletions

View File

@ -19,5 +19,6 @@
</ItemGroup>
<ItemGroup>
<None Remove="DBScripts\001.InvoiceFunctions.sql" />
<None Remove="DBScripts\002.RefactorPayouts.sql" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,56 @@
-- Rename column
ALTER TABLE "Payouts" RENAME COLUMN "PaymentMethodId" TO "PayoutMethodId";
-- Add Currency column, guessed from the PaymentMethodId
ALTER TABLE "Payouts" ADD COLUMN "Currency" TEXT;
UPDATE "Payouts" SET
"Currency" = split_part("PayoutMethodId", '_', 1),
"PayoutMethodId"=
CASE
WHEN split_part("PayoutMethodId", '_', 2) = 'LightningLike' THEN split_part("PayoutMethodId", '_', 1) || '-LN'
ELSE split_part("PayoutMethodId", '_', 1) || '-CHAIN'
END;
ALTER TABLE "Payouts" ALTER COLUMN "Currency" SET NOT NULL;
-- Remove Currency and Limit from PullPayment Blob, and put it into the columns in the table
ALTER TABLE "PullPayments" ADD COLUMN "Currency" TEXT;
UPDATE "PullPayments" SET "Currency" = "Blob"->>'Currency';
ALTER TABLE "PullPayments" ALTER COLUMN "Currency" SET NOT NULL;
ALTER TABLE "PullPayments" ADD COLUMN "Limit" NUMERIC;
UPDATE "PullPayments" SET "Limit" = ("Blob"->>'Limit')::NUMERIC;
ALTER TABLE "PullPayments" ALTER COLUMN "Limit" SET NOT NULL;
-- Remove unused properties, rename SupportedPaymentMethods, and fix legacy payment methods IDs
UPDATE "PullPayments" SET
"Blob" = jsonb_set(
"Blob" - 'SupportedPaymentMethods' - 'Limit' - 'Currency' - 'Period',
'{SupportedPayoutMethods}',
(SELECT jsonb_agg(to_jsonb(
CASE
WHEN split_part(value::TEXT, '_', 2) = 'LightningLike' THEN split_part(value::TEXT, '_', 1) || '-LN'
ELSE split_part(value::TEXT, '_', 1) || '-CHAIN'
END))
FROM jsonb_array_elements_text("Blob"->'SupportedPaymentMethods') AS value
));
--Remove "Amount" and "CryptoAmount" from Payout Blob, and put it into the columns in the table
-- Respectively "OriginalAmount" and "Amount"
ALTER TABLE "Payouts" ADD COLUMN "Amount" NUMERIC;
UPDATE "Payouts" SET "Amount" = ("Blob"->>'CryptoAmount')::NUMERIC;
ALTER TABLE "Payouts" ADD COLUMN "OriginalAmount" NUMERIC;
UPDATE "Payouts" SET "OriginalAmount" = ("Blob"->>'Amount')::NUMERIC;
ALTER TABLE "Payouts" ALTER COLUMN "OriginalAmount" SET NOT NULL;
ALTER TABLE "Payouts" ADD COLUMN "OriginalCurrency" TEXT;
UPDATE "Payouts" p
SET
"OriginalCurrency" = CASE WHEN p."PullPaymentDataId" IS NULL THEN p."Currency" ELSE pp."Currency" END,
"Blob" = p."Blob" - 'Amount' - 'CryptoAmount'
FROM "PullPayments" pp
WHERE pp."Id" = p."PullPaymentDataId" OR p."PullPaymentDataId" IS NULL;
ALTER TABLE "Payouts" ALTER COLUMN "OriginalCurrency" SET NOT NULL;

View File

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

View File

@ -1,18 +0,0 @@
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

@ -18,7 +18,23 @@ namespace BTCPayServer.Data
public DateTimeOffset Date { get; set; }
public string PullPaymentDataId { get; set; }
public string StoreDataId { get; set; }
/// <summary>
/// The currency of the payout (eg. BTC)
/// </summary>
public string Currency { get; set; }
/// <summary>
/// The amount of the payout in Currency.
/// The Amount only get set when the payout is actually approved.
/// </summary>
public decimal? Amount { get; set; }
/// <summary>
/// The original currency of the payout (eg. USD)
/// </summary>
public string OriginalCurrency { get; set; }
/// <summary>
/// The amount of the payout in OriginalCurrency
/// </summary>
public decimal OriginalAmount { get; set; }
public PullPaymentData PullPaymentData { get; set; }
[MaxLength(20)]
public PayoutState State { get; set; }

View File

@ -24,6 +24,8 @@ namespace BTCPayServer.Data
public StoreData StoreData { get; set; }
[MaxLength(50)]
public string StoreId { get; set; }
public string Currency { get; set; }
public decimal Limit { get; set; }
public DateTimeOffset StartDate { get; set; }
public DateTimeOffset? EndDate { get; set; }
public bool Archived { get; set; }

View File

@ -1,29 +0,0 @@
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

@ -0,0 +1,15 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240827034505_migratepayouts")]
[DBScript("002.RefactorPayouts.sql")]
public partial class migratepayouts : DBScriptsMigration
{
}
}

View File

@ -553,6 +553,9 @@ namespace BTCPayServer.Migrations
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("Amount")
.HasColumnType("numeric");
b.Property<string>("Blob")
.HasColumnType("JSONB");
@ -565,6 +568,12 @@ namespace BTCPayServer.Migrations
b.Property<string>("Destination")
.HasColumnType("text");
b.Property<decimal>("OriginalAmount")
.HasColumnType("numeric");
b.Property<string>("OriginalCurrency")
.HasColumnType("text");
b.Property<string>("PayoutMethodId")
.IsRequired()
.HasMaxLength(20)
@ -664,9 +673,15 @@ namespace BTCPayServer.Migrations
b.Property<string>("Blob")
.HasColumnType("JSONB");
b.Property<string>("Currency")
.HasColumnType("text");
b.Property<DateTimeOffset?>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Limit")
.HasColumnType("numeric");
b.Property<DateTimeOffset>("StartDate")
.HasColumnType("timestamp with time zone");

View File

@ -0,0 +1,90 @@
using System;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Tests.Logging;
using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using Npgsql;
namespace BTCPayServer.Tests
{
public class DatabaseTester
{
private readonly ILoggerFactory _loggerFactory;
private readonly string dbname;
private string[] notAppliedMigrations;
public DatabaseTester(ILog log, ILoggerFactory loggerFactory)
{
var connStr = Environment.GetEnvironmentVariable("TESTS_POSTGRES");
if (string.IsNullOrEmpty(connStr))
connStr = ServerTester.DefaultConnectionString;
var r = RandomUtils.GetUInt32();
dbname = $"btcpayserver{r}";
connStr = connStr.Replace("btcpayserver", dbname);
log.LogInformation("DB: " + dbname);
ConnectionString = connStr;
_loggerFactory = loggerFactory;
}
public ApplicationDbContextFactory CreateContextFactory()
{
return new ApplicationDbContextFactory(new OptionsWrapper<DatabaseOptions>(new DatabaseOptions()
{
ConnectionString = ConnectionString
}), _loggerFactory);
}
public ApplicationDbContext CreateContext() => CreateContextFactory().CreateContext();
public async Task MigrateAsync()
{
using var ctx = CreateContext();
await EnsureCreatedAsync();
await ctx.Database.MigrateAsync();
}
private async Task EnsureCreatedAsync()
{
var builder = new Npgsql.NpgsqlConnectionStringBuilder(ConnectionString);
builder.Database = null;
NpgsqlConnection conn = new NpgsqlConnection(builder.ToString());
await conn.ExecuteAsync($"CREATE DATABASE \"{dbname}\";");
}
public async Task MigrateUntil(string migration)
{
using var ctx = CreateContext();
var db = ctx.Database.GetDbConnection();
await EnsureCreatedAsync();
var migrations = ctx.Database.GetMigrations().ToArray();
var untilMigrationIdx = Array.IndexOf(migrations, migration);
if (untilMigrationIdx == -1)
throw new InvalidOperationException($"Migration {migration} not found");
notAppliedMigrations = migrations[untilMigrationIdx..];
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
await ctx.Database.MigrateAsync();
}
public async Task ContinueMigration()
{
if (notAppliedMigrations is null)
throw new InvalidOperationException("Call MigrateUpTo first");
using var ctx = CreateContext();
var db = ctx.Database.GetDbConnection();
await db.ExecuteAsync("DELETE FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = ANY (@migrations)", new { migrations = notAppliedMigrations });
await ctx.Database.MigrateAsync();
notAppliedMigrations = null;
}
public string ConnectionString { get; }
}
}

View File

@ -0,0 +1,80 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using Dapper;
using Microsoft.EntityFrameworkCore;
using NBitcoin.Altcoins;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Integration", "Integration")]
public class DatabaseTests : UnitTestBase
{
public DatabaseTests(ITestOutputHelper helper):base(helper)
{
}
[Fact]
public async Task CanMigratePayoutsAndPullPayments()
{
var tester = CreateDBTester();
await tester.MigrateUntil("20240827034505_migratepayouts");
using var ctx = tester.CreateContext();
var conn = ctx.Database.GetDbConnection();
await conn.ExecuteAsync("INSERT INTO \"Stores\"(\"Id\", \"SpeedPolicy\") VALUES (@store, 0)", new { store = "store1" });
var param = new
{
Id = "pp1",
StoreId = "store1",
Blob = "{\"Name\": \"CoinLottery\", \"View\": {\"Email\": null, \"Title\": \"\", \"Description\": \"\", \"EmbeddedCSS\": null, \"CustomCSSLink\": null}, \"Limit\": \"10.00\", \"Period\": null, \"Currency\": \"GBP\", \"Description\": \"\", \"Divisibility\": 0, \"MinimumClaim\": \"0\", \"AutoApproveClaims\": false, \"SupportedPaymentMethods\": [\"BTC\", \"BTC_LightningLike\"]}"
};
await conn.ExecuteAsync("INSERT INTO \"PullPayments\"(\"Id\", \"StoreId\", \"Blob\", \"StartDate\", \"Archived\") VALUES (@Id, @StoreId, @Blob::JSONB, NOW(), 'f')", param);
var parameters = new[]
{
new
{
Id = "p1",
StoreId = "store1",
PullPaymentDataId = "pp1",
PaymentMethodId = "BTC",
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": \"0.00012225\", \"MinimumConfirmation\": 1}"
},
new
{
Id = "p2",
StoreId = "store1",
PullPaymentDataId = "pp1",
PaymentMethodId = "BTC_LightningLike",
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
},
new
{
Id = "p3",
StoreId = "store1",
PullPaymentDataId = null as string,
PaymentMethodId = "BTC_LightningLike",
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
}
};
await conn.ExecuteAsync("INSERT INTO \"Payouts\"(\"Id\", \"StoreDataId\", \"PullPaymentDataId\", \"PaymentMethodId\", \"Blob\", \"State\", \"Date\") VALUES (@Id, @StoreId, @PullPaymentDataId, @PaymentMethodId, @Blob::JSONB, 'state', NOW())", parameters);
await tester.ContinueMigration();
var migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"PullPayments\" WHERE \"Id\"='pp1' AND \"Limit\"=10.0 AND \"Currency\"='GBP' AND \"Blob\"->>'SupportedPayoutMethods'='[\"BTC-CHAIN\", \"BTC-LN\"]'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p1' AND \"Amount\"= 0.00012225 AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-CHAIN'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p2' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-LN'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p3' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='BTC'");
Assert.True(migrated);
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -121,13 +122,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SignTransaction")).Click();
}
private static string ExtractPSBT(SeleniumTester s)
{
var pageSource = s.Driver.PageSource;
var start = pageSource.IndexOf("id=\"psbt-base64\">");
start += "id=\"psbt-base64\">".Length;
var end = pageSource.IndexOf("<", start);
return pageSource[start..end];
}
private string ExtractPSBT(SeleniumTester s) => s.Driver.FindElement(By.Id("psbt-base64")).GetAttribute("innerText");
}
}

View File

@ -25,6 +25,7 @@ namespace BTCPayServer.Tests
{
public class ServerTester : IDisposable
{
public const string DefaultConnectionString = "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver";
public List<IDisposable> Resources = new List<IDisposable>();
readonly string _Directory;
@ -54,7 +55,7 @@ namespace BTCPayServer.Tests
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
// since in dev we already can have some users / stores registered, while on CI database is being initalized
// for the first time and first registered user gets admin status by default
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
Postgres = GetEnvironment("TESTS_POSTGRES", DefaultConnectionString),
ExplorerPostgres = GetEnvironment("TESTS_EXPLORER_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
};

View File

@ -29,6 +29,11 @@ namespace BTCPayServer.Tests
BTCPayLogs.Configure(LoggerFactory);
}
public DatabaseTester CreateDBTester()
{
return new DatabaseTester(TestLogs, LoggerFactory);
}
public BTCPayNetworkProvider CreateNetworkProvider(ChainName chainName)
{
var conf = new ConfigurationRoot(new List<IConfigurationProvider>()

View File

@ -567,10 +567,10 @@ namespace BTCPayServer.Controllers.Greenfield
Id = pp.Id,
StartsAt = pp.StartDate,
ExpiresAt = pp.EndDate,
Amount = ppBlob.Limit,
Amount = pp.Limit,
Name = ppBlob.Name,
Description = ppBlob.Description,
Currency = ppBlob.Currency,
Currency = pp.Currency,
Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration,

View File

@ -167,10 +167,10 @@ namespace BTCPayServer.Controllers.Greenfield
Id = pp.Id,
StartsAt = pp.StartDate,
ExpiresAt = pp.EndDate,
Amount = ppBlob.Limit,
Amount = pp.Limit,
Name = ppBlob.Name,
Description = ppBlob.Description,
Currency = ppBlob.Currency,
Currency = pp.Currency,
Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration,
@ -223,7 +223,7 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
return this.CreateValidationError(ModelState);
}
if (!_pullPaymentService.SupportsLNURL(pp.GetBlob()))
if (!_pullPaymentService.SupportsLNURL(pp))
{
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
}
@ -338,8 +338,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (pp is null)
return PullPaymentNotFound();
var blob = pp.GetBlob();
if (_pullPaymentService.SupportsLNURL(blob))
if (_pullPaymentService.SupportsLNURL(pp))
{
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
{
@ -365,8 +364,8 @@ namespace BTCPayServer.Controllers.Greenfield
Id = p.Id,
PullPaymentId = p.PullPaymentDataId,
Date = p.Date,
Amount = blob.Amount,
PaymentMethodAmount = blob.CryptoAmount,
Amount = p.OriginalAmount,
PaymentMethodAmount = p.Amount,
Revision = blob.Revision,
State = p.State,
Metadata = blob.Metadata?? new JObject(),
@ -407,7 +406,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, payoutHandler.Currency, ppBlob.Currency);
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, payoutHandler.Currency, pp.Currency);
if (amtError.error is not null)
{
ModelState.AddModelError(nameof(request.Amount), amtError.error );

View File

@ -115,12 +115,12 @@ namespace BTCPayServer
}
var blob = pp.GetBlob();
if (!_pullPaymentHostedService.SupportsLNURL(blob))
if (!_pullPaymentHostedService.SupportsLNURL(pp, blob))
{
return NotFound();
}
var unit = blob.Currency == "SATS" ? LightMoneyUnit.Satoshi : LightMoneyUnit.BTC;
var unit = pp.Currency == "SATS" ? LightMoneyUnit.Satoshi : LightMoneyUnit.BTC;
var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, DateTimeOffset.UtcNow);
var remaining = progress.Limit - progress.Completed - progress.Awaiting;
var request = new LNURLWithdrawRequest

View File

@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, false);
if (pp is null)
return NotFound();
if (!_pullPaymentHostedService.SupportsLNURL(pp.GetBlob()))
if (!_pullPaymentHostedService.SupportsLNURL(pp))
return BadRequest();
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");

View File

@ -83,7 +83,6 @@ namespace BTCPayServer.Controllers
if (pp is null)
return NotFound();
var blob = pp.GetBlob();
var store = await _storeRepository.FindStore(pp.StoreId);
if (store is null)
return NotFound();
@ -98,9 +97,9 @@ namespace BTCPayServer.Controllers
Blob = o.GetBlob(_serializerSettings),
ProofBlob = _payoutHandlers.TryGet(o.GetPayoutMethodId())?.ParseProof(o)
});
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
var amountDue = blob.Limit - totalPaid;
var cd = _currencyNameTable.GetCurrencyData(pp.Currency, false);
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Entity.OriginalAmount).Sum();
var amountDue = pp.Limit - totalPaid;
ViewPullPaymentModel vm = new(pp, DateTimeOffset.UtcNow)
{
@ -113,9 +112,9 @@ namespace BTCPayServer.Controllers
Payouts = payouts.Select(entity => new ViewPullPaymentModel.PayoutLine
{
Id = entity.Entity.Id,
Amount = entity.Blob.Amount,
AmountFormatted = _displayFormatter.Currency(entity.Blob.Amount, blob.Currency),
Currency = blob.Currency,
Amount = entity.Entity.OriginalAmount,
AmountFormatted = _displayFormatter.Currency(entity.Entity.OriginalAmount, entity.Entity.OriginalCurrency),
Currency = entity.Entity.OriginalCurrency,
Status = entity.Entity.State,
Destination = entity.Blob.Destination,
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PayoutMethodId),
@ -126,7 +125,7 @@ namespace BTCPayServer.Controllers
vm.IsPending &= vm.AmountDue > 0.0m;
vm.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
if (_pullPaymentHostedService.SupportsLNURL(blob))
if (_pullPaymentHostedService.SupportsLNURL(pp))
{
var url = Url.Action(nameof(UILNURLController.GetLNURLForPullPayment), "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
@ -222,7 +221,7 @@ namespace BTCPayServer.Controllers
}
var ppBlob = pp.GetBlob();
var supported = ppBlob.SupportedPaymentMethods;
var supported = ppBlob.SupportedPayoutMethods;
PayoutMethodId payoutMethodId = null;
IClaimDestination destination = null;
IPayoutHandler payoutHandler = null;
@ -260,7 +259,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Destination), error ?? "Invalid destination or payment method");
return await ViewPullPayment(pullPaymentId);
}
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, payoutHandler.Currency, ppBlob.Currency);
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, payoutHandler.Currency, pp.Currency);
if (amtError.error is not null)
{
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error);
@ -294,7 +293,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, pp.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Severity = StatusMessageModel.StatusSeverity.Success
});

View File

@ -571,7 +571,7 @@ namespace BTCPayServer.Controllers
SourceLink = payoutSourceLink,
Date = item.Payout.Date,
PayoutId = item.Payout.Id,
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? item.Payout.Currency),
Amount = _displayFormatter.Currency(item.Payout.OriginalAmount, item.Payout.OriginalCurrency),
Destination = payoutBlob.Destination
};
var handler = _payoutHandlers

View File

@ -305,13 +305,13 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
switch (claim.destination)
{
case UriClaimDestination uriClaimDestination:
uriClaimDestination.BitcoinUrl.Amount = new Money(blob.CryptoAmount.Value, MoneyUnit.BTC);
uriClaimDestination.BitcoinUrl.Amount = new Money(payout.Amount.Value, MoneyUnit.BTC);
var newUri = new UriBuilder(uriClaimDestination.BitcoinUrl.Uri);
BTCPayServerClient.AppendPayloadToQuery(newUri, new KeyValuePair<string, object>("payout", payout.Id));
bip21.Add(newUri.Uri.ToString());
break;
case AddressClaimDestination addressClaimDestination:
var bip21New = Network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value);
var bip21New = Network.GenerateBIP21(addressClaimDestination.Address.ToString(), payout.Amount.Value);
bip21New.QueryParams.Add("payout", payout.Id);
bip21.Add(bip21New.ToString());
break;
@ -438,11 +438,11 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
if (!payoutByDestination.TryGetValue(destination, out var payout))
return;
var payoutBlob = payout.GetBlob(_jsonSerializerSettings);
if (payoutBlob.CryptoAmount is null ||
if (payout.Amount is null ||
// The round up here is not strictly necessary, this is temporary to fix existing payout before we
// were properly roundup the crypto amount
destinationSum !=
BTCPayServer.Extensions.RoundUp(payoutBlob.CryptoAmount.Value, network.Divisibility))
BTCPayServer.Extensions.RoundUp(payout.Amount.Value, network.Divisibility))
return;
var derivationSchemeSettings = payout.StoreData

View File

@ -115,7 +115,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return new ConfirmVM
{
Amount = blob.CryptoAmount.Value,
Amount = payoutData.Amount.Value,
Destination = blob.Destination,
PayoutId = payoutData.Id
};
@ -242,7 +242,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var lnurlInfo =
(LNURLPayRequest)await LNURL.LNURL.FetchInformation(endpoint, "payRequest",
httpClient, cancellationToken);
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
var lm = new LightMoney(payoutData.Amount.Value, LightMoneyUnit.BTC);
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
{
@ -281,14 +281,14 @@ namespace BTCPayServer.Data.Payouts.LightningLike
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, CancellationToken cancellationToken)
{
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount > payoutBlob.CryptoAmount)
if (boltAmount > payoutData.Amount)
{
payoutData.State = PayoutState.Cancelled;
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {payoutData.Currency})",
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutData.Amount.GetValueOrDefault()} {payoutData.Currency})",
Destination = payoutBlob.Destination
};
}
@ -312,7 +312,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
new PayInvoiceParams()
{
// CLN does not support explicit amount param if it is the same as the invoice amount
Amount = payoutBlob.CryptoAmount == bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)? null: new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
Amount = payoutData.Amount == bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)? null: new LightMoney((decimal)payoutData.Amount, LightMoneyUnit.BTC)
}, cancellationToken);
if (result == null) throw new NoPaymentResultException();

View File

@ -7,10 +7,6 @@ namespace BTCPayServer.Data
{
public class PayoutBlob
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CryptoAmount { get; set; }
public int MinimumConfirmation { get; set; } = 1;
public string Destination { get; set; }
public int Revision { get; set; }

View File

@ -12,11 +12,8 @@ namespace BTCPayServer.Data
{
public string Name { get; set; }
public string Description { get; set; }
public string Currency { get; set; }
public int Divisibility { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Limit { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal MinimumClaim { get; set; }
public PullPaymentView View { get; set; } = new PullPaymentView();
@ -27,7 +24,7 @@ namespace BTCPayServer.Data
[JsonProperty(ItemConverterType = typeof(PayoutMethodIdJsonConverter))]
public PayoutMethodId[] SupportedPaymentMethods { get; set; }
public PayoutMethodId[] SupportedPayoutMethods { get; set; }
public bool AutoApproveClaims { get; set; }

View File

@ -11,18 +11,16 @@ namespace BTCPayServer.Data
public static PullPaymentBlob GetBlob(this PullPaymentData data)
{
var result = JsonConvert.DeserializeObject<PullPaymentBlob>(data.Blob);
result!.SupportedPaymentMethods = result.SupportedPaymentMethods.Where(id => id is not null).ToArray();
return result;
return JsonConvert.DeserializeObject<PullPaymentBlob>(data.Blob);
}
public static void SetBlob(this PullPaymentData data, PullPaymentBlob blob)
{
data.Blob = JsonConvert.SerializeObject(blob).ToString();
}
public static bool IsSupported(this PullPaymentData data, PayoutMethodId paymentId)
public static bool IsSupported(this PullPaymentData data, PayoutMethodId payoutMethodId)
{
return data.GetBlob().SupportedPaymentMethods.Contains(paymentId);
return data.GetBlob().SupportedPayoutMethods.Contains(payoutMethodId);
}
}
}

View File

@ -1,53 +0,0 @@
#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

@ -135,14 +135,14 @@ namespace BTCPayServer.HostedServices
o.EndDate = create.ExpiresAt is DateTimeOffset date2 ? new DateTimeOffset?(date2) : null;
o.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
o.StoreId = create.StoreId;
o.Currency = create.Currency;
o.Limit = create.Amount;
o.SetBlob(new PullPaymentBlob()
{
Name = create.Name ?? string.Empty,
Description = create.Description ?? string.Empty,
Currency = create.Currency,
Limit = create.Amount,
SupportedPaymentMethods = create.PayoutMethodIds,
SupportedPayoutMethods = create.PayoutMethodIds,
AutoApproveClaims = create.AutoApproveClaims,
View = new PullPaymentBlob.PullPaymentView
{
@ -386,21 +386,21 @@ namespace BTCPayServer.HostedServices
}
}
public bool SupportsLNURL(PullPaymentBlob blob)
public bool SupportsLNURL(PullPaymentData pp, PullPaymentBlob blob = null)
{
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
blob ??= pp.GetBlob();
var pms = blob.SupportedPayoutMethods.FirstOrDefault(id =>
PayoutTypes.LN.GetPayoutMethodId(_networkProvider.DefaultNetwork.CryptoCode)
== id);
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
return pms is not null && _lnurlSupportedCurrencies.Contains(pp.Currency);
}
public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken)
{
var ppBlob = payout.PullPaymentData?.GetBlob();
var payoutPaymentMethod = payout.GetPayoutMethodId();
var cryptoCode = _handlers.TryGetNetwork(payoutPaymentMethod)?.NBXplorerNetwork.CryptoCode;
var currencyPair = new Rating.CurrencyPair(cryptoCode,
ppBlob?.Currency ?? cryptoCode);
payout.PullPaymentData?.Currency ?? cryptoCode);
Rating.RateRule rule = null;
try
{
@ -474,9 +474,9 @@ namespace BTCPayServer.HostedServices
payout.State = PayoutState.AwaitingPayment;
if (payout.PullPaymentData is null ||
cryptoCode == payout.PullPaymentData.GetBlob().Currency)
cryptoCode == payout.PullPaymentData.Currency)
req.Rate = 1.0m;
var cryptoAmount = payoutBlob.Amount / req.Rate;
var cryptoAmount = payout.OriginalAmount / req.Rate;
var payoutHandler = _handlers.TryGet(paymentMethod);
if (payoutHandler is null)
throw new InvalidOperationException($"No payout handler for {paymentMethod}");
@ -489,13 +489,12 @@ namespace BTCPayServer.HostedServices
return;
}
payoutBlob.CryptoAmount = Extensions.RoundUp(cryptoAmount,
payout.Amount = Extensions.RoundUp(cryptoAmount,
network.Divisibility);
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.SaveChangesAsync();
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Approved, payout));
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payout.Amount));
}
catch (Exception ex)
{
@ -581,7 +580,7 @@ namespace BTCPayServer.HostedServices
ppBlob = pp.GetBlob();
if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PayoutMethodId))
if (!ppBlob.SupportedPayoutMethods.Contains(req.ClaimRequest.PayoutMethodId))
{
req.Completion.TrySetResult(
new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
@ -623,8 +622,8 @@ namespace BTCPayServer.HostedServices
.Where(p => p.State != PayoutState.Cancelled).ToListAsync();
var payouts = payoutsRaw?.Select(o => new { Entity = o, Blob = o.GetBlob(_jsonSerializerSettings) });
var limit = ppBlob?.Limit ?? 0;
var totalPayout = payouts?.Select(p => p.Blob.Amount)?.Sum();
var limit = pp?.Limit ?? 0;
var totalPayout = payouts?.Select(p => p.Entity.OriginalAmount)?.Sum();
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - (totalPayout ?? 0);
if (totalPayout is not null && totalPayout + claimed > limit)
{
@ -647,14 +646,15 @@ namespace BTCPayServer.HostedServices
PayoutMethodId = req.ClaimRequest.PayoutMethodId.ToString(),
Destination = req.ClaimRequest.Destination.Id,
StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId,
Currency = payoutHandler.Currency
Currency = payoutHandler.Currency,
OriginalCurrency = pp?.Currency ?? payoutHandler.Currency
};
var payoutBlob = new PayoutBlob()
{
Amount = claimed,
Destination = req.ClaimRequest.Destination.ToString(),
Metadata = req.ClaimRequest.Metadata ?? new JObject(),
};
payout.OriginalAmount = claimed;
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.Payouts.AddAsync(payout);
try
@ -681,8 +681,7 @@ namespace BTCPayServer.HostedServices
if (approveResult.Result == PayoutApproval.Result.Ok)
{
payout.State = PayoutState.AwaitingPayment;
payoutBlob.CryptoAmount = approveResult.CryptoAmount;
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
payout.Amount = approveResult.CryptoAmount;
}
}
}
@ -692,7 +691,7 @@ namespace BTCPayServer.HostedServices
new PayoutNotification()
{
StoreId = payout.StoreDataId,
Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode,
Currency = pp?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode,
Status = payout.State,
PaymentMethod = payout.PayoutMethodId,
PayoutId = payout.Id
@ -809,30 +808,28 @@ namespace BTCPayServer.HostedServices
public PullPaymentsModel.PullPaymentModel.ProgressModel CalculatePullPaymentProgress(PullPaymentData pp,
DateTimeOffset now)
{
var ppBlob = pp.GetBlob();
var ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
var nfi = _currencyNameTable.GetNumberFormatInfo(ppBlob.Currency, true);
var ni = _currencyNameTable.GetCurrencyData(pp.Currency, true);
var nfi = _currencyNameTable.GetNumberFormatInfo(pp.Currency, true);
var totalCompleted = pp.Payouts
.Where(p => (p.State == PayoutState.Completed ||
p.State == PayoutState.InProgress))
.Select(o => o.GetBlob(_jsonSerializerSettings).Amount).Sum().RoundToSignificant(ni.Divisibility);
.Select(o => o.OriginalAmount).Sum().RoundToSignificant(ni.Divisibility);
var totalAwaiting = pp.Payouts
.Where(p => (p.State == PayoutState.AwaitingPayment ||
p.State == PayoutState.AwaitingApproval)).Select(o =>
o.GetBlob(_jsonSerializerSettings).Amount).Sum().RoundToSignificant(ni.Divisibility);
o.OriginalAmount).Sum().RoundToSignificant(ni.Divisibility);
var currencyData = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
var currencyData = _currencyNameTable.GetCurrencyData(pp.Currency, true);
return new PullPaymentsModel.PullPaymentModel.ProgressModel()
{
CompletedPercent = (int)(totalCompleted / ppBlob.Limit * 100m),
AwaitingPercent = (int)(totalAwaiting / ppBlob.Limit * 100m),
CompletedPercent = (int)(totalCompleted / pp.Limit * 100m),
AwaitingPercent = (int)(totalAwaiting / pp.Limit * 100m),
AwaitingFormatted = totalAwaiting.ToString("C", nfi),
Awaiting = totalAwaiting,
Completed = totalCompleted,
CompletedFormatted = totalCompleted.ToString("C", nfi),
Limit = ppBlob.Limit.RoundToSignificant(currencyData.Divisibility),
LimitFormatted = _displayFormatter.Currency(ppBlob.Limit, ppBlob.Currency),
Limit = pp.Limit.RoundToSignificant(currencyData.Divisibility),
LimitFormatted = _displayFormatter.Currency(pp.Limit, pp.Currency),
EndIn = pp.EndsIn() is { } end ? end.TimeString() : null,
};
}

View File

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

View File

@ -22,15 +22,15 @@ namespace BTCPayServer.Models
Id = data.Id;
StoreId = data.StoreId;
var blob = data.GetBlob();
PayoutMethodIds = blob.SupportedPaymentMethods;
BitcoinOnly = blob.SupportedPaymentMethods.All(p => p == PayoutTypes.CHAIN.GetPayoutMethodId("BTC") || p == PayoutTypes.LN.GetPayoutMethodId("BTC"));
PayoutMethodIds = blob.SupportedPayoutMethods;
BitcoinOnly = blob.SupportedPayoutMethods.All(p => p == PayoutTypes.CHAIN.GetPayoutMethodId("BTC") || p == PayoutTypes.LN.GetPayoutMethodId("BTC"));
SelectedPayoutMethod = PayoutMethodIds.First().ToString();
Archived = data.Archived;
AutoApprove = blob.AutoApproveClaims;
Title = blob.View.Title;
Description = blob.View.Description;
Amount = blob.Limit;
Currency = blob.Currency;
Amount = data.Limit;
Currency = data.Currency;
Description = blob.View.Description;
ExpiryDate = data.EndDate is DateTimeOffset dt ? (DateTime?)dt.UtcDateTime : null;
Email = blob.View.Email;

View File

@ -107,19 +107,18 @@ namespace BTCPayServer.PayoutProcessors.OnChain
config.AccountDerivation, DerivationFeature.Change, 0, true);
var processorBlob = GetBlob(PayoutProcessorSettings);
var payoutToBlobs = payouts.ToDictionary(data => data, data => data.GetBlob(_btcPayNetworkJsonSerializerSettings));
if (payoutToBlobs.Sum(pair => pair.Value.CryptoAmount) < processorBlob.Threshold)
if (payouts.Sum(p => p.Amount) < processorBlob.Threshold)
{
return;
}
var feeRate = await this._feeProviderFactory.CreateFeeProvider(Network).GetFeeRateAsync(Math.Max(processorBlob.FeeTargetBlock, 1));
var transfersProcessing = new List<KeyValuePair<PayoutData, PayoutBlob>>();
foreach (var transferRequest in payoutToBlobs)
var transfersProcessing = new List<PayoutData>();
foreach (var payout in payouts)
{
var blob = transferRequest.Value;
if (failedAmount.HasValue && blob.CryptoAmount >= failedAmount)
var blob = payout.GetBlob(_btcPayNetworkJsonSerializerSettings);
if (failedAmount.HasValue && payout.Amount >= failedAmount)
{
continue;
}
@ -146,19 +145,19 @@ namespace BTCPayServer.PayoutProcessors.OnChain
}
txBuilder.Send(bitcoinClaimDestination.Address,
new Money(blob.CryptoAmount.Value, MoneyUnit.BTC));
new Money(payout.Amount.Value, MoneyUnit.BTC));
try
{
txBuilder.SetChange(changeAddress.Address);
txBuilder.SendEstimatedFees(feeRate);
workingTx = txBuilder.BuildTransaction(true);
transfersProcessing.Add(transferRequest);
transfersProcessing.Add(payout);
}
catch (NotEnoughFundsException)
{
failedAmount = blob.CryptoAmount;
failedAmount = payout.Amount;
//keep going, we prioritize withdraws by time but if there is some other we can fit, we should
}
}
@ -170,8 +169,8 @@ namespace BTCPayServer.PayoutProcessors.OnChain
var txHash = workingTx.GetHash();
foreach (var payoutData in transfersProcessing)
{
payoutData.Key.State = PayoutState.InProgress;
_bitcoinLikePayoutHandler.SetProofBlob(payoutData.Key,
payoutData.State = PayoutState.InProgress;
_bitcoinLikePayoutHandler.SetProofBlob(payoutData,
new PayoutTransactionOnChainBlob()
{
Accounted = true,
@ -195,7 +194,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain
{
await WalletRepository.AddWalletTransactionAttachment(walletId,
txHash,
Attachment.Payout(payoutData.Key.PullPaymentDataId, payoutData.Key.Id));
Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id));
}
await Task.WhenAny(tcs.Task, task);
}

View File

@ -67,14 +67,10 @@ public class PayoutsReportProvider : ReportProvider
else
continue;
var ppBlob = payout.PullPaymentData?.GetBlob();
var currency = ppBlob?.Currency ?? payout.Currency;
if (currency is null)
continue;
data.Add(payout.Currency);
data.Add(blob.CryptoAmount is decimal v ? _displayFormatter.ToFormattedAmount(v, payout.Currency) : null);
data.Add(currency);
data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency));
data.Add(payout.Amount is decimal v ? _displayFormatter.ToFormattedAmount(v, payout.Currency) : null);
data.Add(payout.OriginalCurrency);
data.Add(_displayFormatter.ToFormattedAmount(payout.OriginalAmount, payout.OriginalCurrency));
data.Add(blob.Destination);
queryContext.Data.Add(data);
}
@ -90,37 +86,37 @@ public class PayoutsReportProvider : ReportProvider
new("Source", "string"),
new("State", "string"),
new("PaymentType", "string"),
new("Crypto", "string"),
new("CryptoAmount", "amount"),
new("Currency", "string"),
new("CurrencyAmount", "amount"),
new("Amount", "amount"),
new("OriginalCurrency", "string"),
new("OriginalAmount", "amount"),
new("Destination", "string")
},
Charts =
{
new ()
{
Name = "Aggregated crypto amount",
Groups = { "Crypto", "PaymentType", "State" },
Totals = { "Crypto" },
Name = "Aggregated by currency",
Groups = { "Currency", "PaymentType", "State" },
Totals = { "Currency" },
HasGrandTotal = false,
Aggregates = { "CryptoAmount" }
Aggregates = { "Amount" }
},
new ()
{
Name = "Aggregated amount",
Groups = { "Currency", "State" },
Totals = { "CurrencyAmount" },
Name = "Aggregated by original currency",
Groups = { "OriginalCurrency", "State" },
Totals = { "OriginalAmount" },
HasGrandTotal = false,
Aggregates = { "CurrencyAmount" }
Aggregates = { "OriginalAmount" }
},
new ()
{
Name = "Aggregated amount by Source",
Groups = { "Currency", "State", "Source" },
Totals = { "CurrencyAmount" },
Name = "Aggregated by original currency, state and source",
Groups = { "OriginalCurrency", "State", "Source" },
Totals = { "OriginalAmount" },
HasGrandTotal = false,
Aggregates = { "CurrencyAmount" }
Aggregates = { "OriginalAmount" }
}
}
};

View File

@ -67,7 +67,7 @@ namespace BTCPayServer.Services.Reporting
var conn = ctx.Database.GetDbConnection();
var rows = await conn.QueryAsync(
"""
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
SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PayoutMethodId", p."Currency" AS "PayoutCurrency", p."OriginalAmount", pp."Id" AS "PullPaymentId", pp."Limit", pp."Currency" AS "PPCurrency" FROM "Invoices" i
JOIN "Refunds" r ON r."InvoiceDataId"= i."Id"
JOIN "PullPayments" pp ON r."PullPaymentDataId"=pp."Id"
LEFT JOIN "Payouts" p ON p."PullPaymentDataId"=pp."Id"
@ -78,45 +78,25 @@ namespace BTCPayServer.Services.Reporting
""", new { start = queryContext.From, end = queryContext.To, storeId = queryContext.StoreId });
foreach (var r in rows)
{
PullPaymentBlob ppBlob = GetPullPaymentBlob(r);
PayoutBlob? pBlob = GetPayoutBlob(r);
if ((string)r.PullPaymentId != currentRow?.PullPaymentId)
{
AddRow(queryContext, currentRow);
currentRow = new(r.Created, r.InvoiceId, r.PullPaymentId, ppBlob.Currency, ppBlob.Limit);
currentRow = new(r.Created, r.InvoiceId, r.PullPaymentId, r.PPCurrency, r.Limit);
}
if (pBlob is null)
if (r.OriginalAmount is null)
continue;
decimal originalAmount = (decimal)r.OriginalAmount;
var state = Enum.Parse<PayoutState>((string)r.State);
if (state == PayoutState.Cancelled)
continue;
if (state is PayoutState.Completed)
currentRow.Completed += pBlob.Amount;
currentRow.Completed += originalAmount;
else
currentRow.Awaiting += pBlob.Amount;
currentRow.Awaiting += originalAmount;
}
AddRow(queryContext, currentRow);
}
private PayoutBlob? GetPayoutBlob(dynamic r)
{
if (r.pBlob is null)
return null;
Data.PayoutData p = new Data.PayoutData();
p.PayoutMethodId = r.PayoutMethodId;
p.Currency = (string)r.PayoutCurrency;
p.Blob = (string)r.pBlob;
return p.GetBlob(_serializerSettings);
}
private static PullPaymentBlob GetPullPaymentBlob(dynamic r)
{
Data.PullPaymentData pp = new Data.PullPaymentData();
pp.Blob = (string)r.ppBlob;
return pp.GetBlob();
}
private void AddRow(QueryContext queryContext, RefundRow? currentRow)
{
if (currentRow is null)

View File

@ -562,7 +562,6 @@
<tbody>
@foreach (var refund in Model.Refunds)
{
var blob = refund.PullPaymentData.GetBlob();
<tr>
<td>
@ -576,7 +575,7 @@
</span>
</td>
<td>
<span>@blob.Limit @blob.Currency</span>
<span>@refund.PullPaymentData.Limit @refund.PullPaymentData.Currency</span>
</td>
<td>
<span>@refund.PullPaymentData.StartDate.ToBrowserDate()</span>