mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Add topups to payouts (#6187)
This commit is contained in:
parent
b49f6c3f86
commit
4a94074595
@ -7,6 +7,7 @@ UPDATE "Payouts" SET
|
|||||||
"Currency" = split_part("PayoutMethodId", '_', 1),
|
"Currency" = split_part("PayoutMethodId", '_', 1),
|
||||||
"PayoutMethodId"=
|
"PayoutMethodId"=
|
||||||
CASE
|
CASE
|
||||||
|
WHEN ("Blob"->>'Amount')::NUMERIC < 0 THEN 'TOPUP'
|
||||||
WHEN split_part("PayoutMethodId", '_', 2) = 'LightningLike' THEN split_part("PayoutMethodId", '_', 1) || '-LN'
|
WHEN split_part("PayoutMethodId", '_', 2) = 'LightningLike' THEN split_part("PayoutMethodId", '_', 1) || '-LN'
|
||||||
ELSE split_part("PayoutMethodId", '_', 1) || '-CHAIN'
|
ELSE split_part("PayoutMethodId", '_', 1) || '-CHAIN'
|
||||||
END;
|
END;
|
||||||
|
@ -59,6 +59,14 @@ namespace BTCPayServer.Tests
|
|||||||
PullPaymentDataId = null as string,
|
PullPaymentDataId = null as string,
|
||||||
PaymentMethodId = "BTC_LightningLike",
|
PaymentMethodId = "BTC_LightningLike",
|
||||||
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
|
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = "p4",
|
||||||
|
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 conn.ExecuteAsync("INSERT INTO \"Payouts\"(\"Id\", \"StoreDataId\", \"PullPaymentDataId\", \"PaymentMethodId\", \"Blob\", \"State\", \"Date\") VALUES (@Id, @StoreId, @PullPaymentDataId, @PaymentMethodId, @Blob::JSONB, 'state', NOW())", parameters);
|
||||||
@ -75,6 +83,9 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p3' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='BTC'");
|
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);
|
Assert.True(migrated);
|
||||||
|
|
||||||
|
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p4' AND \"Amount\" IS NULL AND \"OriginalAmount\"=-10.0 AND \"OriginalCurrency\"='BTC' AND \"PayoutMethodId\"='TOPUP'");
|
||||||
|
Assert.True(migrated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1448,6 +1448,41 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanTopUpPullPayment()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
await user.GrantAccessAsync(true);
|
||||||
|
await user.RegisterDerivationSchemeAsync("BTC");
|
||||||
|
var client = await user.CreateClient();
|
||||||
|
var pp = await client.CreatePullPayment(user.StoreId, new()
|
||||||
|
{
|
||||||
|
Currency = "BTC",
|
||||||
|
Amount = 1.0m,
|
||||||
|
PaymentMethods = [ "BTC-CHAIN" ]
|
||||||
|
});
|
||||||
|
var controller = user.GetController<UIInvoiceController>();
|
||||||
|
var invoice = await controller.CreateInvoiceCoreRaw(new()
|
||||||
|
{
|
||||||
|
Amount = 0.5m,
|
||||||
|
Currency = "BTC",
|
||||||
|
}, controller.HttpContext.GetStoreData(), controller.Url.Link(null, null), [PullPaymentHostedService.GetInternalTag(pp.Id)]);
|
||||||
|
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new() { Status = InvoiceStatus.Settled });
|
||||||
|
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
var payouts = await client.GetPayouts(pp.Id);
|
||||||
|
var payout = Assert.Single(payouts);
|
||||||
|
Assert.Equal("TOPUP", payout.PaymentMethod);
|
||||||
|
Assert.Equal(invoice.Id, payout.Destination);
|
||||||
|
Assert.Equal(-0.5m, payout.Amount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanUseDefaultCurrency()
|
public async Task CanUseDefaultCurrency()
|
||||||
|
@ -424,7 +424,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||||||
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(newTransaction.CryptoCode);
|
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(newTransaction.CryptoCode);
|
||||||
|
|
||||||
await using var ctx = _dbContextFactory.CreateContext();
|
await using var ctx = _dbContextFactory.CreateContext();
|
||||||
var payouts = await ctx.Payouts
|
var payout = await ctx.Payouts
|
||||||
.Include(o => o.StoreData)
|
.Include(o => o.StoreData)
|
||||||
.Include(o => o.PullPaymentData)
|
.Include(o => o.PullPaymentData)
|
||||||
.Where(p => p.State == PayoutState.AwaitingPayment)
|
.Where(p => p.State == PayoutState.AwaitingPayment)
|
||||||
@ -432,10 +432,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||||||
#pragma warning disable CA1307 // Specify StringComparison
|
#pragma warning disable CA1307 // Specify StringComparison
|
||||||
.Where(p => destination.Equals(p.Destination))
|
.Where(p => destination.Equals(p.Destination))
|
||||||
#pragma warning restore CA1307 // Specify StringComparison
|
#pragma warning restore CA1307 // Specify StringComparison
|
||||||
.ToListAsync();
|
.FirstOrDefaultAsync();
|
||||||
var payoutByDestination = payouts.ToDictionary(p => p.Destination);
|
|
||||||
|
|
||||||
if (!payoutByDestination.TryGetValue(destination, out var payout))
|
if (payout is null)
|
||||||
return;
|
return;
|
||||||
var payoutBlob = payout.GetBlob(_jsonSerializerSettings);
|
var payoutBlob = payout.GetBlob(_jsonSerializerSettings);
|
||||||
if (payout.Amount is null ||
|
if (payout.Amount is null ||
|
||||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models.WalletViewModels;
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
@ -273,7 +274,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
|
|
||||||
return await query.FirstOrDefaultAsync(data => data.Id == pullPaymentId);
|
return await query.FirstOrDefaultAsync(data => data.Id == pullPaymentId);
|
||||||
}
|
}
|
||||||
|
record TopUpRequest(string PullPaymentId, InvoiceEntity InvoiceEntity);
|
||||||
class PayoutRequest
|
class PayoutRequest
|
||||||
{
|
{
|
||||||
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
|
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
|
||||||
@ -336,10 +337,21 @@ namespace BTCPayServer.HostedServices
|
|||||||
{
|
{
|
||||||
payoutHandler.StartBackgroundCheck(Subscribe);
|
payoutHandler.StartBackgroundCheck(Subscribe);
|
||||||
}
|
}
|
||||||
|
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoiceCore);
|
||||||
return new[] { Loop() };
|
return new[] { Loop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TopUpInvoiceCore(InvoiceEvent evt)
|
||||||
|
{
|
||||||
|
if (evt.EventCode == InvoiceEventCode.Completed || evt.EventCode == InvoiceEventCode.MarkedCompleted)
|
||||||
|
{
|
||||||
|
foreach (var pullPaymentId in evt.Invoice.GetInternalTags("PULLPAY#"))
|
||||||
|
{
|
||||||
|
_Channel.Writer.TryWrite(new TopUpRequest(pullPaymentId, evt.Invoice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Subscribe(params Type[] events)
|
private void Subscribe(params Type[] events)
|
||||||
{
|
{
|
||||||
foreach (Type @event in events)
|
foreach (Type @event in events)
|
||||||
@ -352,6 +364,11 @@ namespace BTCPayServer.HostedServices
|
|||||||
{
|
{
|
||||||
await foreach (var o in _Channel.Reader.ReadAllAsync())
|
await foreach (var o in _Channel.Reader.ReadAllAsync())
|
||||||
{
|
{
|
||||||
|
if (o is TopUpRequest topUp)
|
||||||
|
{
|
||||||
|
await HandleTopUp(topUp);
|
||||||
|
}
|
||||||
|
|
||||||
if (o is PayoutRequest req)
|
if (o is PayoutRequest req)
|
||||||
{
|
{
|
||||||
await HandleCreatePayout(req);
|
await HandleCreatePayout(req);
|
||||||
@ -386,6 +403,38 @@ namespace BTCPayServer.HostedServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleTopUp(TopUpRequest topUp)
|
||||||
|
{
|
||||||
|
var pp = await this.GetPullPayment(topUp.PullPaymentId, false);
|
||||||
|
using var ctx = _dbContextFactory.CreateContext();
|
||||||
|
|
||||||
|
var payout = new Data.PayoutData()
|
||||||
|
{
|
||||||
|
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
|
||||||
|
PayoutMethodId = PayoutMethodIds.TopUp.ToString(),
|
||||||
|
Date = DateTimeOffset.UtcNow,
|
||||||
|
State = PayoutState.Completed,
|
||||||
|
PullPaymentDataId = pp.Id,
|
||||||
|
Destination = topUp.InvoiceEntity.Id,
|
||||||
|
StoreDataId = pp.StoreId
|
||||||
|
};
|
||||||
|
if (topUp.InvoiceEntity.Currency != pp.Currency ||
|
||||||
|
pp.Currency is not ("SATS" or "BTC"))
|
||||||
|
return;
|
||||||
|
payout.Currency = pp.Currency;
|
||||||
|
payout.Amount = -topUp.InvoiceEntity.Price;
|
||||||
|
payout.OriginalCurrency = payout.Currency;
|
||||||
|
payout.OriginalAmount = payout.Amount.Value;
|
||||||
|
var payoutBlob = new PayoutBlob()
|
||||||
|
{
|
||||||
|
Destination = topUp.InvoiceEntity.Id,
|
||||||
|
Metadata = new JObject()
|
||||||
|
};
|
||||||
|
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||||
|
await ctx.Payouts.AddAsync(payout);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public bool SupportsLNURL(PullPaymentData pp, PullPaymentBlob blob = null)
|
public bool SupportsLNURL(PullPaymentData pp, PullPaymentBlob blob = null)
|
||||||
{
|
{
|
||||||
blob ??= pp.GetBlob();
|
blob ??= pp.GetBlob();
|
||||||
@ -842,6 +891,10 @@ namespace BTCPayServer.HostedServices
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetInternalTag(string ppId)
|
||||||
|
{
|
||||||
|
return $"PULLPAY#{ppId}";
|
||||||
|
}
|
||||||
|
|
||||||
class InternalPayoutPaidRequest
|
class InternalPayoutPaidRequest
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,10 @@ using BTCPayServer.Payments;
|
|||||||
|
|
||||||
namespace BTCPayServer.Payouts
|
namespace BTCPayServer.Payouts
|
||||||
{
|
{
|
||||||
|
public class PayoutMethodIds
|
||||||
|
{
|
||||||
|
public static readonly PayoutMethodId TopUp = PayoutMethodId.Parse("TOPUP");
|
||||||
|
}
|
||||||
public class PayoutTypes
|
public class PayoutTypes
|
||||||
{
|
{
|
||||||
public static readonly PayoutType LN = new("LN");
|
public static readonly PayoutType LN = new("LN");
|
||||||
|
Loading…
Reference in New Issue
Block a user