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();
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var repo = tester.PayTester.GetService<PayJoinRepository>();
var repo = tester.PayTester.GetService<UTXOLocker>();
var outpoint = RandomOutpoint();
// Should not be locked
@ -166,7 +166,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
@ -633,7 +633,7 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
@ -1155,7 +1155,7 @@ retry:
Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted));
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
Assert.False(await payjoinRepository.TryUnlock(ourOutpoint));
}

View file

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

View file

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

View file

@ -14,8 +14,6 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.PaymentRequests;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.HostedServices
{
@ -48,14 +46,6 @@ namespace BTCPayServer.HostedServices
{
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))
{
labels.Add(UpdateTransactionLabel.PaymentRequestLabelTemplate(paymentId));

View file

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

View file

@ -83,7 +83,7 @@ namespace BTCPayServer.Payments.PayJoin
private readonly InvoiceRepository _invoiceRepository;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly PayJoinRepository _payJoinRepository;
private readonly UTXOLocker _utxoLocker;
private readonly EventAggregator _eventAggregator;
private readonly NBXplorerDashboard _dashboard;
private readonly DelayedTransactionBroadcaster _broadcaster;
@ -97,7 +97,7 @@ namespace BTCPayServer.Payments.PayJoin
public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider,
BTCPayWalletProvider btcPayWalletProvider,
PayJoinRepository payJoinRepository,
UTXOLocker utxoLocker,
EventAggregator eventAggregator,
NBXplorerDashboard dashboard,
DelayedTransactionBroadcaster broadcaster,
@ -111,7 +111,7 @@ namespace BTCPayServer.Payments.PayJoin
_invoiceRepository = invoiceRepository;
_explorerClientProvider = explorerClientProvider;
_btcPayWalletProvider = btcPayWalletProvider;
_payJoinRepository = payJoinRepository;
_utxoLocker = utxoLocker;
_eventAggregator = eventAggregator;
_dashboard = dashboard;
_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)
{
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
ctx.DoNotBroadcast();
@ -502,18 +502,23 @@ namespace BTCPayServer.Payments.PayJoin
_eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
}
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()
{
WalletId = walletId,
TransactionLabels = 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)
TransactionLabels = labels
});
ctx.Success();
// BTCPay Server support PSBT set as hex
@ -608,7 +613,7 @@ namespace BTCPayServer.Payments.PayJoin
{
continue;
}
if (await _payJoinRepository.TryLock(availableUtxo.Outpoint))
if (await _utxoLocker.TryLock(availableUtxo.Outpoint))
{
return (new[] { availableUtxo }, PayjoinUtxoSelectionType.HeuristicBased);
}
@ -620,7 +625,7 @@ namespace BTCPayServer.Payments.PayJoin
{
if (currentTry >= maxTries)
break;
if (await _payJoinRepository.TryLock(utxo.Outpoint))
if (await _utxoLocker.TryLock(utxo.Outpoint))
{
return (new[] { utxo }, PayjoinUtxoSelectionType.Ordered);
}

View file

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

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@ -6,21 +7,19 @@ using NBitcoin;
namespace BTCPayServer.Payments.PayJoin
{
public class PayJoinRepository
public class UTXOLocker : IUTXOLocker
{
private readonly ApplicationDbContextFactory _dbContextFactory;
public PayJoinRepository(ApplicationDbContextFactory dbContextFactory)
public UTXOLocker(ApplicationDbContextFactory dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<bool> TryLock(OutPoint outpoint)
{
using var ctx = _dbContextFactory.CreateContext();
ctx.PayjoinLocks.Add(new PayjoinLock()
{
Id = outpoint.ToString()
});
ctx.PayjoinLocks.Add(new PayjoinLock() {Id = outpoint.ToString()});
try
{
return await ctx.SaveChangesAsync() == 1;
@ -36,11 +35,9 @@ namespace BTCPayServer.Payments.PayJoin
using var ctx = _dbContextFactory.CreateContext();
foreach (OutPoint outPoint in outPoints)
{
ctx.PayjoinLocks.Remove(new PayjoinLock()
{
Id = outPoint.ToString()
});
ctx.PayjoinLocks.Remove(new PayjoinLock() {Id = outPoint.ToString()});
}
try
{
return await ctx.SaveChangesAsync() == outPoints.Length;
@ -63,6 +60,7 @@ namespace BTCPayServer.Payments.PayJoin
Id = "K-" + outPoint.ToString()
});
}
try
{
return await ctx.SaveChangesAsync() == outPoints.Length;
@ -72,5 +70,13 @@ namespace BTCPayServer.Payments.PayJoin
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 ExplorerClient _explorerClient;
private readonly PayJoinRepository _payJoinRepository;
private readonly UTXOLocker _utxoLocker;
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;
_invoiceRepository = invoiceRepository;
_explorerClient = explorerClient;
_payJoinRepository = payJoinRepository;
_utxoLocker = utxoLocker;
}
public Invoice Invoice { get; set; }
public NBitcoin.Transaction OriginalTransaction { get; set; }
@ -40,7 +40,7 @@ namespace BTCPayServer.Payments.PayJoin
}
if (!success && LockedUTXOs != null)
{
disposing.Add(_payJoinRepository.TryUnlock(LockedUTXOs));
disposing.Add(_utxoLocker.TryUnlock(LockedUTXOs));
}
try
{

View file

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