Improve everything

This commit is contained in:
Kukks
2023-11-15 15:24:21 +01:00
parent 206875b133
commit 83807d18b8
16 changed files with 233 additions and 95 deletions

View File

@@ -71,17 +71,43 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
_wallet.BatchPayments _wallet.BatchPayments
? await _wallet.DestinationProvider.GetPendingPaymentsAsync(utxoSelectionParameters) ? await _wallet.DestinationProvider.GetPendingPaymentsAsync(utxoSelectionParameters)
: Array.Empty<PendingPayment>(); : Array.Empty<PendingPayment>();
var minCoins = new Dictionary<AnonsetType, int>();
var maxPerType = new Dictionary<AnonsetType, int>();
var attemptingTobeParanoid = payments.Any() && _wallet.WabisabiStoreSettings.ParanoidPayments;
var attemptingToMixToOtherWallet = string.IsNullOrEmpty(_wallet.WabisabiStoreSettings.MixToOtherWallet);
selectCoins:
maxPerType.Clear();
if (attemptingTobeParanoid || attemptingToMixToOtherWallet)
{
maxPerType.Add(AnonsetType.Red,0);
maxPerType.Add(AnonsetType.Orange,0);
}
if (_wallet.RedCoinIsolation) if (_wallet.RedCoinIsolation)
{ {
minCoins.Add(AnonsetType.Red, 1); maxPerType.TryAdd(AnonsetType.Red, 1);
} }
var solution = SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments, var solution = SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments,
Random.Shared.Next(10, 31), Random.Shared.Next(10, 31),
minCoins, maxPerType,
new Dictionary<AnonsetType, int>() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}}, new Dictionary<AnonsetType, int>() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}},
_wallet.ConsolidationMode, liquidityClue, secureRandom); _wallet.ConsolidationMode, liquidityClue, secureRandom);
if (attemptingTobeParanoid && !solution.HandledPayments.Any())
{
attemptingTobeParanoid = false;
payments = Array.Empty<PendingPayment>();
goto selectCoins;
}
if (attemptingToMixToOtherWallet && !solution.Coins.Any())
{
// check that we have enough coins to mix to other wallet
attemptingToMixToOtherWallet = false;
goto selectCoins;
}
_logger.LogTrace(solution.ToString()); _logger.LogTrace(solution.ToString());
return solution.Coins.ToImmutableList(); return solution.Coins.ToImmutableList();
} }

View File

@@ -13,7 +13,7 @@
<PropertyGroup> <PropertyGroup>
<Product>Wabisabi Coinjoin</Product> <Product>Wabisabi Coinjoin</Product>
<Description>Allows you to integrate your btcpayserver store with coinjoins.</Description> <Description>Allows you to integrate your btcpayserver store with coinjoins.</Description>
<Version>1.0.63</Version> <Version>1.0.64</Version>
</PropertyGroup> </PropertyGroup>
<!-- Plugin development properties --> <!-- Plugin development properties -->

View File

@@ -15,6 +15,7 @@ using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using LinqKit;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
@@ -92,7 +93,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public string StoreId { get; set; } public string StoreId { get; set; }
public string WalletName => StoreId; public string WalletName => StoreId;
public bool IsUnderPlebStop => false; public bool IsUnderPlebStop => !WabisabiStoreSettings.Active;
bool IWallet.IsMixable(string coordinator) bool IWallet.IsMixable(string coordinator)
{ {
@@ -114,7 +115,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public async Task<bool> IsWalletPrivateAsync() public async Task<bool> IsWalletPrivateAsync()
{ {
return !BatchPayments && await GetPrivacyPercentageAsync()>= 1; return !BatchPayments && await GetPrivacyPercentageAsync() >= 1 && (WabisabiStoreSettings.PlebMode ||
string.IsNullOrEmpty(WabisabiStoreSettings
.MixToOtherWallet));
} }
public async Task<double> GetPrivacyPercentageAsync() public async Task<double> GetPrivacyPercentageAsync()
@@ -127,11 +130,18 @@ public class BTCPayWallet : IWallet, IDestinationProvider
await _savingProgress; await _savingProgress;
var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme); var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme);
var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos); var utxoLabels = await GetUtxoLabels(_memoryCache ,_walletRepository, StoreId,utxos, false);
await _smartifier.LoadCoins(utxos.ToList(), 1, utxoLabels); await _smartifier.LoadCoins(utxos.ToList(), 1, utxoLabels);
var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.OutPoint == pair.Key)) var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.OutPoint == pair.Key))
.Select(pair => pair.Value)); .Select(pair => pair.Value));
foreach (var c in coins)
{
var utxo = utxos.Single(coin => coin.OutPoint == c.Outpoint);
c.Height = utxo.Confirmations > 0 ? new Height((uint) utxo.Confirmations) : Height.Mempool;
}
return new CoinsView(coins); return new CoinsView(coins);
} }
@@ -164,6 +174,25 @@ public class BTCPayWallet : IWallet, IDestinationProvider
var coordSettings = WabisabiStoreSettings.Settings.Find(settings => settings.Coordinator == coordinatorName && settings.Enabled); var coordSettings = WabisabiStoreSettings.Settings.Find(settings => settings.Coordinator == coordinatorName && settings.Enabled);
return coordSettings is not null && IsRoundOk(roundParameters, coordSettings); return coordSettings is not null && IsRoundOk(roundParameters, coordSettings);
} }
public async Task CompletedCoinjoin(CoinJoinTracker finishedCoinJoin)
{
try
{
var successfulCoinJoinResult = (await finishedCoinJoin.CoinJoinTask) as SuccessfulCoinJoinResult;
await RegisterCoinjoinTransaction(successfulCoinJoinResult,
finishedCoinJoin.CoinJoinClient.CoordinatorName);
}
catch (Exception e)
{
}
}
public static bool IsRoundOk(RoundParameters roundParameters, WabisabiStoreCoordinatorSettings coordSettings) public static bool IsRoundOk(RoundParameters roundParameters, WabisabiStoreCoordinatorSettings coordSettings)
{ {
try try
@@ -183,20 +212,15 @@ public class BTCPayWallet : IWallet, IDestinationProvider
{ {
try try
{ {
await _savingProgress; await _savingProgress;
}
catch (Exception e)
{
}
try
{
if (IsUnderPlebStop) if (IsUnderPlebStop)
{ {
return Array.Empty<SmartCoin>(); return Array.Empty<SmartCoin>();
} }
var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme, true, CancellationToken.None); var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme, true, CancellationToken.None);
var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos); var utxoLabels = await GetUtxoLabels(_memoryCache, _walletRepository, StoreId,utxos, false);
if (!WabisabiStoreSettings.PlebMode) if (!WabisabiStoreSettings.PlebMode)
{ {
if (WabisabiStoreSettings.InputLabelsAllowed?.Any() is true) if (WabisabiStoreSettings.InputLabelsAllowed?.Any() is true)
@@ -239,7 +263,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
foreach (SmartCoin c in resultX) foreach (SmartCoin c in resultX)
{ {
var utxo = utxos.Single(coin => coin.OutPoint == c.Outpoint); var utxo = utxos.Single(coin => coin.OutPoint == c.Outpoint);
c.Height = new Height((uint) utxo.Confirmations); c.Height = utxo.Confirmations > 0 ? new Height((uint) utxo.Confirmations) : Height.Mempool;
} }
return resultX; return resultX;
@@ -251,12 +275,30 @@ public class BTCPayWallet : IWallet, IDestinationProvider
} }
} }
public static async Task<Dictionary<OutPoint, (HashSet<string> labels, double anonset, CoinjoinData coinjoinData)>> GetUtxoLabels(WalletRepository walletRepository, string storeId ,ReceivedCoin[] utxos)
{
var walletTransactionsInfoAsync = await walletRepository.GetWalletTransactionsInfo(new WalletId(storeId, "BTC"),
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
var utxoLabels = utxos.Select(coin =>
public static async Task<Dictionary<OutPoint, (HashSet<string> labels, double anonset, CoinjoinData coinjoinData)>> GetUtxoLabels(IMemoryCache memoryCache, WalletRepository walletRepository, string storeId ,ReceivedCoin[] utxos, bool isDepth)
{
var utxoToQuery = utxos.ToArray();
var cacheResult = new Dictionary<OutPoint, (HashSet<string> labels, double anonset, CoinjoinData coinjoinData)>();
foreach (var utxo in utxoToQuery)
{
if (memoryCache.TryGetValue<(HashSet<string> labels, double anonset, CoinjoinData coinjoinData)>(
$"wabisabi_{utxo.OutPoint}_utxo", out var cacheVariant ) )
{
if (!cacheResult.TryAdd(utxo.OutPoint, cacheVariant))
{
//wtf!
}
}
}
utxoToQuery = utxoToQuery.Where(utxo => !cacheResult.ContainsKey(utxo.OutPoint)).ToArray();
var walletTransactionsInfoAsync = await walletRepository.GetWalletTransactionsInfo(new WalletId(storeId, "BTC"),
utxoToQuery.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
var utxoLabels = utxoToQuery.Select(coin =>
{ {
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1); walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2); walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2);
@@ -268,25 +310,23 @@ public class BTCPayWallet : IWallet, IDestinationProvider
} }
return (coin.OutPoint, info); return (coin.OutPoint, info);
}).Where(tuple => tuple.info is not null) }).Where(tuple => tuple.info is not null).DistinctBy(tuple => tuple.OutPoint)
.ToDictionary(tuple => tuple.OutPoint, tuple => tuple.info); .ToDictionary(tuple => tuple.OutPoint, pair =>
return utxoLabels.ToDictionary(pair => pair.Key, pair =>
{ {
var labels = new HashSet<string>(); var labels = new HashSet<string>();
if (pair.Value.LabelColors.Any()) if (pair.info.LabelColors.Any())
{ {
labels.AddRange((pair.Value.LabelColors.Select(pair => pair.Key))); labels.AddRange((pair.info.LabelColors.Select(pair => pair.Key)));
} }
if (pair.Value.Attachments.Any() is true) if (pair.info.Attachments.Any() is true)
{ {
labels.AddRange((pair.Value.Attachments.Select(attachment => attachment.Id))); labels.AddRange((pair.info.Attachments.Select(attachment => attachment.Id)));
} }
var cjData = pair.Value.Attachments var cjData = pair.info.Attachments
.FirstOrDefault(attachment => attachment.Type == "coinjoin")?.Data .FirstOrDefault(attachment => attachment.Type == "coinjoin")?.Data
?.ToObject<CoinjoinData>(); ?.ToObject<CoinjoinData>();
var explicitAnonset = pair.Value.Attachments.FirstOrDefault(attachment => attachment.Type == "anonset") var explicitAnonset = pair.info.Attachments.FirstOrDefault(attachment => attachment.Type == "anonset")
?.Id; ?.Id;
double anonset = 1; double anonset = 1;
if (!string.IsNullOrEmpty(explicitAnonset)) if (!string.IsNullOrEmpty(explicitAnonset))
@@ -294,7 +334,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
anonset = double.Parse(explicitAnonset); anonset = double.Parse(explicitAnonset);
}else if (cjData is not null) }else if (cjData is not null)
{ {
var utxo = cjData.CoinsOut.FirstOrDefault(dataCoin => dataCoin.Outpoint == pair.Key.ToString()); var utxo = cjData.CoinsOut.FirstOrDefault(dataCoin => dataCoin.Outpoint == pair.OutPoint.ToString());
if (utxo is not null) if (utxo is not null)
{ {
anonset = utxo.AnonymitySet; anonset = utxo.AnonymitySet;
@@ -305,6 +345,11 @@ public class BTCPayWallet : IWallet, IDestinationProvider
return (labels, anonset, cjData); return (labels, anonset, cjData);
}); });
foreach (var pair in utxoLabels)
{
memoryCache.Set($"wabisabi_{pair.Key.Hash}_utxo", pair.Value, isDepth? TimeSpan.FromMinutes(10): TimeSpan.FromMinutes(5));
}
return utxoLabels.Concat(cacheResult).ToDictionary(pair => pair.Key, pair => pair.Value);
} }
@@ -389,7 +434,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
{ {
coin.HdPubKey.SetKeyState(KeyState.Used); coin.HdPubKey.SetKeyState(KeyState.Used);
coin.SpenderTransaction = smartTx; coin.SpenderTransaction = smartTx;
smartTx.TryAddWalletInput(coin); smartTx.TryAddWalletInput(SmartCoin.Clone(coin));
}); });
result.Outputs.ForEach(s => result.Outputs.ForEach(s =>
{ {
@@ -422,7 +467,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
Round = result.RoundId.ToString(), Round = result.RoundId.ToString(),
CoordinatorName = coordinatorName, CoordinatorName = coordinatorName,
Transaction = txHash.ToString(), Transaction = txHash.ToString(),
CoinsIn = smartTx.WalletInputs.Select(coin => new CoinjoinData.CoinjoinDataCoin() CoinsIn = result.Coins.Select(coin => new CoinjoinData.CoinjoinDataCoin()
{ {
AnonymitySet = coin.AnonymitySet, AnonymitySet = coin.AnonymitySet,
PayoutId = null, PayoutId = null,
@@ -503,6 +548,12 @@ public class BTCPayWallet : IWallet, IDestinationProvider
} }
_smartifier.SmartTransactions.AddOrReplace(txHash, Task.FromResult(smartTx)); _smartifier.SmartTransactions.AddOrReplace(txHash, Task.FromResult(smartTx));
smartTx.WalletOutputs.ForEach(coin =>
{
_smartifier.Coins.AddOrReplace(coin.Outpoint, Task.FromResult(coin));
});
// //
// var kp = await ExplorerClient.GetMetadataAsync<RootedKeyPath>(DerivationScheme, // var kp = await ExplorerClient.GetMetadataAsync<RootedKeyPath>(DerivationScheme,
// WellknownMetadataKeys.AccountKeyPath); // WellknownMetadataKeys.AccountKeyPath);

View File

@@ -17,7 +17,7 @@ public static class CoordinatorExtensions
services.AddTransient(provider => services.AddTransient(provider =>
{ {
var s = provider.GetRequiredService<WabisabiCoordinatorService>(); var s = provider.GetRequiredService<WabisabiCoordinatorService>();
if (!s.Started) if (!s.Started )
{ {
return null; return null;
} }

View File

@@ -3,11 +3,14 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using WalletWasabi.Blockchain.TransactionOutputs;
namespace BTCPayServer.Plugins.Wabisabi; namespace BTCPayServer.Plugins.Wabisabi;
public static class Extensions public static class Extensions
{ {
public static string ToSentenceCase(this string str) public static string ToSentenceCase(this string str)
{ {
return Regex.Replace(str, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1])); return Regex.Replace(str, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));

View File

@@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBXplorer; using NBXplorer;
@@ -24,6 +25,7 @@ namespace BTCPayServer.Plugins.Wabisabi;
public class Smartifier public class Smartifier
{ {
private readonly IMemoryCache _memoryCache;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly WalletRepository _walletRepository; private readonly WalletRepository _walletRepository;
private readonly ExplorerClient _explorerClient; private readonly ExplorerClient _explorerClient;
@@ -32,11 +34,13 @@ public class Smartifier
private readonly IUTXOLocker _utxoLocker; private readonly IUTXOLocker _utxoLocker;
public Smartifier( public Smartifier(
IMemoryCache memoryCache,
ILogger logger, ILogger logger,
WalletRepository walletRepository, WalletRepository walletRepository,
ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId, ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId,
IUTXOLocker utxoLocker, RootedKeyPath accountKeyPath) IUTXOLocker utxoLocker, RootedKeyPath accountKeyPath)
{ {
_memoryCache = memoryCache;
_logger = logger; _logger = logger;
_walletRepository = walletRepository; _walletRepository = walletRepository;
_explorerClient = explorerClient; _explorerClient = explorerClient;
@@ -44,7 +48,32 @@ public class Smartifier
_storeId = storeId; _storeId = storeId;
_utxoLocker = utxoLocker; _utxoLocker = utxoLocker;
_accountKeyPath = accountKeyPath; _accountKeyPath = accountKeyPath;
_ = LoadInitialTxs();
} }
private async Task LoadInitialTxs()
{
try
{
var txsBulk = await _explorerClient.GetTransactionsAsync(DerivationScheme);
foreach (var transactionInformation in txsBulk.ConfirmedTransactions.Transactions.Concat(txsBulk.UnconfirmedTransactions.Transactions))
{
TransactionInformations.AddOrReplace(transactionInformation.TransactionId,
new Lazy<Task<TransactionInformation>>(() => Task.FromResult(transactionInformation)));
}
}
finally
{
_loadInitialTxs.TrySetResult();
}
}
private TaskCompletionSource _loadInitialTxs = new();
public readonly ConcurrentDictionary<uint256, Lazy<Task<TransactionInformation>>> TransactionInformations = new(); public readonly ConcurrentDictionary<uint256, Lazy<Task<TransactionInformation>>> TransactionInformations = new();
public readonly ConcurrentDictionary<uint256, Task<SmartTransaction>> SmartTransactions = new(); public readonly ConcurrentDictionary<uint256, Task<SmartTransaction>> SmartTransactions = new();
public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new(); public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new();
@@ -95,6 +124,8 @@ public class Smartifier
public async Task LoadCoins(List<ReceivedCoin> coins, int current , public async Task LoadCoins(List<ReceivedCoin> coins, int current ,
Dictionary<OutPoint, (HashSet<string> labels, double anonset, BTCPayWallet.CoinjoinData coinjoinData)> utxoLabels) Dictionary<OutPoint, (HashSet<string> labels, double anonset, BTCPayWallet.CoinjoinData coinjoinData)> utxoLabels)
{ {
await _loadInitialTxs.Task;
coins = coins.Where(data => data is not null).ToList(); coins = coins.Where(data => data is not null).ToList();
if (current > 3) if (current > 3)
{ {
@@ -168,7 +199,7 @@ public class Smartifier
}; };
}).Where(receivedCoin => receivedCoin is not null).ToList(); }).Where(receivedCoin => receivedCoin is not null).ToList();
await LoadCoins(inputsToLoad,current+1, await BTCPayWallet.GetUtxoLabels(_walletRepository, _storeId, inputsToLoad.ToArray())); await LoadCoins(inputsToLoad,current+1, await BTCPayWallet.GetUtxoLabels( _memoryCache ,_walletRepository, _storeId, inputsToLoad.ToArray(), true ));
foreach (MatchedOutput input in unsmartTx.Inputs) foreach (MatchedOutput input in unsmartTx.Inputs)
{ {
if (!ourSpentUtxos.TryGetValue(input, out var outputtxin)) if (!ourSpentUtxos.TryGetValue(input, out var outputtxin))

View File

@@ -2,6 +2,7 @@
@using BTCPayServer.Common @using BTCPayServer.Common
@using BTCPayServer.Plugins.Wabisabi @using BTCPayServer.Plugins.Wabisabi
@using NBitcoin @using NBitcoin
@using Org.BouncyCastle.Asn1.Ocsp
@using WalletWasabi.Extensions @using WalletWasabi.Extensions
@using WalletWasabi.WabiSabi.Backend.Rounds @using WalletWasabi.WabiSabi.Backend.Rounds
@using WalletWasabi.WabiSabi.Client @using WalletWasabi.WabiSabi.Client
@@ -100,7 +101,7 @@
function getColor(isPrivate, score, maxScore) { function getColor(isPrivate, score, maxScore) {
let normalizedScore = Math.min(Math.max(score, 0), maxScore) / maxScore; let normalizedScore = Math.min(Math.max(score, 0), maxScore) / maxScore;
return isPrivate ? `rgb(0, ${Math.floor(255 * normalizedScore)}, 0)` : `rgb(255, ${Math.floor(128 * normalizedScore)}, 0)`; return isPrivate ? `rgb(81, 177, 62)` : `rgb(255, ${Math.floor(128 * normalizedScore)}, 0)`;
} }
function prepareDatasets(data) { function prepareDatasets(data) {
@@ -122,8 +123,8 @@ function prepareDatasets(data) {
id: "inprogresscoins", id: "inprogresscoins",
label: "In Progress Coins", label: "In Progress Coins",
data: inProgressCoins.map(coin => coin.value), data: inProgressCoins.map(coin => coin.value),
backgroundColor: inProgressCoins.map(() => "rgba(81, 177, 62,1)"), backgroundColor: inProgressCoins.map(() => "rgb(56, 151, 37)"),
alternativeBackgroundColor: inProgressCoins.map(() => "rgba(30, 122, 68,1)"), alternativeBackgroundColor: inProgressCoins.map(() => "rgb(28, 113, 11)"),
borderColor: "transparent", borderColor: "transparent",
borderWidth: 3, borderWidth: 3,
coins: inProgressCoins coins: inProgressCoins
@@ -286,16 +287,21 @@ updateInProgressAnimation(myChart);
} }
<header> <header>
<h4>Coinjoin stats</h4> <h4>Coinjoin stats</h4>
<a asp-controller="WabisabiStore" asp-action="UpdateWabisabiStoreSettings" asp-route-storeId="@storeId" class="fw-semibold"> <div class="d-flex gap-1">
Configure coinjoin settings <a asp-controller="WabisabiStore" asp-action="ToggleActive" asp-route-storeId="@storeId" asp-route-returnUrl="@Context.Request.GetCurrentUrl()" class="fw-semibold">
</a> @(settings.Active ? "Deactivate" : "Activate")
</a>
-
<a asp-controller="WabisabiStore" asp-action="UpdateWabisabiStoreSettings" asp-route-storeId="@storeId" class="fw-semibold">
Configure
</a>
</div>
</header> </header>
<div class="w-100"> <div class="w-100">
@if (coins.Any()) @if (coins.Any())
{ {
<div class="d-flex justify-content-center" style="max-height: 400px; "> <canvas id="cjchart" class="mb-4"></canvas></div> <div class="d-flex justify-content-center mb-4" style="max-height: 400px; "> <canvas id="cjchart"></canvas></div>
} }
@* <div> *@ @* <div> *@
@@ -508,10 +514,15 @@ updateInProgressAnimation(myChart);
</tr> </tr>
@if (!tracker.CoinJoinClient.CoinsToRegister.IsEmpty) @if (!tracker.CoinJoinClient.CoinsToRegister.IsEmpty)
{ {
var statement = $"Registered {tracker.CoinJoinClient.CoinsInCriticalPhase.Count()} inputs ({tracker.CoinJoinClient.CoinsInCriticalPhase.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC))} BTC)";
if (tracker.CoinJoinClient.CoinsInCriticalPhase.Count() != tracker.CoinJoinClient.CoinsToRegister.Count())
{
statement += $" / {tracker.CoinJoinClient.CoinsToRegister.Count()} inputs ({tracker.CoinJoinClient.CoinsToRegister.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC))} BTC)";
}
<tr> <tr>
<th scope="">Your inputs</th> <th scope="">Your inputs</th>
<td class=""> <td class="">
<span class="w-100">Registered @tracker.CoinJoinClient.CoinsInCriticalPhase.Count() inputs (@tracker.CoinJoinClient.CoinsInCriticalPhase.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) / @tracker.CoinJoinClient.CoinsToRegister.Count() inputs (@tracker.CoinJoinClient.CoinsToRegister.Sum(coin => coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) </span> <span class="w-100">@statement</span>
@if (tracker.BannedCoins.Any()) @if (tracker.BannedCoins.Any())
{ {
<span class="w-100 text-danger">but got @tracker.BannedCoins.Count() inputs (@tracker.BannedCoins.Sum(coin => coin.Coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) banned</span> <span class="w-100 text-danger">but got @tracker.BannedCoins.Count() inputs (@tracker.BannedCoins.Sum(coin => coin.Coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) banned</span>
@@ -528,9 +539,11 @@ updateInProgressAnimation(myChart);
</tr> </tr>
if (tracker.CoinJoinClient.OutputTxOuts is { } outputs) if (tracker.CoinJoinClient.OutputTxOuts is { } outputs)
{ {
var statement = $"{outputs.outputTxOuts.Count()} outputs ({outputs.outputTxOuts.Sum(coin => coin.Value.ToDecimal(MoneyUnit.BTC))} BTC {(outputs.batchedPayments.Any()? $"{outputs.batchedPayments.Count()} batched payments": "")}";
<tr> <tr>
<th scope="">Your outputs</th> <th scope="">Your outputs</th>
<td >@outputs.outputTxOuts.Count() outputs (@outputs.outputTxOuts.Sum(coin => coin.Value.ToDecimal(MoneyUnit.BTC)) BTC, @outputs.batchedPayments.Count() batched payments)</td> <td >@statement</td>
</tr> </tr>
} }
} }

View File

@@ -35,7 +35,7 @@
@if (ViewData.ModelState.TryGetValue("config", out var error) && error.Errors.Any()) @if (ViewData.ModelState.TryGetValue("config", out var error) && error.Errors.Any())
{ {
<span class="text-danger">@string.Join("\n", error.Errors)</span> <span class="text-danger">@string.Join("\n", error.Errors.Select(modelError => modelError.ErrorMessage))</span>
} }
<textarea rows="10" cols="40" class="form-control" id="config" name="config" > <textarea rows="10" cols="40" class="form-control" id="config" name="config" >
@Html.Raw(ViewBag.Config) @Html.Raw(ViewBag.Config)

View File

@@ -44,19 +44,22 @@
<vc:icon symbol="info"/> <vc:icon symbol="info"/>
</a> </a>
</h3> </h3>
<div> <div class="d-flex align-items-center gap-1 ">
<div class="d-flex align-items-center">
<input asp-for="Active" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="Active" class="form-label mb-0 me-1"></label>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary mt-3 mt-sm-0">Save</button> <button name="command" type="submit" value="save" class="btn btn-primary mt-3 mt-sm-0">Save</button>
<a asp-action="ListCoinjoins" asp-route-storeId="@storeId" class="btn btn-secondary mt-3 mt-sm-0" role="button"> <a asp-action="ListCoinjoins" asp-route-storeId="@storeId" class="btn btn-secondary mt-3 mt-sm-0" role="button">
Coinjoin History Coinjoin History
</a> </a>
<button type="button" class="btn btn-secondary mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings" <button type="button" class="btn btn-secondary mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings"
data-bs-toggle="modal" data-bs-target="#discover-prompt"> data-bs-toggle="modal" data-bs-target="#discover-prompt">
Add Coordinator Add Coordinator
</button> </button>
<a asp-controller="WabisabiCoordinatorConfig" asp-action="UpdateWabisabiSettings" class="btn btn-secondary mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings">Coordinator</a> <a asp-controller="WabisabiCoordinatorConfig" asp-action="UpdateWabisabiSettings" class="btn btn-secondary mt-3 mt-sm-0" permission="@Policies.CanModifyServerSettings">Coordinator</a>
@* <a class="btn btn-secondary mt-3 mt-sm-0" href="https://gist.github.com/nopara73/bb17e89d7dc9af536ca41f50f705d329" rel="noreferrer noopener" target="_blank">Enable Discreet payments - Coming soon</a> *@
@* <a class="btn btn-secondary mt-3 mt-sm-0" href="https://gist.github.com/nopara73/bb17e89d7dc9af536ca41f50f705d329" rel="noreferrer noopener" target="_blank">Enable Discreet payments - Coming soon</a> *@
</div> </div>
</div> </div>
@{ @{
@@ -80,9 +83,26 @@
} }
} }
} }
<style>
#blocker:hover{
background-color: rgba(128,128,128, 0.5);
}
#blocker:hover h4{
display: block !important;
left: 0;
position: fixed;
width: 100%
}
</style>
<div class="@(anyEnabled ? "" : "d-none") card card-body coordinator-settings"> <div class="@(anyEnabled ? "" : "d-none") card card-body coordinator-settings">
@if (Model.Active && anyEnabled)
{
<div class="position-absolute w-100 h-100 text-center rounded" id="blocker" style=" left: 0; top: 0; z-index: 1">
<h4 class="d-none pt-4">Settings cannot be changed while active</h4>
</div>
}
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-6"> <div class="col-sm-12 col-md-6">
<div class="form-check"> <div class="form-check">
@@ -140,6 +160,11 @@
<input asp-for="BatchPayments" type="checkbox" class="form-check-input"/> <input asp-for="BatchPayments" type="checkbox" class="form-check-input"/>
<p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p> <p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p>
</div> </div>
<div class="form-group form-check">
<label asp-for="ParanoidPayments" class="form-check-label">Paranoid payments</label>
<input asp-for="ParanoidPayments" type="checkbox" class="form-check-input"/>
<p class="text-muted">Only batch payments with fully private coins.</p>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="CrossMixBetweenCoordinatorsMode" class="form-label">Mix funds between different coordinators</label> <label asp-for="CrossMixBetweenCoordinatorsMode" class="form-label">Mix funds between different coordinators</label>
<select asp-for="CrossMixBetweenCoordinatorsMode" class="form-select"> <select asp-for="CrossMixBetweenCoordinatorsMode" class="form-select">

View File

@@ -324,32 +324,6 @@ public class WabisabiCoordinatorClientInstance:IHostedService
_logger.LogTrace(coinJoinStatusEventArgs.CoinJoinProgressEventArgs.GetType() + " :" + _logger.LogTrace(coinJoinStatusEventArgs.CoinJoinProgressEventArgs.GetType() + " :" +
e.Wallet.WalletName); e.Wallet.WalletName);
break; break;
case CompletedEventArgs completedEventArgs:
var result = completedEventArgs.CoinJoinResult;
if (completedEventArgs.CompletionStatus == CompletionStatus.Success && result is SuccessfulCoinJoinResult successfulCoinJoinResult)
{
Task.Run(async () =>
{
var wallet = (BTCPayWallet) e.Wallet;
await wallet.RegisterCoinjoinTransaction(successfulCoinJoinResult, CoordinatorName);
});
}
else if(result is DisruptedCoinJoinResult disruptedCoinJoinResult )
{
Task.Run(async () =>
{
// _logger.LogInformation("unlocking coins because round failed");
await _utxoLocker.TryUnlock(
disruptedCoinJoinResult.SignedCoins.Select(coin => coin.Outpoint).ToArray());
});
break;
}
_logger.LogTrace("Coinjoin complete! :" + e.Wallet.WalletName);
break;
case LoadedEventArgs loadedEventArgs: case LoadedEventArgs loadedEventArgs:
stopWhenAllMixed = !((BTCPayWallet)loadedEventArgs.Wallet).BatchPayments; stopWhenAllMixed = !((BTCPayWallet)loadedEventArgs.Wallet).BatchPayments;
_ = CoinJoinManager.StartAsync(loadedEventArgs.Wallet, stopWhenAllMixed, false, CancellationToken.None); _ = CoinJoinManager.StartAsync(loadedEventArgs.Wallet, stopWhenAllMixed, false, CancellationToken.None);

View File

@@ -7,6 +7,7 @@ using BTCPayServer.Abstractions.Services;
using BTCPayServer.Common; using BTCPayServer.Common;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
@@ -60,7 +61,8 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin
utxoLocker, utxoLocker,
provider.GetRequiredService<EventAggregator>(), provider.GetRequiredService<EventAggregator>(),
provider.GetRequiredService<ILogger<WalletProvider>>(), provider.GetRequiredService<ILogger<WalletProvider>>(),
provider.GetRequiredService<BTCPayNetworkProvider>() provider.GetRequiredService<BTCPayNetworkProvider>(),
provider.GetRequiredService<IMemoryCache>()
)); ));
applicationBuilder.AddWabisabiCoordinator(); applicationBuilder.AddWabisabiCoordinator();
applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>()); applicationBuilder.AddSingleton<IWalletProvider>(provider => provider.GetRequiredService<WalletProvider>());

View File

@@ -82,13 +82,6 @@ namespace BTCPayServer.Plugins.Wabisabi
} }
} }
if (wabisabiSettings.Settings.All(settings => !settings.Enabled))
{
await _storeRepository.UpdateSetting<WabisabiStoreSettings>(storeId, nameof(WabisabiStoreSettings), null!);
}
else
{
var res = await GetWabisabiForStore(storeId); var res = await GetWabisabiForStore(storeId);
foreach (var wabisabiStoreCoordinatorSettings in wabisabiSettings.Settings) foreach (var wabisabiStoreCoordinatorSettings in wabisabiSettings.Settings)
{ {
@@ -114,7 +107,7 @@ namespace BTCPayServer.Plugins.Wabisabi
} }
await _storeRepository.UpdateSetting(storeId, nameof(WabisabiStoreSettings), wabisabiSettings!); await _storeRepository.UpdateSetting(storeId, nameof(WabisabiStoreSettings), wabisabiSettings!);
}
_memoryCache.Remove(GetCacheKey(storeId)); _memoryCache.Remove(GetCacheKey(storeId));
await _walletProvider.SettingsUpdated(storeId, wabisabiSettings); await _walletProvider.SettingsUpdated(storeId, wabisabiSettings);
// var existingProcessor = (await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery() // var existingProcessor = (await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery()

View File

@@ -57,6 +57,24 @@ namespace BTCPayServer.Plugins.Wabisabi
_socks5HttpClientHandler = serviceProvider.GetRequiredService<Socks5HttpClientHandler>(); _socks5HttpClientHandler = serviceProvider.GetRequiredService<Socks5HttpClientHandler>();
} }
[HttpGet("toggle-active")]
public async Task<IActionResult> ToggleActive(string storeId, string returnUrl)
{
var settings = await _WabisabiService.GetWabisabiForStore(storeId);
if (settings is null)
{
return NotFound();
}
settings.Active = !settings.Active;
await _WabisabiService.SetWabisabiForStore(storeId, settings);
TempData["SuccessMessage"] = $"Coinjoin is now {(settings.Active ? "active" : "inactive")}";
returnUrl = Url.EnsureLocal(returnUrl, HttpContext.Request);
if(returnUrl is null)
return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId});
return Redirect(returnUrl);
}
[HttpGet("")] [HttpGet("")]
public async Task<IActionResult> UpdateWabisabiStoreSettings(string storeId) public async Task<IActionResult> UpdateWabisabiStoreSettings(string storeId)
{ {

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Client.JsonConverters;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -9,7 +7,7 @@ namespace BTCPayServer.Plugins.Wabisabi;
public class WabisabiStoreSettings public class WabisabiStoreSettings
{ {
public List<WabisabiStoreCoordinatorSettings> Settings { get; set; } = new(); public List<WabisabiStoreCoordinatorSettings> Settings { get; set; } = new();
public bool Active { get; set; } = true;
public string MixToOtherWallet { get; set; } public string MixToOtherWallet { get; set; }
@@ -22,6 +20,7 @@ public class WabisabiStoreSettings
public int AnonymitySetTarget { get; set; } = 5; public int AnonymitySetTarget { get; set; } = 5;
public bool BatchPayments { get; set; } = true; public bool BatchPayments { get; set; } = true;
public bool ParanoidPayments { get; set; } = false;
public int ExtraJoinProbability { get; set; } = 0; public int ExtraJoinProbability { get; set; } = 0;
public CrossMixMode CrossMixBetweenCoordinatorsMode { get; set; } = CrossMixMode.WhenFree; public CrossMixMode CrossMixBetweenCoordinatorsMode { get; set; } = CrossMixMode.WhenFree;
public int FeeRateMedianTimeFrameHours { get; set; } public int FeeRateMedianTimeFrameHours { get; set; }

View File

@@ -37,6 +37,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly ILogger<WalletProvider> _logger; private readonly ILogger<WalletProvider> _logger;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly IMemoryCache _memoryCache;
public WalletProvider( public WalletProvider(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
@@ -46,7 +47,8 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
IUTXOLocker utxoLocker, IUTXOLocker utxoLocker,
EventAggregator eventAggregator, EventAggregator eventAggregator,
ILogger<WalletProvider> logger, ILogger<WalletProvider> logger,
BTCPayNetworkProvider networkProvider) : base(TimeSpan.FromMinutes(5)) BTCPayNetworkProvider networkProvider,
IMemoryCache memoryCache) : base(TimeSpan.FromMinutes(5))
{ {
UtxoLocker = utxoLocker; UtxoLocker = utxoLocker;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
@@ -56,6 +58,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
_networkProvider = networkProvider; _networkProvider = networkProvider;
_memoryCache = memoryCache;
} }
public readonly ConcurrentDictionary<string, Lazy<Task<IWallet>>> LoadedWallets = new(); public readonly ConcurrentDictionary<string, Lazy<Task<IWallet>>> LoadedWallets = new();
@@ -102,7 +105,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
var accountKeyPath2 = await explorerClient.GetMetadataAsync<RootedKeyPath>(derivationStrategy, var accountKeyPath2 = await explorerClient.GetMetadataAsync<RootedKeyPath>(derivationStrategy,
WellknownMetadataKeys.AccountKeyPath); WellknownMetadataKeys.AccountKeyPath);
accountKeyPath = accountKeyPath2 ?? accountKeyPath; accountKeyPath = accountKeyPath2 ?? accountKeyPath;
var smartifier = new Smartifier(_logger,_serviceProvider.GetRequiredService<WalletRepository>(), var smartifier = new Smartifier(_memoryCache,_logger,_serviceProvider.GetRequiredService<WalletRepository>(),
explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath); explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath);
if (masterKey is null || accountKey is null || accountKeyPath is null) if (masterKey is null || accountKey is null || accountKeyPath is null)
{ {
@@ -113,7 +116,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
} }
else else
{ {
var smartifier = new Smartifier(_logger,_serviceProvider.GetRequiredService<WalletRepository>(), explorerClient, var smartifier = new Smartifier(_memoryCache, _logger,_serviceProvider.GetRequiredService<WalletRepository>(), explorerClient,
derivationStrategy, name, UtxoLocker, accountKeyPath); derivationStrategy, name, UtxoLocker, accountKeyPath);
keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, smartifier); keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, smartifier);
} }
@@ -242,7 +245,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
public async Task SettingsUpdated(string storeId, WabisabiStoreSettings wabisabiSettings) public async Task SettingsUpdated(string storeId, WabisabiStoreSettings wabisabiSettings)
{ {
if (wabisabiSettings.Settings.All(settings => !settings.Enabled)) if (wabisabiSettings.Settings.All(settings => !settings.Enabled) || !wabisabiSettings.Active)
{ {
_cachedSettings?.Remove(storeId); _cachedSettings?.Remove(storeId);
await UnloadWallet(storeId); await UnloadWallet(storeId);