Refactor Payouts (#4032)

Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2022-08-17 09:45:51 +02:00
committed by GitHub
parent d6ae34929e
commit d0b26e9f69
19 changed files with 435 additions and 106 deletions

View File

@@ -21,4 +21,9 @@ public class PayoutProcessorData
.HasOne(o => o.Store) .HasOne(o => o.Store)
.WithMany(data => data.PayoutProcessors).OnDelete(DeleteBehavior.Cascade); .WithMany(data => data.PayoutProcessors).OnDelete(DeleteBehavior.Cascade);
} }
public override string ToString()
{
return $"{Processor} {PaymentMethod} {StoreId}";
}
} }

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Events;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications;
@@ -2455,6 +2456,50 @@ namespace BTCPayServer.Tests
await newUserBasicClient.GetCurrentUser(); await newUserBasicClient.GetCurrentUser();
} }
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNPayoutProcessor()
{
LightningPendingPayoutListener.SecondsDelay = 0;
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
admin.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var payoutAmount = LightMoney.Satoshis(1000);
var inv = await tester.MerchantLnd.Client.CreateInvoice(payoutAmount, "Donation to merchant", TimeSpan.FromHours(1), default);
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
Assert.Equal(PayResult.Ok, resp.Result);
var customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
var payout = await adminClient.CreatePayout(admin.StoreId,
new CreatePayoutThroughStoreRequest()
{
Approved = true, PaymentMethod = "BTC_LightningNetwork", Destination = customerInvoice.BOLT11
});
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() {IntervalSeconds = TimeSpan.FromSeconds(2)});
Assert.Equal(2, Assert.Single( await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
var payoutC =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed , payoutC.State);
});
}
[Fact(Timeout = 60 * 2 * 1000)] [Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task CanUsePayoutProcessorsThroughAPI() public async Task CanUsePayoutProcessorsThroughAPI()

View File

@@ -0,0 +1,16 @@
#!/bin/bash
PREIMAGE=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 64 | head -n 1)
HASH=`node -e "console.log(require('crypto').createHash('sha256').update(Buffer.from('$PREIMAGE', 'hex')).digest('hex'))"`
PAYREQ=$(./docker-customer-lncli.sh addholdinvoice $HASH $@ | jq -r ".payment_request")
echo "HASH: $HASH"
echo "PREIMAGE: $PREIMAGE"
echo "PAY REQ: $PAYREQ"
echo ""
echo "SETTLE: ./docker-customer-lncli.sh settleinvoice $PREIMAGE"
echo "CANCEL: ./docker-customer-lncli.sh cancelinvoice $HASH"
echo "LOOKUP: ./docker-customer-lncli.sh lookupinvoice $HASH"
echo ""
echo "TRACK: ./docker-merchant-lncli.sh trackpayment $HASH"
echo "PAY: ./docker-merchant-lncli.sh payinvoice $PAYREQ"

View File

@@ -180,6 +180,15 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(CreatePullPaymentData(pp)); return Ok(CreatePullPaymentData(pp));
} }
private PayoutState[]? GetStateFilter(bool includeCancelled) =>
includeCancelled
? null
: new[]
{
PayoutState.Completed, PayoutState.AwaitingApproval, PayoutState.AwaitingPayment,
PayoutState.InProgress
};
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts")] [HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> GetPayouts(string pullPaymentId, bool includeCancelled = false) public async Task<IActionResult> GetPayouts(string pullPaymentId, bool includeCancelled = false)
@@ -189,7 +198,12 @@ namespace BTCPayServer.Controllers.Greenfield
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, true); var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, true);
if (pp is null) if (pp is null)
return PullPaymentNotFound(); return PullPaymentNotFound();
var payouts = pp.Payouts.Where(p => p.State != PayoutState.Cancelled || includeCancelled).ToList();
var payouts =await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
{
PullPayments = new[] {pullPaymentId},
States = GetStateFilter(includeCancelled)
});
return base.Ok(payouts return base.Ok(payouts
.Select(ToModel).ToList()); .Select(ToModel).ToList());
} }
@@ -201,10 +215,13 @@ namespace BTCPayServer.Controllers.Greenfield
if (payoutId is null) if (payoutId is null)
return PayoutNotFound(); return PayoutNotFound();
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, true);
if (pp is null) var payout = (await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
return PullPaymentNotFound(); {
var payout = pp.Payouts.FirstOrDefault(p => p.Id == payoutId); PullPayments = new[] {pullPaymentId}, PayoutIds = new[] {payoutId}
})).FirstOrDefault();
if (payout is null) if (payout is null)
return PayoutNotFound(); return PayoutNotFound();
return base.Ok(ToModel(payout)); return base.Ok(ToModel(payout));
@@ -392,10 +409,13 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetStorePayouts(string storeId, bool includeCancelled = false) public async Task<IActionResult> GetStorePayouts(string storeId, bool includeCancelled = false)
{ {
await using var ctx = _dbContextFactory.CreateContext(); var payouts = await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
var payouts = await ctx.Payouts {
.Where(p => p.StoreDataId == storeId && (p.State != PayoutState.Cancelled || includeCancelled)) Stores = new[] {storeId},
.ToListAsync(); States = GetStateFilter(includeCancelled)
});
return base.Ok(payouts return base.Ok(payouts
.Select(ToModel).ToList()); .Select(ToModel).ToList());
} }

View File

@@ -5,6 +5,7 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data; using BTCPayServer.Data.Data;
using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.Lightning; using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.PayoutProcessors.Settings; using BTCPayServer.PayoutProcessors.Settings;
@@ -36,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors( public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
string storeId, string? paymentMethod) string storeId, string? paymentMethod)
{ {
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null;
var configured = var configured =
await _payoutProcessorService.GetProcessors( await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
@@ -68,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor( public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request) string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
{ {
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
var activeProcessor = var activeProcessor =
(await _payoutProcessorService.GetProcessors( (await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()

View File

@@ -5,6 +5,7 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data; using BTCPayServer.Data.Data;
using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain; using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.PayoutProcessors.Settings; using BTCPayServer.PayoutProcessors.Settings;
@@ -36,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors( public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
string storeId, string? paymentMethod) string storeId, string? paymentMethod)
{ {
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null;
var configured = var configured =
await _payoutProcessorService.GetProcessors( await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
@@ -68,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor( public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request) string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
{ {
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
var activeProcessor = var activeProcessor =
(await _payoutProcessorService.GetProcessors( (await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()

View File

@@ -103,9 +103,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
{ {
return null; return null;
} }
var raw = JObject.Parse(Encoding.UTF8.GetString(payout.Proof));
if (raw.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType) && ParseProofType(payout.Proof, out var raw, out var proofType);
proofType.Value<string>() == ManualPayoutProof.Type) if (proofType == ManualPayoutProof.Type)
{ {
return raw.ToObject<ManualPayoutProof>(); return raw.ToObject<ManualPayoutProof>();
} }
@@ -118,6 +118,22 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return res; return res;
} }
public static void ParseProofType(byte[] proof, out JObject obj, out string type)
{
type = null;
if (proof is null)
{
obj = null;
return;
}
obj = JObject.Parse(Encoding.UTF8.GetString(proof));
if (obj.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType))
{
type = proofType.Value<string>();
}
}
public void StartBackgroundCheck(Action<Type[]> subscribe) public void StartBackgroundCheck(Action<Type[]> subscribe)
{ {
subscribe(new[] { typeof(NewOnChainTransactionEvent), typeof(NewBlockEvent) }); subscribe(new[] { typeof(NewOnChainTransactionEvent), typeof(NewBlockEvent) });
@@ -443,11 +459,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob) public void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob)
{ {
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, _jsonSerializerSettings.GetSerializer(data.GetPaymentMethodId().CryptoCode))); data.SetProofBlob(blob, _jsonSerializerSettings.GetSerializer(data.GetPaymentMethodId().CryptoCode));
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
{
data.Proof = bytes;
}
} }
} }

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
@@ -13,6 +14,8 @@ using LNURL;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data.Payouts.LightningLike namespace BTCPayServer.Data.Payouts.LightningLike
{ {
@@ -117,7 +120,17 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public IPayoutProof ParseProof(PayoutData payout) public IPayoutProof ParseProof(PayoutData payout)
{ {
return null; BitcoinLikePayoutHandler.ParseProofType(payout.Proof, out var raw, out var proofType);
if (proofType is null)
{
return null;
}
if (proofType == ManualPayoutProof.Type)
{
return raw.ToObject<ManualPayoutProof>();
}
return raw.ToObject<PayoutLightningBlob>();
} }
public void StartBackgroundCheck(Action<Type[]> subscribe) public void StartBackgroundCheck(Action<Type[]> subscribe)
@@ -174,5 +187,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout", return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout",
"UILightningLikePayout", new { cryptoCode = paymentMethodId.CryptoCode, payoutIds })); "UILightningLikePayout", new { cryptoCode = paymentMethodId.CryptoCode, payoutIds }));
} }
} }
} }

View File

@@ -2,12 +2,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
public class PayoutLightningBlob : IPayoutProof public class PayoutLightningBlob : IPayoutProof
{ {
public string Bolt11Invoice { get; set; }
public string Preimage { get; set; }
public string PaymentHash { get; set; } public string PaymentHash { get; set; }
public string ProofType { get; } public string ProofType { get; } = "PayoutLightningBlob";
public string Link { get; } = null; public string Link { get; } = null;
public string Id => PaymentHash; public string Id => PaymentHash;
public string Preimage { get; set; }
} }
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
@@ -258,11 +259,16 @@ namespace BTCPayServer.Data.Payouts.LightningLike
} }
public static async Task<ResultVM> TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, PaymentMethodId pmi) public static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(20);
public static async Task<ResultVM> TrypayBolt(
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
PaymentMethodId pmi)
{ {
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC); var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount != payoutBlob.CryptoAmount) if (boltAmount != payoutBlob.CryptoAmount)
{ {
payoutData.State = PayoutState.Cancelled;
return new ResultVM return new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
@@ -271,13 +277,36 @@ namespace BTCPayServer.Data.Payouts.LightningLike
Destination = payoutBlob.Destination Destination = payoutBlob.Destination
}; };
} }
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(), new PayInvoiceParams());
if (result.Result == PayResult.Ok) var proofBlob = new PayoutLightningBlob() {PaymentHash = bolt11PaymentRequest.PaymentHash.ToString()};
try
{ {
var message = result.Details?.TotalAmount != null using var cts = new CancellationTokenSource(SendTimeout);
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}" var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
: null; new PayInvoiceParams()
payoutData.State = PayoutState.Completed; {
Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero
? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
: null
}, cts.Token);
string message = null;
if (result.Result == PayResult.Ok)
{
message = result.Details?.TotalAmount != null
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
: null;
payoutData.State = PayoutState.Completed;
try
{
var payment = await lightningClient.GetPayment(bolt11PaymentRequest.PaymentHash.ToString());
proofBlob.Preimage = payment.Preimage;
}
catch (Exception e)
{
}
}
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM return new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
@@ -286,14 +315,21 @@ namespace BTCPayServer.Data.Payouts.LightningLike
Message = message Message = message
}; };
} }
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
return new ResultVM
{ {
PayoutId = payoutData.Id, // Timeout, potentially caused by hold invoices
Result = result.Result, // Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
Destination = payoutBlob.Destination, payoutData.State = PayoutState.InProgress;
Message = result.ErrorDetail
}; payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Ok,
Destination = payoutBlob.Destination,
Message = "The payment timed out. We will verify if it completed later."
};
}
} }

View File

@@ -39,12 +39,13 @@ namespace BTCPayServer.Data
{ {
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode))); data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
} }
public static void SetProofBlob(this PayoutData data, ManualPayoutProof blob) public static void SetProofBlob(this PayoutData data, IPayoutProof blob, JsonSerializerSettings settings)
{ {
if (blob is null) if (blob is null)
return; return;
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob)); var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, settings));
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much // We only update the property if the bytes actually changed, this prevent from hammering the DB too much
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof)) if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
{ {

View File

@@ -138,6 +138,52 @@ namespace BTCPayServer.HostedServices
return o.Id; return o.Id;
} }
public class PayoutQuery
{
public PayoutState[] States { get; set; }
public string[] PullPayments { get; set; }
public string[] PayoutIds { get; set; }
public string[] PaymentMethods { get; set; }
public string[] Stores { get; set; }
}
public async Task<List<PayoutData>> GetPayouts(PayoutQuery payoutQuery)
{
await using var ctx = _dbContextFactory.CreateContext();
return await GetPayouts(payoutQuery, ctx);
}
public async Task<List<PayoutData>> GetPayouts(PayoutQuery payoutQuery, ApplicationDbContext ctx)
{
var query = ctx.Payouts.AsQueryable();
if (payoutQuery.States is not null)
{
query = query.Where(data => payoutQuery.States.Contains(data.State));
}
if (payoutQuery.PullPayments is not null)
{
query = query.Where(data => payoutQuery.PullPayments.Contains(data.PullPaymentDataId));
}
if (payoutQuery.PayoutIds is not null)
{
query = query.Where(data => payoutQuery.PayoutIds.Contains(data.Id));
}
if (payoutQuery.PaymentMethods is not null)
{
query = query.Where(data => payoutQuery.PaymentMethods.Contains(data.PaymentMethodId));
}
if (payoutQuery.Stores is not null)
{
query = query.Where(data => payoutQuery.Stores.Contains(data.StoreDataId));
}
return await query.ToListAsync();
}
public async Task<Data.PullPaymentData> GetPullPayment(string pullPaymentId, bool includePayouts) public async Task<Data.PullPaymentData> GetPullPayment(string pullPaymentId, bool includePayouts)
{ {
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
@@ -205,7 +251,7 @@ namespace BTCPayServer.HostedServices
payoutHandler.StartBackgroundCheck(Subscribe); payoutHandler.StartBackgroundCheck(Subscribe);
} }
return new[] { Loop() }; return new[] {Loop()};
} }
private void Subscribe(params Type[] events) private void Subscribe(params Type[] events)
@@ -326,7 +372,8 @@ namespace BTCPayServer.HostedServices
payout.State = PayoutState.AwaitingPayment; payout.State = PayoutState.AwaitingPayment;
if (payout.PullPaymentData is null || paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency) if (payout.PullPaymentData is null ||
paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency)
req.Rate = 1.0m; req.Rate = 1.0m;
var cryptoAmount = payoutBlob.Amount / req.Rate; var cryptoAmount = payoutBlob.Amount / req.Rate;
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethod); var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethod);
@@ -375,7 +422,7 @@ namespace BTCPayServer.HostedServices
if (req.Request.Proof != null) if (req.Request.Proof != null)
{ {
payout.SetProofBlob(req.Request.Proof); payout.SetProofBlob(req.Request.Proof, null);
} }
payout.State = PayoutState.Completed; payout.State = PayoutState.Completed;
@@ -465,7 +512,7 @@ namespace BTCPayServer.HostedServices
: await ctx.Payouts.GetPayoutInPeriod(pp, now) : await ctx.Payouts.GetPayoutInPeriod(pp, now)
.Where(p => p.State != PayoutState.Cancelled).ToListAsync(); .Where(p => p.State != PayoutState.Cancelled).ToListAsync();
var payouts = payoutsRaw?.Select(o => new { Entity = o, Blob = o.GetBlob(_jsonSerializerSettings) }); var payouts = payoutsRaw?.Select(o => new {Entity = o, Blob = o.GetBlob(_jsonSerializerSettings)});
var limit = ppBlob?.Limit ?? 0; var limit = ppBlob?.Limit ?? 0;
var totalPayout = payouts?.Select(p => p.Blob.Amount)?.Sum(); var totalPayout = payouts?.Select(p => p.Blob.Amount)?.Sum();
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - (totalPayout ?? 0); var claimed = req.ClaimRequest.Value is decimal v ? v : limit - (totalPayout ?? 0);
@@ -493,8 +540,7 @@ namespace BTCPayServer.HostedServices
}; };
var payoutBlob = new PayoutBlob() var payoutBlob = new PayoutBlob()
{ {
Amount = claimed, Amount = claimed, Destination = req.ClaimRequest.Destination.ToString()
Destination = req.ClaimRequest.Destination.ToString()
}; };
payout.SetBlob(payoutBlob, _jsonSerializerSettings); payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.Payouts.AddAsync(payout); await ctx.Payouts.AddAsync(payout);
@@ -502,7 +548,7 @@ namespace BTCPayServer.HostedServices
{ {
await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination); await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true) ) if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true))
{ {
payout.StoreData = await ctx.Stores.FindAsync(payout.StoreDataId); payout.StoreData = await ctx.Stores.FindAsync(payout.StoreDataId);
var rateResult = await GetRate(payout, null, CancellationToken.None); var rateResult = await GetRate(payout, null, CancellationToken.None);
@@ -511,15 +557,19 @@ namespace BTCPayServer.HostedServices
var approveResult = new TaskCompletionSource<PayoutApproval.Result>(); var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
await HandleApproval(new PayoutApproval() await HandleApproval(new PayoutApproval()
{ {
PayoutId = payout.Id, Revision = payoutBlob.Revision, Rate = rateResult.BidAsk.Ask, Completion =approveResult PayoutId = payout.Id,
Revision = payoutBlob.Revision,
Rate = rateResult.BidAsk.Ask,
Completion = approveResult
}); });
if ((await approveResult.Task) == PayoutApproval.Result.Ok) if ((await approveResult.Task) == PayoutApproval.Result.Ok)
{ {
payout.State = PayoutState.AwaitingPayment; payout.State = PayoutState.AwaitingPayment;
} }
} }
} }
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout)); req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout));
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId), await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
new PayoutNotification() new PayoutNotification()
@@ -550,7 +600,7 @@ namespace BTCPayServer.HostedServices
List<PayoutData> payouts = null; List<PayoutData> payouts = null;
if (cancel.PullPaymentId != null) if (cancel.PullPaymentId != null)
{ {
ctx.PullPayments.Attach(new Data.PullPaymentData() { Id = cancel.PullPaymentId, Archived = true }) ctx.PullPayments.Attach(new Data.PullPaymentData() {Id = cancel.PullPaymentId, Archived = true})
.Property(o => o.Archived).IsModified = true; .Property(o => o.Archived).IsModified = true;
payouts = await ctx.Payouts payouts = await ctx.Payouts
.Where(p => p.PullPaymentDataId == cancel.PullPaymentId) .Where(p => p.PullPaymentDataId == cancel.PullPaymentId)
@@ -617,10 +667,11 @@ namespace BTCPayServer.HostedServices
} }
public PullPaymentsModel.PullPaymentModel.ProgressModel CalculatePullPaymentProgress(PullPaymentData pp, DateTimeOffset now) public PullPaymentsModel.PullPaymentModel.ProgressModel CalculatePullPaymentProgress(PullPaymentData pp,
DateTimeOffset now)
{ {
var ppBlob = pp.GetBlob(); var ppBlob = pp.GetBlob();
var ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true); var ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
var nfi = _currencyNameTable.GetNumberFormatInfo(ppBlob.Currency, true); var nfi = _currencyNameTable.GetNumberFormatInfo(ppBlob.Currency, true);
var totalCompleted = pp.Payouts.Where(p => (p.State == PayoutState.Completed || var totalCompleted = pp.Payouts.Where(p => (p.State == PayoutState.Completed ||
@@ -647,16 +698,15 @@ namespace BTCPayServer.HostedServices
EndIn = pp.EndDate is { } end ? ZeroIfNegative(end - now).TimeString() : null, EndIn = pp.EndDate is { } end ? ZeroIfNegative(end - now).TimeString() : null,
}; };
} }
public TimeSpan ZeroIfNegative(TimeSpan time) public TimeSpan ZeroIfNegative(TimeSpan time)
{ {
if (time < TimeSpan.Zero) if (time < TimeSpan.Zero)
time = TimeSpan.Zero; time = TimeSpan.Zero;
return time; return time;
} }
class InternalPayoutPaidRequest class InternalPayoutPaidRequest
{ {

View File

@@ -365,6 +365,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IUIExtension>(new UIExtension("LNURL/LightningAddressOption", services.AddSingleton<IUIExtension>(new UIExtension("LNURL/LightningAddressOption",
"store-integrations-list")); "store-integrations-list"));
services.AddSingleton<IHostedService, LightningListener>(); services.AddSingleton<IHostedService, LightningListener>();
services.AddSingleton<IHostedService, LightningPendingPayoutListener>();
services.AddSingleton<PaymentMethodHandlerDictionary>(); services.AddSingleton<PaymentMethodHandlerDictionary>();

View File

@@ -0,0 +1,137 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PayoutData = BTCPayServer.Data.PayoutData;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Payments.Lightning;
public class LightningPendingPayoutListener : BaseAsyncService
{
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly StoreRepository _storeRepository;
private readonly IOptions<LightningNetworkOptions> _options;
private readonly BTCPayNetworkProvider _networkProvider;
public static int SecondsDelay = 60 * 10;
public LightningPendingPayoutListener(
LightningClientFactoryService lightningClientFactoryService,
ApplicationDbContextFactory applicationDbContextFactory,
PullPaymentHostedService pullPaymentHostedService,
LightningLikePayoutHandler lightningLikePayoutHandler,
StoreRepository storeRepository,
IOptions<LightningNetworkOptions> options,
BTCPayNetworkProvider networkProvider,
ILogger<LightningPendingPayoutListener> logger) : base(logger)
{
_lightningClientFactoryService = lightningClientFactoryService;
_applicationDbContextFactory = applicationDbContextFactory;
_pullPaymentHostedService = pullPaymentHostedService;
_lightningLikePayoutHandler = lightningLikePayoutHandler;
_storeRepository = storeRepository;
_options = options;
_networkProvider = networkProvider;
}
private async Task Act()
{
await using var context = _applicationDbContextFactory.CreateContext();
var networks = _networkProvider.GetAll()
.OfType<BTCPayNetwork>()
.Where(network => network.SupportLightning)
.ToDictionary(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike));
var payouts = await _pullPaymentHostedService.GetPayouts(
new PullPaymentHostedService.PayoutQuery()
{
States = new PayoutState[] {PayoutState.InProgress},
PaymentMethods = networks.Keys.Select(id => id.ToString()).ToArray()
}, context);
var storeIds = payouts.Select(data => data.StoreDataId).Distinct();
var stores = (await Task.WhenAll(storeIds.Select(_storeRepository.FindStore)))
.Where(data => data is not null).ToDictionary(data => data.Id, data => (StoreData)data);
foreach (IGrouping<string, PayoutData> payoutByStore in payouts.GroupBy(data => data.StoreDataId))
{
if (!stores.TryGetValue(payoutByStore.Key, out var store))
{
foreach (PayoutData payoutData in payoutByStore)
{
payoutData.State = PayoutState.Cancelled;
}
continue;
}
foreach (IGrouping<string, PayoutData> payoutByStoreByPaymentMethod in payoutByStore.GroupBy(data =>
data.PaymentMethodId))
{
var pmi = PaymentMethodId.Parse(payoutByStoreByPaymentMethod.Key);
var pm = store.GetSupportedPaymentMethods(_networkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == pmi);
if (pm is null)
{
continue;
}
var client =
pm.CreateLightningClient(networks[pmi], _options.Value, _lightningClientFactoryService);
foreach (PayoutData payoutData in payoutByStoreByPaymentMethod)
{
var proof = _lightningLikePayoutHandler.ParseProof(payoutData);
switch (proof)
{
case null:
break;
case PayoutLightningBlob payoutLightningBlob:
{
var payment = await client.GetPayment(payoutLightningBlob.Id, Cancellation);
if (payment is null)
{
continue;
}
switch (payment.Status)
{
case LightningPaymentStatus.Complete:
payoutData.State = PayoutState.Completed;
payoutLightningBlob.Preimage = payment.Preimage;
payoutData.SetProofBlob(payoutLightningBlob, null);
break;
case LightningPaymentStatus.Failed:
payoutData.State = PayoutState.Cancelled;
break;
}
break;
}
}
}
}
}
await context.SaveChangesAsync(Cancellation);
await Task.Delay(TimeSpan.FromSeconds(SecondsDelay), Cancellation);
}
internal override Task[] InitializeTasks()
{
return new[] {CreateLoopTask(Act)};
}
}

View File

@@ -1,4 +1,6 @@
using System.Linq; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -20,6 +22,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
protected readonly StoreRepository _storeRepository; protected readonly StoreRepository _storeRepository;
protected readonly PayoutProcessorData _PayoutProcesserSettings; protected readonly PayoutProcessorData _PayoutProcesserSettings;
protected readonly ApplicationDbContextFactory _applicationDbContextFactory; protected readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly PullPaymentHostedService _pullPaymentHostedService;
protected readonly BTCPayNetworkProvider _btcPayNetworkProvider; protected readonly BTCPayNetworkProvider _btcPayNetworkProvider;
protected readonly PaymentMethodId PaymentMethodId; protected readonly PaymentMethodId PaymentMethodId;
@@ -28,12 +31,14 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
StoreRepository storeRepository, StoreRepository storeRepository,
PayoutProcessorData payoutProcesserSettings, PayoutProcessorData payoutProcesserSettings,
ApplicationDbContextFactory applicationDbContextFactory, ApplicationDbContextFactory applicationDbContextFactory,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkProvider btcPayNetworkProvider) : base(logger.CreateLogger($"{payoutProcesserSettings.Processor}:{payoutProcesserSettings.StoreId}:{payoutProcesserSettings.PaymentMethod}")) BTCPayNetworkProvider btcPayNetworkProvider) : base(logger.CreateLogger($"{payoutProcesserSettings.Processor}:{payoutProcesserSettings.StoreId}:{payoutProcesserSettings.PaymentMethod}"))
{ {
_storeRepository = storeRepository; _storeRepository = storeRepository;
_PayoutProcesserSettings = payoutProcesserSettings; _PayoutProcesserSettings = payoutProcesserSettings;
PaymentMethodId = _PayoutProcesserSettings.GetPaymentMethodId(); PaymentMethodId = _PayoutProcesserSettings.GetPaymentMethodId();
_applicationDbContextFactory = applicationDbContextFactory; _applicationDbContextFactory = applicationDbContextFactory;
_pullPaymentHostedService = pullPaymentHostedService;
_btcPayNetworkProvider = btcPayNetworkProvider; _btcPayNetworkProvider = btcPayNetworkProvider;
} }
@@ -42,7 +47,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
return new[] { CreateLoopTask(Act) }; return new[] { CreateLoopTask(Act) };
} }
protected abstract Task Process(ISupportedPaymentMethod paymentMethod, PayoutData[] payouts); protected abstract Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> payouts);
private async Task Act() private async Task Act()
{ {
@@ -54,11 +59,20 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
var blob = GetBlob(_PayoutProcesserSettings); var blob = GetBlob(_PayoutProcesserSettings);
if (paymentMethod is not null) if (paymentMethod is not null)
{ {
var payouts = await GetRelevantPayouts();
if (payouts.Length > 0) await using var context = _applicationDbContextFactory.CreateContext();
var payouts = await _pullPaymentHostedService.GetPayouts(
new PullPaymentHostedService.PayoutQuery()
{
States = new[] {PayoutState.AwaitingPayment},
PaymentMethods = new[] {_PayoutProcesserSettings.PaymentMethod},
Stores = new[] {_PayoutProcesserSettings.StoreId}
}, context);
if (payouts.Any())
{ {
Logs.PayServer.LogInformation($"{payouts.Length} found to process. Starting (and after will sleep for {blob.Interval})"); Logs.PayServer.LogInformation($"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})");
await Process(paymentMethod, payouts); await Process(paymentMethod, payouts);
await context.SaveChangesAsync();
} }
} }
await Task.Delay(blob.Interval, CancellationToken); await Task.Delay(blob.Interval, CancellationToken);
@@ -69,16 +83,4 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
{ {
return InvoiceRepository.FromBytes<T>(data.Blob); return InvoiceRepository.FromBytes<T>(data.Blob);
} }
private async Task<PayoutData[]> GetRelevantPayouts()
{
await using var context = _applicationDbContextFactory.CreateContext();
var pmi = _PayoutProcesserSettings.PaymentMethod;
return await context.Payouts
.Where(data => data.State == PayoutState.AwaitingPayment)
.Where(data => data.PaymentMethodId == pmi)
.Where(data => data.StoreDataId == _PayoutProcesserSettings.StoreId)
.OrderBy(data => data.Date)
.ToArrayAsync();
}
} }

View File

@@ -7,6 +7,7 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Data.Data; using BTCPayServer.Data.Data;
using BTCPayServer.Data.Payouts.LightningLike; using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
@@ -38,8 +39,8 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
UserService userService, UserService userService,
ILoggerFactory logger, IOptions<LightningNetworkOptions> options, ILoggerFactory logger, IOptions<LightningNetworkOptions> options,
StoreRepository storeRepository, PayoutProcessorData payoutProcesserSettings, StoreRepository storeRepository, PayoutProcessorData payoutProcesserSettings,
ApplicationDbContextFactory applicationDbContextFactory, BTCPayNetworkProvider btcPayNetworkProvider) : ApplicationDbContextFactory applicationDbContextFactory, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkProvider btcPayNetworkProvider) :
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, pullPaymentHostedService,
btcPayNetworkProvider) btcPayNetworkProvider)
{ {
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
@@ -51,11 +52,8 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
_network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(_PayoutProcesserSettings.GetPaymentMethodId().CryptoCode); _network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(_PayoutProcesserSettings.GetPaymentMethodId().CryptoCode);
} }
protected override async Task Process(ISupportedPaymentMethod paymentMethod, PayoutData[] payouts) protected override async Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> payouts)
{ {
await using var ctx = _applicationDbContextFactory.CreateContext();
var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod; var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod;
if (lightningSupportedPaymentMethod.IsInternalNode && if (lightningSupportedPaymentMethod.IsInternalNode &&
@@ -70,8 +68,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
lightningSupportedPaymentMethod.CreateLightningClient(_network, _options.Value, lightningSupportedPaymentMethod.CreateLightningClient(_network, _options.Value,
_lightningClientFactoryService); _lightningClientFactoryService);
foreach (var payoutData in payouts) foreach (var payoutData in payouts)
{ {
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings); var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
@@ -81,29 +77,18 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
switch (claim.destination) switch (claim.destination)
{ {
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData, _payoutHandler, blob, var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData,
_payoutHandler, blob,
lnurlPayClaimDestinaton, _network.NBitcoinNetwork); lnurlPayClaimDestinaton, _network.NBitcoinNetwork);
if (lnurlResult.Item2 is not null) if (lnurlResult.Item2 is not null)
{ {
continue; continue;
} }
await TrypayBolt(client, blob, payoutData,
if (await TrypayBolt(client, blob, payoutData, lnurlResult.Item1);
lnurlResult.Item1))
{
ctx.Attach(payoutData).State = EntityState.Modified;
payoutData.State = PayoutState.Completed;
}
break; break;
case BoltInvoiceClaimDestination item1: case BoltInvoiceClaimDestination item1:
if (await TrypayBolt(client, blob, payoutData, item1.PaymentRequest)) await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
{
ctx.Attach(payoutData).State = EntityState.Modified;
payoutData.State = PayoutState.Completed;
}
break; break;
} }
} }
@@ -112,9 +97,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}"); Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}");
} }
} }
await ctx.SaveChangesAsync();
} }
//we group per store and init the transfers by each //we group per store and init the transfers by each

View File

@@ -41,8 +41,9 @@ namespace BTCPayServer.PayoutProcessors.OnChain
EventAggregator eventAggregator, EventAggregator eventAggregator,
StoreRepository storeRepository, StoreRepository storeRepository,
PayoutProcessorData payoutProcesserSettings, PayoutProcessorData payoutProcesserSettings,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkProvider btcPayNetworkProvider) : BTCPayNetworkProvider btcPayNetworkProvider) :
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,pullPaymentHostedService,
btcPayNetworkProvider) btcPayNetworkProvider)
{ {
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
@@ -52,7 +53,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
} }
protected override async Task Process(ISupportedPaymentMethod paymentMethod, PayoutData[] payouts) protected override async Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> payouts)
{ {
var storePaymentMethod = paymentMethod as DerivationSchemeSettings; var storePaymentMethod = paymentMethod as DerivationSchemeSettings;
if (storePaymentMethod?.IsHotWallet is not true) if (storePaymentMethod?.IsHotWallet is not true)
@@ -143,11 +144,9 @@ namespace BTCPayServer.PayoutProcessors.OnChain
{ {
try try
{ {
await using var context = _applicationDbContextFactory.CreateContext();
var txHash = workingTx.GetHash(); var txHash = workingTx.GetHash();
foreach (PayoutData payoutData in transfersProcessing) foreach (PayoutData payoutData in transfersProcessing)
{ {
context.Attach(payoutData).State = EntityState.Modified;
payoutData.State = PayoutState.InProgress; payoutData.State = PayoutState.InProgress;
_bitcoinLikePayoutHandler.SetProofBlob(payoutData, _bitcoinLikePayoutHandler.SetProofBlob(payoutData,
new PayoutTransactionOnChainBlob() new PayoutTransactionOnChainBlob()
@@ -156,7 +155,6 @@ namespace BTCPayServer.PayoutProcessors.OnChain
TransactionId = txHash, TransactionId = txHash,
Candidates = new HashSet<uint256>() { txHash } Candidates = new HashSet<uint256>() { txHash }
}); });
await context.SaveChangesAsync();
} }
TaskCompletionSource<bool> tcs = new(); TaskCompletionSource<bool> tcs = new();
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();

View File

@@ -9,6 +9,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.PayoutProcessors; namespace BTCPayServer.PayoutProcessors;
@@ -18,6 +19,10 @@ public class PayoutProcessorUpdated
public PayoutProcessorData Data { get; set; } public PayoutProcessorData Data { get; set; }
public TaskCompletionSource Processed { get; set; } public TaskCompletionSource Processed { get; set; }
public override string ToString()
{
return $"{Data}";
}
} }
public class PayoutProcessorService : EventHostedServiceBase public class PayoutProcessorService : EventHostedServiceBase

View File

@@ -63,9 +63,9 @@ namespace BTCPayServer.Services.Labels
string PayoutLabelText(KeyValuePair<string, List<string>> pair) string PayoutLabelText(KeyValuePair<string, List<string>> pair)
{ {
if (pair.Value.Count == 1) if (pair.Value.Count == 1)
return $"Paid a payout of a pull payment ({pair.Key})"; return $"Paid a payout {(string.IsNullOrEmpty(pair.Key)? string.Empty: $"of a pull payment ({pair.Key})")}";
else else
return $"Paid payouts of a pull payment ({pair.Key})"; return $"Paid {pair.Value.Count} payouts {(string.IsNullOrEmpty(pair.Key)? string.Empty: $"of a pull payment ({pair.Key})")}";
} }
if (uncoloredLabel is ReferenceLabel refLabel) if (uncoloredLabel is ReferenceLabel refLabel)
@@ -103,7 +103,7 @@ namespace BTCPayServer.Services.Labels
{ {
coloredLabel.Tooltip = payoutLabel.PullPaymentPayouts.Count > 1 coloredLabel.Tooltip = payoutLabel.PullPaymentPayouts.Count > 1
? $"<ul>{string.Join(string.Empty, payoutLabel.PullPaymentPayouts.Select(pair => $"<li>{PayoutLabelText(pair)}</li>"))}</ul>" ? $"<ul>{string.Join(string.Empty, payoutLabel.PullPaymentPayouts.Select(pair => $"<li>{PayoutLabelText(pair)}</li>"))}</ul>"
: payoutLabel.PullPaymentPayouts.Select(PayoutLabelText).ToString(); : PayoutLabelText(payoutLabel.PullPaymentPayouts.First());
coloredLabel.Link = string.IsNullOrEmpty(payoutLabel.WalletId) coloredLabel.Link = string.IsNullOrEmpty(payoutLabel.WalletId)
? null ? null