diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs index ef3ae94..21ac6fc 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs @@ -71,17 +71,43 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector _wallet.BatchPayments ? await _wallet.DestinationProvider.GetPendingPaymentsAsync(utxoSelectionParameters) : Array.Empty(); - var minCoins = new Dictionary(); + + var maxPerType = new Dictionary(); + + 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) { - minCoins.Add(AnonsetType.Red, 1); + maxPerType.TryAdd(AnonsetType.Red, 1); } var solution = SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments, Random.Shared.Next(10, 31), - minCoins, + maxPerType, new Dictionary() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}}, _wallet.ConsolidationMode, liquidityClue, secureRandom); + + if (attemptingTobeParanoid && !solution.HandledPayments.Any()) + { + attemptingTobeParanoid = false; + payments = Array.Empty(); + 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()); return solution.Coins.ToImmutableList(); } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj index 58e1058..5d7d988 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj @@ -13,7 +13,7 @@ Wabisabi Coinjoin Allows you to integrate your btcpayserver store with coinjoins. - 1.0.63 + 1.0.64 diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs index b2eced9..b7e299a 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs @@ -15,6 +15,7 @@ using BTCPayServer.Payments.PayJoin; using BTCPayServer.Services; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; +using LinqKit; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NBitcoin; @@ -92,7 +93,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider public string StoreId { get; set; } public string WalletName => StoreId; - public bool IsUnderPlebStop => false; + public bool IsUnderPlebStop => !WabisabiStoreSettings.Active; bool IWallet.IsMixable(string coordinator) { @@ -114,7 +115,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider public async Task IsWalletPrivateAsync() { - return !BatchPayments && await GetPrivacyPercentageAsync()>= 1; + return !BatchPayments && await GetPrivacyPercentageAsync() >= 1 && (WabisabiStoreSettings.PlebMode || + string.IsNullOrEmpty(WabisabiStoreSettings + .MixToOtherWallet)); } public async Task GetPrivacyPercentageAsync() @@ -127,11 +130,18 @@ public class BTCPayWallet : IWallet, IDestinationProvider await _savingProgress; 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); var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.OutPoint == pair.Key)) .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); } @@ -164,6 +174,25 @@ public class BTCPayWallet : IWallet, IDestinationProvider var coordSettings = WabisabiStoreSettings.Settings.Find(settings => settings.Coordinator == coordinatorName && settings.Enabled); 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) { try @@ -183,20 +212,15 @@ public class BTCPayWallet : IWallet, IDestinationProvider { try { + await _savingProgress; - } - catch (Exception e) - { - } - try - { if (IsUnderPlebStop) { return Array.Empty(); } 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.InputLabelsAllowed?.Any() is true) @@ -239,7 +263,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider foreach (SmartCoin c in resultX) { 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; @@ -251,12 +275,30 @@ public class BTCPayWallet : IWallet, IDestinationProvider } } - public static async Task labels, double anonset, CoinjoinData coinjoinData)>> GetUtxoLabels(WalletRepository walletRepository, string storeId ,ReceivedCoin[] utxos) + + + public static async Task labels, double anonset, CoinjoinData coinjoinData)>> GetUtxoLabels(IMemoryCache memoryCache, WalletRepository walletRepository, string storeId ,ReceivedCoin[] utxos, bool isDepth) { + var utxoToQuery = utxos.ToArray(); + var cacheResult = new Dictionary labels, double anonset, CoinjoinData coinjoinData)>(); + foreach (var utxo in utxoToQuery) + { + + if (memoryCache.TryGetValue<(HashSet 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"), - utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray()); + utxoToQuery.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray()); - var utxoLabels = utxos.Select(coin => + var utxoLabels = utxoToQuery.Select(coin => { walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1); walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2); @@ -268,25 +310,23 @@ public class BTCPayWallet : IWallet, IDestinationProvider } return (coin.OutPoint, info); - }).Where(tuple => tuple.info is not null) - .ToDictionary(tuple => tuple.OutPoint, tuple => tuple.info); - return utxoLabels.ToDictionary(pair => pair.Key, pair => + }).Where(tuple => tuple.info is not null).DistinctBy(tuple => tuple.OutPoint) + .ToDictionary(tuple => tuple.OutPoint, pair => { - var labels = new HashSet(); - 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 ?.ToObject(); - var explicitAnonset = pair.Value.Attachments.FirstOrDefault(attachment => attachment.Type == "anonset") + var explicitAnonset = pair.info.Attachments.FirstOrDefault(attachment => attachment.Type == "anonset") ?.Id; double anonset = 1; if (!string.IsNullOrEmpty(explicitAnonset)) @@ -294,7 +334,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider anonset = double.Parse(explicitAnonset); }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) { anonset = utxo.AnonymitySet; @@ -305,6 +345,11 @@ public class BTCPayWallet : IWallet, IDestinationProvider 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.SpenderTransaction = smartTx; - smartTx.TryAddWalletInput(coin); + smartTx.TryAddWalletInput(SmartCoin.Clone(coin)); }); result.Outputs.ForEach(s => { @@ -422,7 +467,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider Round = result.RoundId.ToString(), CoordinatorName = coordinatorName, Transaction = txHash.ToString(), - CoinsIn = smartTx.WalletInputs.Select(coin => new CoinjoinData.CoinjoinDataCoin() + CoinsIn = result.Coins.Select(coin => new CoinjoinData.CoinjoinDataCoin() { AnonymitySet = coin.AnonymitySet, PayoutId = null, @@ -503,6 +548,12 @@ public class BTCPayWallet : IWallet, IDestinationProvider } _smartifier.SmartTransactions.AddOrReplace(txHash, Task.FromResult(smartTx)); + smartTx.WalletOutputs.ForEach(coin => + { + + _smartifier.Coins.AddOrReplace(coin.Outpoint, Task.FromResult(coin)); + }); + // // var kp = await ExplorerClient.GetMetadataAsync(DerivationScheme, // WellknownMetadataKeys.AccountKeyPath); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs index 8fa4a15..08f7c91 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs @@ -17,7 +17,7 @@ public static class CoordinatorExtensions services.AddTransient(provider => { var s = provider.GetRequiredService(); - if (!s.Started) + if (!s.Started ) { return null; } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Extensions.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Extensions.cs index 7860d4f..301d0d2 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Extensions.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Extensions.cs @@ -3,11 +3,14 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Newtonsoft.Json; +using WalletWasabi.Blockchain.TransactionOutputs; namespace BTCPayServer.Plugins.Wabisabi; public static class Extensions { + public static string ToSentenceCase(this string str) { return Regex.Replace(str, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1])); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs index 83ef360..0a851d1 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs @@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Payments.PayJoin; using BTCPayServer.Services; using BTCPayServer.Services.Wallets; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; @@ -24,6 +25,7 @@ namespace BTCPayServer.Plugins.Wabisabi; public class Smartifier { + private readonly IMemoryCache _memoryCache; private readonly ILogger _logger; private readonly WalletRepository _walletRepository; private readonly ExplorerClient _explorerClient; @@ -32,11 +34,13 @@ public class Smartifier private readonly IUTXOLocker _utxoLocker; public Smartifier( + IMemoryCache memoryCache, ILogger logger, WalletRepository walletRepository, ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId, IUTXOLocker utxoLocker, RootedKeyPath accountKeyPath) { + _memoryCache = memoryCache; _logger = logger; _walletRepository = walletRepository; _explorerClient = explorerClient; @@ -44,7 +48,32 @@ public class Smartifier _storeId = storeId; _utxoLocker = utxoLocker; _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.FromResult(transactionInformation))); + } + } + finally + { + + _loadInitialTxs.TrySetResult(); + } + + + } + + private TaskCompletionSource _loadInitialTxs = new(); + public readonly ConcurrentDictionary>> TransactionInformations = new(); public readonly ConcurrentDictionary> SmartTransactions = new(); public readonly ConcurrentDictionary> Coins = new(); @@ -95,6 +124,8 @@ public class Smartifier public async Task LoadCoins(List coins, int current , Dictionary labels, double anonset, BTCPayWallet.CoinjoinData coinjoinData)> utxoLabels) { + + await _loadInitialTxs.Task; coins = coins.Where(data => data is not null).ToList(); if (current > 3) { @@ -168,7 +199,7 @@ public class Smartifier }; }).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) { if (!ourSpentUtxos.TryGetValue(input, out var outputtxin)) diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml index 52fcfff..82c6f61 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml @@ -2,6 +2,7 @@ @using BTCPayServer.Common @using BTCPayServer.Plugins.Wabisabi @using NBitcoin +@using Org.BouncyCastle.Asn1.Ocsp @using WalletWasabi.Extensions @using WalletWasabi.WabiSabi.Backend.Rounds @using WalletWasabi.WabiSabi.Client @@ -100,7 +101,7 @@ function getColor(isPrivate, score, 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) { @@ -122,8 +123,8 @@ function prepareDatasets(data) { id: "inprogresscoins", label: "In Progress Coins", data: inProgressCoins.map(coin => coin.value), - backgroundColor: inProgressCoins.map(() => "rgba(81, 177, 62,1)"), - alternativeBackgroundColor: inProgressCoins.map(() => "rgba(30, 122, 68,1)"), + backgroundColor: inProgressCoins.map(() => "rgb(56, 151, 37)"), + alternativeBackgroundColor: inProgressCoins.map(() => "rgb(28, 113, 11)"), borderColor: "transparent", borderWidth: 3, coins: inProgressCoins @@ -286,16 +287,21 @@ updateInProgressAnimation(myChart); }

Coinjoin stats

- - Configure coinjoin settings - - +
@if (coins.Any()) { -
+
} @*
*@ @@ -508,10 +514,15 @@ updateInProgressAnimation(myChart); @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)"; + } Your inputs - 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) + @statement @if (tracker.BannedCoins.Any()) { but got @tracker.BannedCoins.Count() inputs (@tracker.BannedCoins.Sum(coin => coin.Coin.Amount.ToDecimal(MoneyUnit.BTC)) BTC) banned @@ -528,9 +539,11 @@ updateInProgressAnimation(myChart); 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": "")}"; + Your outputs - @outputs.outputTxOuts.Count() outputs (@outputs.outputTxOuts.Sum(coin => coin.Value.ToDecimal(MoneyUnit.BTC)) BTC, @outputs.batchedPayments.Count() batched payments) + @statement } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiCoordinatorConfig/UpdateWabisabiSettings.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiCoordinatorConfig/UpdateWabisabiSettings.cshtml index 2203d5d..430b69d 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiCoordinatorConfig/UpdateWabisabiSettings.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiCoordinatorConfig/UpdateWabisabiSettings.cshtml @@ -35,7 +35,7 @@ @if (ViewData.ModelState.TryGetValue("config", out var error) && error.Errors.Any()) { - @string.Join("\n", error.Errors) + @string.Join("\n", error.Errors.Select(modelError => modelError.ErrorMessage)) }