mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
parent
d6ae34929e
commit
d0b26e9f69
19 changed files with 435 additions and 106 deletions
|
@ -21,4 +21,9 @@ public class PayoutProcessorData
|
|||
.HasOne(o => o.Store)
|
||||
.WithMany(data => data.PayoutProcessors).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Processor} {PaymentMethod} {StoreId}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ using BTCPayServer.Events;
|
|||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
|
@ -2455,6 +2456,50 @@ namespace BTCPayServer.Tests
|
|||
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)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePayoutProcessorsThroughAPI()
|
||||
|
|
16
BTCPayServer.Tests/docker-customer-lncli-holdinvoice.sh
Executable file
16
BTCPayServer.Tests/docker-customer-lncli-holdinvoice.sh
Executable 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"
|
|
@ -180,6 +180,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
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")]
|
||||
[AllowAnonymous]
|
||||
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);
|
||||
if (pp is null)
|
||||
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
|
||||
.Select(ToModel).ToList());
|
||||
}
|
||||
|
@ -201,10 +215,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
if (payoutId is null)
|
||||
return PayoutNotFound();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, true);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var payout = pp.Payouts.FirstOrDefault(p => p.Id == payoutId);
|
||||
|
||||
var payout = (await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
PullPayments = new[] {pullPaymentId}, PayoutIds = new[] {payoutId}
|
||||
})).FirstOrDefault();
|
||||
|
||||
|
||||
if (payout is null)
|
||||
return PayoutNotFound();
|
||||
return base.Ok(ToModel(payout));
|
||||
|
@ -392,10 +409,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetStorePayouts(string storeId, bool includeCancelled = false)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payouts = await ctx.Payouts
|
||||
.Where(p => p.StoreDataId == storeId && (p.State != PayoutState.Cancelled || includeCancelled))
|
||||
.ToListAsync();
|
||||
var payouts = await _pullPaymentService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
Stores = new[] {storeId},
|
||||
States = GetStateFilter(includeCancelled)
|
||||
});
|
||||
|
||||
|
||||
return base.Ok(payouts
|
||||
.Select(ToModel).ToList());
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using BTCPayServer.Abstractions.Constants;
|
|||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.Lightning;
|
||||
using BTCPayServer.PayoutProcessors.Settings;
|
||||
|
@ -36,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
|
||||
string storeId, string? paymentMethod)
|
||||
{
|
||||
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null;
|
||||
var configured =
|
||||
await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery()
|
||||
|
@ -68,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
|
||||
{
|
||||
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
|
||||
var activeProcessor =
|
||||
(await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery()
|
||||
|
|
|
@ -5,6 +5,7 @@ using BTCPayServer.Abstractions.Constants;
|
|||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.PayoutProcessors.Settings;
|
||||
|
@ -36,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
|
||||
string storeId, string? paymentMethod)
|
||||
{
|
||||
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null;
|
||||
var configured =
|
||||
await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery()
|
||||
|
@ -68,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
|
||||
{
|
||||
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
|
||||
var activeProcessor =
|
||||
(await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery()
|
||||
|
|
|
@ -103,9 +103,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
{
|
||||
return null;
|
||||
}
|
||||
var raw = JObject.Parse(Encoding.UTF8.GetString(payout.Proof));
|
||||
if (raw.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType) &&
|
||||
proofType.Value<string>() == ManualPayoutProof.Type)
|
||||
|
||||
ParseProofType(payout.Proof, out var raw, out var proofType);
|
||||
if (proofType == ManualPayoutProof.Type)
|
||||
{
|
||||
return raw.ToObject<ManualPayoutProof>();
|
||||
}
|
||||
|
@ -118,6 +118,22 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
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)
|
||||
{
|
||||
subscribe(new[] { typeof(NewOnChainTransactionEvent), typeof(NewBlockEvent) });
|
||||
|
@ -443,11 +459,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
|
||||
public void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(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;
|
||||
}
|
||||
data.SetProofBlob(blob, _jsonSerializerSettings.GetSerializer(data.GetPaymentMethodId().CryptoCode));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
@ -13,6 +14,8 @@ using LNURL;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
{
|
||||
|
@ -117,7 +120,17 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
|
||||
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)
|
||||
|
@ -174,5 +187,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout",
|
||||
"UILightningLikePayout", new { cryptoCode = paymentMethodId.CryptoCode, payoutIds }));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
{
|
||||
public class PayoutLightningBlob : IPayoutProof
|
||||
{
|
||||
public string Bolt11Invoice { get; set; }
|
||||
public string Preimage { get; set; }
|
||||
public string PaymentHash { get; set; }
|
||||
|
||||
public string ProofType { get; }
|
||||
public string ProofType { get; } = "PayoutLightningBlob";
|
||||
public string Link { get; } = null;
|
||||
public string Id => PaymentHash;
|
||||
public string Preimage { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
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);
|
||||
if (boltAmount != payoutBlob.CryptoAmount)
|
||||
{
|
||||
|
||||
payoutData.State = PayoutState.Cancelled;
|
||||
return new ResultVM
|
||||
{
|
||||
PayoutId = payoutData.Id,
|
||||
|
@ -271,13 +277,36 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
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
|
||||
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
|
||||
: null;
|
||||
payoutData.State = PayoutState.Completed;
|
||||
using var cts = new CancellationTokenSource(SendTimeout);
|
||||
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
|
||||
new PayInvoiceParams()
|
||||
{
|
||||
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
|
||||
{
|
||||
PayoutId = payoutData.Id,
|
||||
|
@ -286,14 +315,21 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
return new ResultVM
|
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
||||
{
|
||||
PayoutId = payoutData.Id,
|
||||
Result = result.Result,
|
||||
Destination = payoutBlob.Destination,
|
||||
Message = result.ErrorDetail
|
||||
};
|
||||
// Timeout, potentially caused by hold invoices
|
||||
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
|
||||
payoutData.State = PayoutState.InProgress;
|
||||
|
||||
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."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -39,12 +39,13 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
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)
|
||||
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
|
||||
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
|
||||
{
|
||||
|
|
|
@ -138,6 +138,52 @@ namespace BTCPayServer.HostedServices
|
|||
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)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
|
@ -205,7 +251,7 @@ namespace BTCPayServer.HostedServices
|
|||
payoutHandler.StartBackgroundCheck(Subscribe);
|
||||
}
|
||||
|
||||
return new[] { Loop() };
|
||||
return new[] {Loop()};
|
||||
}
|
||||
|
||||
private void Subscribe(params Type[] events)
|
||||
|
@ -326,7 +372,8 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
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;
|
||||
var cryptoAmount = payoutBlob.Amount / req.Rate;
|
||||
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethod);
|
||||
|
@ -375,7 +422,7 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
if (req.Request.Proof != null)
|
||||
{
|
||||
payout.SetProofBlob(req.Request.Proof);
|
||||
payout.SetProofBlob(req.Request.Proof, null);
|
||||
}
|
||||
|
||||
payout.State = PayoutState.Completed;
|
||||
|
@ -465,7 +512,7 @@ namespace BTCPayServer.HostedServices
|
|||
: await ctx.Payouts.GetPayoutInPeriod(pp, now)
|
||||
.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 totalPayout = payouts?.Select(p => p.Blob.Amount)?.Sum();
|
||||
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - (totalPayout ?? 0);
|
||||
|
@ -493,8 +540,7 @@ namespace BTCPayServer.HostedServices
|
|||
};
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
Amount = claimed,
|
||||
Destination = req.ClaimRequest.Destination.ToString()
|
||||
Amount = claimed, Destination = req.ClaimRequest.Destination.ToString()
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
|
@ -502,7 +548,7 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination);
|
||||
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);
|
||||
var rateResult = await GetRate(payout, null, CancellationToken.None);
|
||||
|
@ -511,15 +557,19 @@ namespace BTCPayServer.HostedServices
|
|||
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
|
||||
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)
|
||||
{
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout));
|
||||
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
|
||||
new PayoutNotification()
|
||||
|
@ -550,7 +600,7 @@ namespace BTCPayServer.HostedServices
|
|||
List<PayoutData> payouts = 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;
|
||||
payouts = await ctx.Payouts
|
||||
.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 ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
|
||||
var nfi = _currencyNameTable.GetNumberFormatInfo(ppBlob.Currency, true);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public TimeSpan ZeroIfNegative(TimeSpan time)
|
||||
{
|
||||
if (time < TimeSpan.Zero)
|
||||
time = TimeSpan.Zero;
|
||||
return time;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class InternalPayoutPaidRequest
|
||||
{
|
||||
|
|
|
@ -365,6 +365,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddSingleton<IUIExtension>(new UIExtension("LNURL/LightningAddressOption",
|
||||
"store-integrations-list"));
|
||||
services.AddSingleton<IHostedService, LightningListener>();
|
||||
services.AddSingleton<IHostedService, LightningPendingPayoutListener>();
|
||||
|
||||
services.AddSingleton<PaymentMethodHandlerDictionary>();
|
||||
|
||||
|
|
|
@ -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)};
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -20,6 +22,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
protected readonly StoreRepository _storeRepository;
|
||||
protected readonly PayoutProcessorData _PayoutProcesserSettings;
|
||||
protected readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
protected readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
protected readonly PaymentMethodId PaymentMethodId;
|
||||
|
||||
|
@ -28,12 +31,14 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
StoreRepository storeRepository,
|
||||
PayoutProcessorData payoutProcesserSettings,
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider) : base(logger.CreateLogger($"{payoutProcesserSettings.Processor}:{payoutProcesserSettings.StoreId}:{payoutProcesserSettings.PaymentMethod}"))
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_PayoutProcesserSettings = payoutProcesserSettings;
|
||||
PaymentMethodId = _PayoutProcesserSettings.GetPaymentMethodId();
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
|
||||
|
@ -42,7 +47,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
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()
|
||||
{
|
||||
|
@ -54,11 +59,20 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
var blob = GetBlob(_PayoutProcesserSettings);
|
||||
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 context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
await Task.Delay(blob.Interval, CancellationToken);
|
||||
|
@ -69,16 +83,4 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using BTCPayServer.Configuration;
|
|||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Data;
|
||||
using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
@ -38,8 +39,8 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
UserService userService,
|
||||
ILoggerFactory logger, IOptions<LightningNetworkOptions> options,
|
||||
StoreRepository storeRepository, PayoutProcessorData payoutProcesserSettings,
|
||||
ApplicationDbContextFactory applicationDbContextFactory, BTCPayNetworkProvider btcPayNetworkProvider) :
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,
|
||||
ApplicationDbContextFactory applicationDbContextFactory, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkProvider btcPayNetworkProvider) :
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, pullPaymentHostedService,
|
||||
btcPayNetworkProvider)
|
||||
{
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
|
@ -51,11 +52,8 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
_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;
|
||||
|
||||
if (lightningSupportedPaymentMethod.IsInternalNode &&
|
||||
|
@ -70,8 +68,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
lightningSupportedPaymentMethod.CreateLightningClient(_network, _options.Value,
|
||||
_lightningClientFactoryService);
|
||||
|
||||
|
||||
|
||||
foreach (var payoutData in payouts)
|
||||
{
|
||||
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||
|
@ -81,29 +77,18 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
switch (claim.destination)
|
||||
{
|
||||
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
|
||||
var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData, _payoutHandler, blob,
|
||||
var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData,
|
||||
_payoutHandler, blob,
|
||||
lnurlPayClaimDestinaton, _network.NBitcoinNetwork);
|
||||
if (lnurlResult.Item2 is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await TrypayBolt(client, blob, payoutData,
|
||||
lnurlResult.Item1))
|
||||
{
|
||||
ctx.Attach(payoutData).State = EntityState.Modified;
|
||||
payoutData.State = PayoutState.Completed;
|
||||
}
|
||||
|
||||
await TrypayBolt(client, blob, payoutData,
|
||||
lnurlResult.Item1);
|
||||
break;
|
||||
|
||||
case BoltInvoiceClaimDestination item1:
|
||||
if (await TrypayBolt(client, blob, payoutData, item1.PaymentRequest))
|
||||
{
|
||||
ctx.Attach(payoutData).State = EntityState.Modified;
|
||||
payoutData.State = PayoutState.Completed;
|
||||
}
|
||||
|
||||
await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -112,9 +97,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
//we group per store and init the transfers by each
|
||||
|
|
|
@ -41,8 +41,9 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
PayoutProcessorData payoutProcesserSettings,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider) :
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,pullPaymentHostedService,
|
||||
btcPayNetworkProvider)
|
||||
{
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
|
@ -52,7 +53,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
_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;
|
||||
if (storePaymentMethod?.IsHotWallet is not true)
|
||||
|
@ -143,11 +144,9 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
{
|
||||
try
|
||||
{
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var txHash = workingTx.GetHash();
|
||||
foreach (PayoutData payoutData in transfersProcessing)
|
||||
{
|
||||
context.Attach(payoutData).State = EntityState.Modified;
|
||||
payoutData.State = PayoutState.InProgress;
|
||||
_bitcoinLikePayoutHandler.SetProofBlob(payoutData,
|
||||
new PayoutTransactionOnChainBlob()
|
||||
|
@ -156,7 +155,6 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
TransactionId = txHash,
|
||||
Candidates = new HashSet<uint256>() { txHash }
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
TaskCompletionSource<bool> tcs = new();
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
|
@ -9,6 +9,7 @@ using BTCPayServer.HostedServices;
|
|||
using BTCPayServer.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
|
@ -18,6 +19,10 @@ public class PayoutProcessorUpdated
|
|||
public PayoutProcessorData Data { get; set; }
|
||||
|
||||
public TaskCompletionSource Processed { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Data}";
|
||||
}
|
||||
}
|
||||
|
||||
public class PayoutProcessorService : EventHostedServiceBase
|
||||
|
|
|
@ -63,9 +63,9 @@ namespace BTCPayServer.Services.Labels
|
|||
string PayoutLabelText(KeyValuePair<string, List<string>> pair)
|
||||
{
|
||||
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
|
||||
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)
|
||||
|
@ -103,7 +103,7 @@ namespace BTCPayServer.Services.Labels
|
|||
{
|
||||
coloredLabel.Tooltip = payoutLabel.PullPaymentPayouts.Count > 1
|
||||
? $"<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)
|
||||
? null
|
||||
|
|
Loading…
Add table
Reference in a new issue