mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Refactor Payouts and PullPayments DB models (#6173)
This commit is contained in:
parent
3c40dc1f49
commit
1dd37c5020
@ -19,5 +19,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="DBScripts\001.InvoiceFunctions.sql" />
|
||||
<None Remove="DBScripts\002.RefactorPayouts.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
56
BTCPayServer.Data/DBScripts/002.RefactorPayouts.sql
Normal file
56
BTCPayServer.Data/DBScripts/002.RefactorPayouts.sql
Normal 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;
|
@ -37,10 +37,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
paymentData.Migrate();
|
||||
}
|
||||
else if (entity is PayoutData payoutData && payoutData.Currency is null)
|
||||
{
|
||||
payoutData.Migrate();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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; }
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
||||
|
90
BTCPayServer.Tests/DatabaseTester.cs
Normal file
90
BTCPayServer.Tests/DatabaseTester.cs
Normal 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; }
|
||||
}
|
||||
}
|
80
BTCPayServer.Tests/DatabaseTests.cs
Normal file
80
BTCPayServer.Tests/DatabaseTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
};
|
||||
|
@ -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>()
|
||||
|
@ -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,
|
||||
|
@ -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 );
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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")));
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user