Payjoin label fixes (#3986)

* Payjoin label fixes

* When a payjoin label was applied, coin selection filter would not work
* When a payjoin happened with a receive address wallet, the payjoin label was not applied
* Coin selection shows when a utxo is currently reserved for a payjoin. Applies both to UI and to GF API

* remove reserved label

* Update BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs
This commit is contained in:
Andrew Camilleri 2022-07-23 13:26:13 +02:00 committed by GitHub
parent 2e6246e385
commit bec888da19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 82 additions and 71 deletions

View file

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Payments.PayJoin;
public interface IUTXOLocker
{
Task<bool> TryLock(OutPoint outpoint);
Task<bool> TryUnlock(params OutPoint[] outPoints);
Task<bool> TryLockInputs(OutPoint[] outPoints);
Task<HashSet<OutPoint>> FindLocks(OutPoint[] outpoints);
}

View file

@ -69,7 +69,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(); using var tester = CreateServerTester();
await tester.StartAsync(); await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC"); var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var repo = tester.PayTester.GetService<PayJoinRepository>(); var repo = tester.PayTester.GetService<UTXOLocker>();
var outpoint = RandomOutpoint(); var outpoint = RandomOutpoint();
// Should not be locked // Should not be locked
@ -166,7 +166,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester(); using var tester = CreateServerTester();
await tester.StartAsync(); await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>(); var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>(); var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
broadcaster.Disable(); broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC"); var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network); var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
@ -633,7 +633,7 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>(); var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>(); var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
broadcaster.Disable(); broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC"); var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network); var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
@ -1155,7 +1155,7 @@ retry:
Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted)); Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0]; ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0];
}); });
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>(); var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
// The outpoint should now be available for next pj selection // The outpoint should now be available for next pj selection
Assert.False(await payjoinRepository.TryUnlock(ourOutpoint)); Assert.False(await payjoinRepository.TryUnlock(ourOutpoint));
} }

View file

@ -53,6 +53,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly WalletReceiveService _walletReceiveService; private readonly WalletReceiveService _walletReceiveService;
private readonly IFeeProviderFactory _feeProviderFactory; private readonly IFeeProviderFactory _feeProviderFactory;
private readonly LabelFactory _labelFactory; private readonly LabelFactory _labelFactory;
private readonly UTXOLocker _utxoLocker;
public GreenfieldStoreOnChainWalletsController( public GreenfieldStoreOnChainWalletsController(
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
@ -68,7 +69,8 @@ namespace BTCPayServer.Controllers.Greenfield
EventAggregator eventAggregator, EventAggregator eventAggregator,
WalletReceiveService walletReceiveService, WalletReceiveService walletReceiveService,
IFeeProviderFactory feeProviderFactory, IFeeProviderFactory feeProviderFactory,
LabelFactory labelFactory LabelFactory labelFactory,
UTXOLocker utxoLocker
) )
{ {
_authorizationService = authorizationService; _authorizationService = authorizationService;
@ -85,6 +87,7 @@ namespace BTCPayServer.Controllers.Greenfield
_walletReceiveService = walletReceiveService; _walletReceiveService = walletReceiveService;
_feeProviderFactory = feeProviderFactory; _feeProviderFactory = feeProviderFactory;
_labelFactory = labelFactory; _labelFactory = labelFactory;
_utxoLocker = utxoLocker;
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -317,9 +320,11 @@ namespace BTCPayServer.Controllers.Greenfield
var walletId = new WalletId(storeId, cryptoCode); var walletId = new WalletId(storeId, cryptoCode);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId); var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation); var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
return Ok(utxos.Select(coin => return Ok(utxos.Select(coin =>
{ {
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info); walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
var labels = info?.Labels ?? new Dictionary<string, LabelData>();
return new OnChainWalletUTXOData() return new OnChainWalletUTXOData()
{ {
Outpoint = coin.OutPoint, Outpoint = coin.OutPoint,

View file

@ -65,11 +65,7 @@ namespace BTCPayServer.Controllers
private readonly PayjoinClient _payjoinClient; private readonly PayjoinClient _payjoinClient;
private readonly LabelFactory _labelFactory; private readonly LabelFactory _labelFactory;
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly UTXOLocker _utxoLocker;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly NBXplorerConnectionFactory _connectionFactory;
private readonly WalletHistogramService _walletHistogramService; private readonly WalletHistogramService _walletHistogramService;
readonly CurrencyNameTable _currencyTable; readonly CurrencyNameTable _currencyTable;
@ -79,10 +75,8 @@ namespace BTCPayServer.Controllers
CurrencyNameTable currencyTable, CurrencyNameTable currencyTable,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
MvcNewtonsoftJsonOptions mvcJsonOptions,
NBXplorerDashboard dashboard, NBXplorerDashboard dashboard,
WalletHistogramService walletHistogramService, WalletHistogramService walletHistogramService,
NBXplorerConnectionFactory connectionFactory,
RateFetcher rateProvider, RateFetcher rateProvider,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
ExplorerClientProvider explorerProvider, ExplorerClientProvider explorerProvider,
@ -94,12 +88,9 @@ namespace BTCPayServer.Controllers
DelayedTransactionBroadcaster broadcaster, DelayedTransactionBroadcaster broadcaster,
PayjoinClient payjoinClient, PayjoinClient payjoinClient,
LabelFactory labelFactory, LabelFactory labelFactory,
ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
PullPaymentHostedService pullPaymentService,
IEnumerable<IPayoutHandler> payoutHandlers,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
PullPaymentHostedService pullPaymentHostedService) PullPaymentHostedService pullPaymentHostedService,
UTXOLocker utxoLocker)
{ {
_currencyTable = currencyTable; _currencyTable = currencyTable;
Repository = repo; Repository = repo;
@ -119,12 +110,8 @@ namespace BTCPayServer.Controllers
_payjoinClient = payjoinClient; _payjoinClient = payjoinClient;
_labelFactory = labelFactory; _labelFactory = labelFactory;
_pullPaymentHostedService = pullPaymentHostedService; _pullPaymentHostedService = pullPaymentHostedService;
_dbContextFactory = dbContextFactory; _utxoLocker = utxoLocker;
_jsonSerializerSettings = jsonSerializerSettings;
_pullPaymentService = pullPaymentService;
_payoutHandlers = payoutHandlers;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
_connectionFactory = connectionFactory;
_walletHistogramService = walletHistogramService; _walletHistogramService = walletHistogramService;
} }
@ -625,15 +612,15 @@ namespace BTCPayServer.Controllers
vm.InputsAvailable = utxos.Select(coin => vm.InputsAvailable = utxos.Select(coin =>
{ {
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info); walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
var labels = info?.Labels == null
? new List<ColoredLabel>()
: _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request).ToList();
return new WalletSendModel.InputSelectionOption() return new WalletSendModel.InputSelectionOption()
{ {
Outpoint = coin.OutPoint.ToString(), Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network), Amount = coin.Value.GetValue(network),
Comment = info?.Comment, Comment = info?.Comment,
Labels = Labels = labels,
info == null
? null
: _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()), coin.OutPoint.Hash.ToString()),
Confirmations = coin.Confirmations Confirmations = coin.Confirmations

View file

@ -14,8 +14,6 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Labels; using BTCPayServer.Services.Labels;
using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.PaymentRequests;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.HostedServices namespace BTCPayServer.HostedServices
{ {
@ -48,14 +46,6 @@ namespace BTCPayServer.HostedServices
{ {
UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id) UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id)
}; };
if (invoiceEvent.Invoice.GetPayments(invoiceEvent.Payment.GetCryptoCode(), false).Any(entity =>
entity.GetCryptoPaymentData() is BitcoinLikePaymentData pData &&
pData.PayjoinInformation?.CoinjoinTransactionHash == transactionId))
{
labels.Add(UpdateTransactionLabel.PayjoinLabelTemplate());
}
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice)) foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{ {
labels.Add(UpdateTransactionLabel.PaymentRequestLabelTemplate(paymentId)); labels.Add(UpdateTransactionLabel.PaymentRequestLabelTemplate(paymentId));

View file

@ -27,7 +27,7 @@ namespace BTCPayServer.Payments.Bitcoin
public class NBXplorerListener : IHostedService public class NBXplorerListener : IHostedService
{ {
readonly EventAggregator _Aggregator; readonly EventAggregator _Aggregator;
private readonly PayJoinRepository _payJoinRepository; private readonly UTXOLocker _utxoLocker;
readonly ExplorerClientProvider _ExplorerClients; readonly ExplorerClientProvider _ExplorerClients;
private readonly PaymentService _paymentService; private readonly PaymentService _paymentService;
readonly InvoiceRepository _InvoiceRepository; readonly InvoiceRepository _InvoiceRepository;
@ -38,7 +38,7 @@ namespace BTCPayServer.Payments.Bitcoin
BTCPayWalletProvider wallets, BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
EventAggregator aggregator, EventAggregator aggregator,
PayJoinRepository payjoinRepository, UTXOLocker payjoinRepository,
PaymentService paymentService, PaymentService paymentService,
Logs logs) Logs logs)
{ {
@ -48,7 +48,7 @@ namespace BTCPayServer.Payments.Bitcoin
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients; _ExplorerClients = explorerClients;
_Aggregator = aggregator; _Aggregator = aggregator;
_payJoinRepository = payjoinRepository; _utxoLocker = payjoinRepository;
_paymentService = paymentService; _paymentService = paymentService;
} }
@ -343,7 +343,7 @@ namespace BTCPayServer.Payments.Bitcoin
// reuse our outpoint for another PJ // reuse our outpoint for another PJ
(originalPJBroadcastable is false && !cjPJBroadcasted)) (originalPJBroadcastable is false && !cjPJBroadcasted))
{ {
await _payJoinRepository.TryUnlock(payjoinInformation.ContributedOutPoints); await _utxoLocker.TryUnlock(payjoinInformation.ContributedOutPoints);
} }
await _paymentService.UpdatePayments(updatedPaymentEntities); await _paymentService.UpdatePayments(updatedPaymentEntities);

View file

@ -83,7 +83,7 @@ namespace BTCPayServer.Payments.PayJoin
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly ExplorerClientProvider _explorerClientProvider; private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayWalletProvider _btcPayWalletProvider; private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly PayJoinRepository _payJoinRepository; private readonly UTXOLocker _utxoLocker;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly NBXplorerDashboard _dashboard; private readonly NBXplorerDashboard _dashboard;
private readonly DelayedTransactionBroadcaster _broadcaster; private readonly DelayedTransactionBroadcaster _broadcaster;
@ -97,7 +97,7 @@ namespace BTCPayServer.Payments.PayJoin
public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider, public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider, InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider,
BTCPayWalletProvider btcPayWalletProvider, BTCPayWalletProvider btcPayWalletProvider,
PayJoinRepository payJoinRepository, UTXOLocker utxoLocker,
EventAggregator eventAggregator, EventAggregator eventAggregator,
NBXplorerDashboard dashboard, NBXplorerDashboard dashboard,
DelayedTransactionBroadcaster broadcaster, DelayedTransactionBroadcaster broadcaster,
@ -111,7 +111,7 @@ namespace BTCPayServer.Payments.PayJoin
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_btcPayWalletProvider = btcPayWalletProvider; _btcPayWalletProvider = btcPayWalletProvider;
_payJoinRepository = payJoinRepository; _utxoLocker = utxoLocker;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_dashboard = dashboard; _dashboard = dashboard;
_broadcaster = broadcaster; _broadcaster = broadcaster;
@ -148,7 +148,7 @@ namespace BTCPayServer.Payments.PayJoin
}); });
} }
await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository, Logs); await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _utxoLocker, Logs);
ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug) ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug)
{ {
ctx.Logs.Write($"Payjoin error: {debug}", InvoiceEventData.EventSeverity.Error); ctx.Logs.Write($"Payjoin error: {debug}", InvoiceEventData.EventSeverity.Error);
@ -322,7 +322,7 @@ namespace BTCPayServer.Payments.PayJoin
} }
if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray())) if (!await _utxoLocker.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray()))
{ {
// We do not broadcast, since we might double spend a delayed transaction of a previous payjoin // We do not broadcast, since we might double spend a delayed transaction of a previous payjoin
ctx.DoNotBroadcast(); ctx.DoNotBroadcast();
@ -502,18 +502,23 @@ namespace BTCPayServer.Payments.PayJoin
_eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
} }
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction);
var labels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo =>
new KeyValuePair<uint256, List<(string color, Label label)>>(utxo.Key,
new List<(string color, Label label)>()
{
UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice?.Id)
}))
.ToDictionary(pair => pair.Key, pair => pair.Value);
labels.Add(originalPaymentData.PayjoinInformation.CoinjoinTransactionHash, new List<(string color, Label label)>()
{
UpdateTransactionLabel.PayjoinLabelTemplate()
});
_eventAggregator.Publish(new UpdateTransactionLabel() _eventAggregator.Publish(new UpdateTransactionLabel()
{ {
WalletId = walletId, WalletId = walletId,
TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => TransactionLabels = labels
new KeyValuePair<uint256, List<(string color, Label label)>>(utxo.Key,
new List<(string color, Label label)>()
{
UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice?.Id)
}))
.ToDictionary(pair => pair.Key, pair => pair.Value)
}); });
ctx.Success(); ctx.Success();
// BTCPay Server support PSBT set as hex // BTCPay Server support PSBT set as hex
@ -608,7 +613,7 @@ namespace BTCPayServer.Payments.PayJoin
{ {
continue; continue;
} }
if (await _payJoinRepository.TryLock(availableUtxo.Outpoint)) if (await _utxoLocker.TryLock(availableUtxo.Outpoint))
{ {
return (new[] { availableUtxo }, PayjoinUtxoSelectionType.HeuristicBased); return (new[] { availableUtxo }, PayjoinUtxoSelectionType.HeuristicBased);
} }
@ -620,7 +625,7 @@ namespace BTCPayServer.Payments.PayJoin
{ {
if (currentTry >= maxTries) if (currentTry >= maxTries)
break; break;
if (await _payJoinRepository.TryLock(utxo.Outpoint)) if (await _utxoLocker.TryLock(utxo.Outpoint))
{ {
return (new[] { utxo }, PayjoinUtxoSelectionType.Ordered); return (new[] { utxo }, PayjoinUtxoSelectionType.Ordered);
} }

View file

@ -13,7 +13,8 @@ namespace BTCPayServer.Payments.PayJoin
{ {
services.AddSingleton<DelayedTransactionBroadcaster>(); services.AddSingleton<DelayedTransactionBroadcaster>();
services.AddSingleton<IHostedService, HostedServices.DelayedTransactionBroadcasterHostedService>(); services.AddSingleton<IHostedService, HostedServices.DelayedTransactionBroadcasterHostedService>();
services.AddSingleton<PayJoinRepository>(); services.AddSingleton<UTXOLocker>();
services.AddSingleton<IUTXOLocker>(provider => provider.GetRequiredService<UTXOLocker>());
services.AddSingleton<IPayjoinServerCommunicator, PayjoinServerCommunicator>(); services.AddSingleton<IPayjoinServerCommunicator, PayjoinServerCommunicator>();
services.AddSingleton<PayjoinClient>(); services.AddSingleton<PayjoinClient>();
services.AddTransient<Socks5HttpClientHandler>(); services.AddTransient<Socks5HttpClientHandler>();

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -6,21 +7,19 @@ using NBitcoin;
namespace BTCPayServer.Payments.PayJoin namespace BTCPayServer.Payments.PayJoin
{ {
public class PayJoinRepository public class UTXOLocker : IUTXOLocker
{ {
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
public PayJoinRepository(ApplicationDbContextFactory dbContextFactory) public UTXOLocker(ApplicationDbContextFactory dbContextFactory)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
} }
public async Task<bool> TryLock(OutPoint outpoint) public async Task<bool> TryLock(OutPoint outpoint)
{ {
using var ctx = _dbContextFactory.CreateContext(); using var ctx = _dbContextFactory.CreateContext();
ctx.PayjoinLocks.Add(new PayjoinLock() ctx.PayjoinLocks.Add(new PayjoinLock() {Id = outpoint.ToString()});
{
Id = outpoint.ToString()
});
try try
{ {
return await ctx.SaveChangesAsync() == 1; return await ctx.SaveChangesAsync() == 1;
@ -36,11 +35,9 @@ namespace BTCPayServer.Payments.PayJoin
using var ctx = _dbContextFactory.CreateContext(); using var ctx = _dbContextFactory.CreateContext();
foreach (OutPoint outPoint in outPoints) foreach (OutPoint outPoint in outPoints)
{ {
ctx.PayjoinLocks.Remove(new PayjoinLock() ctx.PayjoinLocks.Remove(new PayjoinLock() {Id = outPoint.ToString()});
{
Id = outPoint.ToString()
});
} }
try try
{ {
return await ctx.SaveChangesAsync() == outPoints.Length; return await ctx.SaveChangesAsync() == outPoints.Length;
@ -63,6 +60,7 @@ namespace BTCPayServer.Payments.PayJoin
Id = "K-" + outPoint.ToString() Id = "K-" + outPoint.ToString()
}); });
} }
try try
{ {
return await ctx.SaveChangesAsync() == outPoints.Length; return await ctx.SaveChangesAsync() == outPoints.Length;
@ -72,5 +70,13 @@ namespace BTCPayServer.Payments.PayJoin
return false; return false;
} }
} }
public async Task<HashSet<OutPoint>> FindLocks(OutPoint[] outpoints)
{
var outPointsStr = outpoints.Select(o => o.ToString());
await using var ctx = _dbContextFactory.CreateContext();
return (await ctx.PayjoinLocks.Where(l => outPointsStr.Contains(l.Id)).ToArrayAsync())
.Select(l => OutPoint.Parse(l.Id)).ToHashSet();
}
} }
} }

View file

@ -14,14 +14,14 @@ namespace BTCPayServer.Payments.PayJoin
{ {
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly ExplorerClient _explorerClient; private readonly ExplorerClient _explorerClient;
private readonly PayJoinRepository _payJoinRepository; private readonly UTXOLocker _utxoLocker;
private readonly BTCPayServer.Logging.Logs BTCPayLogs; private readonly BTCPayServer.Logging.Logs BTCPayLogs;
public PayjoinReceiverContext(InvoiceRepository invoiceRepository, ExplorerClient explorerClient, PayJoinRepository payJoinRepository, BTCPayServer.Logging.Logs logs) public PayjoinReceiverContext(InvoiceRepository invoiceRepository, ExplorerClient explorerClient, UTXOLocker utxoLocker, BTCPayServer.Logging.Logs logs)
{ {
this.BTCPayLogs = logs; this.BTCPayLogs = logs;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_explorerClient = explorerClient; _explorerClient = explorerClient;
_payJoinRepository = payJoinRepository; _utxoLocker = utxoLocker;
} }
public Invoice Invoice { get; set; } public Invoice Invoice { get; set; }
public NBitcoin.Transaction OriginalTransaction { get; set; } public NBitcoin.Transaction OriginalTransaction { get; set; }
@ -40,7 +40,7 @@ namespace BTCPayServer.Payments.PayJoin
} }
if (!success && LockedUTXOs != null) if (!success && LockedUTXOs != null)
{ {
disposing.Add(_payJoinRepository.TryUnlock(LockedUTXOs)); disposing.Add(_utxoLocker.TryUnlock(LockedUTXOs));
} }
try try
{ {

View file

@ -56,6 +56,7 @@ namespace BTCPayServer.Services.Labels
{ {
Text = uncoloredLabel.Text, Text = uncoloredLabel.Text,
Color = color, Color = color,
Tooltip = "",
TextColor = TextColor(color) TextColor = TextColor(color)
}; };
@ -108,7 +109,10 @@ namespace BTCPayServer.Services.Labels
? null ? null
: _linkGenerator.PayoutLink(payoutLabel.WalletId, null, PayoutState.Completed, request.Scheme, request.Host, : _linkGenerator.PayoutLink(payoutLabel.WalletId, null, PayoutState.Completed, request.Scheme, request.Host,
request.PathBase); request.PathBase);
}
else if (uncoloredLabel.Text == "payjoin")
{
coloredLabel.Tooltip = $"This UTXO was part of a PayJoin transaction.";
} }
return coloredLabel; return coloredLabel;
} }