diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs index 87bb076..18c7dc5 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs @@ -11,6 +11,7 @@ using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.Crypto.Randomness; using WalletWasabi.Extensions; using WalletWasabi.Helpers; +using WalletWasabi.WabiSabi; using WalletWasabi.WabiSabi.Backend.Rounds; using WalletWasabi.WabiSabi.Client; using WalletWasabi.Wallets; @@ -20,12 +21,10 @@ namespace BTCPayServer.Plugins.Wabisabi; public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector { private readonly BTCPayWallet _wallet; - private readonly ILogger _logger; - public BTCPayCoinjoinCoinSelector(BTCPayWallet wallet, ILogger logger) + public BTCPayCoinjoinCoinSelector(BTCPayWallet wallet) { _wallet = wallet; - _logger = logger; } public async Task> SelectCoinsAsync(IEnumerable coinCandidates, @@ -98,12 +97,13 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector ConsolidationModeType.WhenLowFeeAndManyUTXO => isLowFee && coinCandidates.Count() > BTCPayWallet.HighAmountOfCoins, _ => throw new ArgumentOutOfRangeException() }; - + Dictionary idealMinimumPerType = new Dictionary() + {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}}; var solution = await SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments, Random.Shared.Next(10, 31), maxPerType, - new Dictionary() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}}, + idealMinimumPerType, consolidationMode, liquidityClue, secureRandom); if (attemptingTobeParanoid && !solution.HandledPayments.Any()) @@ -119,7 +119,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector attemptingToMixToOtherWallet = false; goto selectCoins; } - _logger.LogTrace(solution.ToString()); + _wallet.LogTrace(solution.ToString()); return solution.Coins.ToImmutableList(); } @@ -140,19 +140,26 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector var solution = new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonScoreTarget, utxoSelectionParameters); - + var cv = new CoinsView(remainingCoins); + var percentage = await _wallet.GetPrivacyPercentageAsync(cv); var fullyPrivate = await _wallet.IsWalletPrivateAsync(new CoinsView(remainingCoins)); var coinjoiningOnlyForPayments = fullyPrivate && remainingPendingPayments.Any(); + + if (!consolidationMode && percentage < 1 && _wallet.ConsolidationMode != ConsolidationModeType.Never) + { + consolidationMode = true; + } + solution.ConsolidationMode = consolidationMode; if (fullyPrivate && !coinjoiningOnlyForPayments ) { var rand = Random.Shared.Next(1, 1001); if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability) { - _logger.LogTrace($"All coins are private and we have no pending payments. Skipping join."); + _wallet.LogTrace($"All coins are private and we have no pending payments. Skipping join."); return solution; } - _logger.LogTrace( + _wallet.LogTrace( "All coins are private and we have no pending payments but will join just to reduce timing analysis"); } @@ -234,6 +241,8 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector { continue; } + + //if we have less than the max suggested output registration, we should add more coins to reach that number to avoid breaking up into too many coins? var isLessThanMaxOutputRegistration = solution.Coins.Count < Math.Max(solution.HandledPayments.Count +1, 8); var rand = Random.Shared.Next(1, 101); @@ -253,10 +262,17 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector chance -= maxCoinCapacityPercentage; } - _logger.LogDebug($"coin selection: no payments left but at {solution.Coins.Count()} coins. random chance to add another coin if: {chance} > {rand} (random 0-100) continue: {chance > rand}"); - + if (chance <= rand) { + if (_wallet.MinimumDenominationAmount is not null && + Money.Coins(solution.LeftoverValue).Satoshi < _wallet.MinimumDenominationAmount) + { + _wallet.LogDebug( + $"coin selection: leftover value {solution.LeftoverValue} is less than minimum denomination amount {_wallet.MinimumDenominationAmount} so we will try to add more coins"); + continue; + } + _wallet.LogDebug($"coin selection: no payments left but at {solution.Coins.Count()} coins. random chance to add another coin if: {chance} > {rand} (random 0-100) continue: {chance > rand}"); break; } @@ -265,7 +281,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector if (coinjoiningOnlyForPayments && solution.HandledPayments?.Any() is not true) { - _logger.LogInformation( + _wallet.LogInfo( "Attempted to coinjoin only to fulfill payments but the coin selection results yielded no handled payment."); return new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonScoreTarget, utxoSelectionParameters); @@ -326,8 +342,6 @@ public static class SmartCoinExtensions public static AnonsetType CoinColor(this SmartCoin coin, int anonsetTarget) { return coin.IsPrivate(anonsetTarget)? AnonsetType.Green: coin.IsSemiPrivate(anonsetTarget)? AnonsetType.Orange: AnonsetType.Red; - return coin.AnonymitySet <= 1 ? AnonsetType.Red : - coin.AnonymitySet >= anonsetTarget ? AnonsetType.Green : AnonsetType.Orange; } } @@ -365,10 +379,10 @@ public class SubsetSolution payment.ToTxOut().EffectiveCost(_utxoSelectionParameters.MiningFeeRate).ToDecimal(MoneyUnit.BTC)); public decimal LeftoverValue => TotalValue - TotalPaymentCost; + public bool ConsolidationMode { get; set; } public override string ToString() { - var sb = new StringBuilder(); if (!Coins.Any()) { return "Solution yielded no selection of coins"; @@ -378,20 +392,8 @@ public class SubsetSolution sc.TryGetValue(AnonsetType.Green, out var gcoins); sc.TryGetValue(AnonsetType.Orange, out var ocoins); sc.TryGetValue(AnonsetType.Red, out var rcoins); - sb.AppendLine( - $"Solution total coins:{Coins.Count} R:{rcoins?.Length ?? 0} O:{ocoins?.Length ?? 0} G:{gcoins?.Length ?? 0} AL:{GetAnonLoss(Coins)} total value: {TotalValue} total payments:{TotalPaymentCost}/{TotalPaymentsGross} leftover: {LeftoverValue}"); - if (HandledPayments.Any()) - sb.AppendLine($"handled payments: {string.Join(", ", HandledPayments.Select(p => p.Value))} "); - return sb.ToString(); - } - - private static decimal GetAnonLoss(IEnumerable coins) - where TCoin : SmartCoin - { - double minimumAnonScore = coins.Min(x => x.AnonymitySet); - var rawSum = coins.Sum(x => x.Amount); - return coins.Sum(x => - ((decimal)x.AnonymitySet - (decimal)minimumAnonScore) * x.Amount.ToDecimal(MoneyUnit.BTC)) / rawSum; + + return $"Selected {Coins.Count} ({TotalValue} BTC) ({ocoins?.Length + rcoins?.Length} not private, {gcoins?.Length ?? 0} private) coins to pay {TotalPaymentsGross} payments ({TotalPaymentCost} BTC) with {LeftoverValue} BTC leftover\n Consolidation mode:{ConsolidationMode}"; } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj index 16c068f..feb7c9f 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj @@ -13,7 +13,7 @@ Coinjoin Allows you to integrate your btcpayserver store with coinjoins. - 1.0.67 + 1.0.68 true diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs index 6a8c5a8..10dfb84 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs @@ -51,7 +51,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider // public readonly IBTCPayServerClientFactory BtcPayServerClientFactory; public WabisabiStoreSettings WabisabiStoreSettings; public readonly IUTXOLocker UtxoLocker; - public readonly ILogger Logger; + // public readonly ILogger Logger; public static readonly BlockchainAnalyzer BlockchainAnalyzer = new(); public BTCPayWallet( @@ -85,12 +85,12 @@ public class BTCPayWallet : IWallet, IDestinationProvider UtxoLocker = utxoLocker; _storeRepository = storeRepository; _memoryCache = memoryCache; - Logger = loggerFactory.CreateLogger($"BTCPayWallet_{storeId}"); + _logger = loggerFactory.CreateLogger($"BTCPayWallet_{storeId}"); } public string StoreId { get; set; } - public List<(Microsoft.Extensions.Logging.LogLevel, string)> LastLogs { get; private set; } = new(); + public List<(DateTimeOffset time, Microsoft.Extensions.Logging.LogLevel level , string message)> LastLogs { get; private set; } = new(); public void Log(LogLevel logLevel, string logMessage, string callerFilePath = "", string callerMemberName = "", int callerLineNumber = -1) { @@ -104,12 +104,12 @@ public class BTCPayWallet : IWallet, IDestinationProvider LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical, _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) }; - if(LastLogs.FirstOrDefault().Item2 != logMessage) - LastLogs.Insert(0, (ll, logMessage) ); + if(LastLogs.FirstOrDefault().message != logMessage) + LastLogs.Insert(0, (DateTimeOffset.Now, ll, logMessage) ); if (LastLogs.Count >= 100) LastLogs.RemoveLast(); - Logger.Log(ll, logMessage, callerFilePath, callerMemberName, callerLineNumber); + _logger.Log(ll, logMessage, callerFilePath, callerMemberName, callerLineNumber); } public string WalletName => StoreId; @@ -147,16 +147,16 @@ public class BTCPayWallet : IWallet, IDestinationProvider public async Task IsWalletPrivateAsync(CoinsView coins) { - var privacy= GetPrivacyPercentage(coins, AnonScoreTarget); + var privacy= await GetPrivacyPercentageAsync(coins); var mixToOtherWallet = !WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings .MixToOtherWallet); var forceConsolidate = ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && coins.Available().Confirmed().Count() > HighAmountOfCoins; return !BatchPayments && privacy >= 1 && !mixToOtherWallet && !forceConsolidate; } - public async Task GetPrivacyPercentageAsync() + public async Task GetPrivacyPercentageAsync(CoinsView coins) { - return GetPrivacyPercentage(await GetAllCoins(), AnonScoreTarget); + return GetPrivacyPercentage(coins, AnonScoreTarget); } public async Task GetAllCoins() @@ -199,7 +199,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider public IRoundCoinSelector GetCoinSelector() { - _coinSelector??= new BTCPayCoinjoinCoinSelector(this, Logger ); + _coinSelector??= new BTCPayCoinjoinCoinSelector(this ); return _coinSelector; } @@ -213,9 +213,8 @@ public class BTCPayWallet : IWallet, IDestinationProvider { try { - - var successfulCoinJoinResult = (await finishedCoinJoin.CoinJoinTask) as SuccessfulCoinJoinResult; - + if(await finishedCoinJoin.CoinJoinTask is not SuccessfulCoinJoinResult successfulCoinJoinResult) + return; await RegisterCoinjoinTransaction(successfulCoinJoinResult, finishedCoinJoin.CoinJoinClient.CoordinatorName); } @@ -410,6 +409,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider } private Task _savingProgress = Task.CompletedTask; + private readonly ILogger _logger; public async Task RegisterCoinjoinTransaction(SuccessfulCoinJoinResult result, string coordinatorName) { @@ -419,209 +419,216 @@ public class BTCPayWallet : IWallet, IDestinationProvider } private async Task RegisterCoinjoinTransactionInternal(SuccessfulCoinJoinResult result, string coordinatorName) { - try + var attempts = 0; + while (attempts < 5) { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - var txHash = result.UnsignedCoinJoin.GetHash(); - var kp = await ExplorerClient.GetMetadataAsync(DerivationScheme, - WellknownMetadataKeys.AccountKeyPath); - - var storeIdForutxo = WabisabiStoreSettings.PlebMode || - string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet)? StoreId: WabisabiStoreSettings.MixToOtherWallet; - var utxoDerivationScheme = DerivationScheme; - if (storeIdForutxo != StoreId) - { - var s = await _storeRepository.FindStore(storeIdForutxo); - var scheme = s.GetDerivationSchemeSettings(_btcPayNetworkProvider, "BTC"); - utxoDerivationScheme = scheme.AccountDerivation; - } - List<(IndexedTxOut txout, Task)> scriptInfos = new(); - - - Dictionary indexToPayment = new(); - foreach (var script in result.Outputs) - { - var txout = result.UnsignedCoinJoin.Outputs.AsIndexedOutputs() - .Single(@out => @out.TxOut.ScriptPubKey == script.ScriptPubKey && @out.TxOut.Value == script.Value); - - - //this was not a mix to self, but rather a payment - var isPayment = result.HandledPayments.Where(pair => - pair.Key.ScriptPubKey == txout.TxOut.ScriptPubKey && pair.Key.Value == txout.TxOut.Value); - if (isPayment.Any()) - { - indexToPayment.Add(txout, isPayment.First().Value); - continue; - } - - var privateEnough = result.Coins.All(c => c.AnonymitySet >= WabisabiStoreSettings.AnonymitySetTarget ); - scriptInfos.Add((txout, ExplorerClient.GetKeyInformationAsync(BlockchainAnalyzer.StdDenoms.Contains(txout.TxOut.Value)&& privateEnough?utxoDerivationScheme:DerivationScheme, script.ScriptPubKey))); - } - - await Task.WhenAll(scriptInfos.Select(t => t.Item2)); - var scriptInfos2 = scriptInfos.Where(tuple => tuple.Item2.Result is not null).ToDictionary(tuple => tuple.txout.TxOut.ScriptPubKey); - var smartTx = new SmartTransaction(result.UnsignedCoinJoin, new Height(HeightType.Unknown)); - result.Coins.ForEach(coin => - { - coin.HdPubKey.SetKeyState(KeyState.Used); - coin.SpenderTransaction = smartTx; - smartTx.TryAddWalletInput(SmartCoin.Clone(coin)); - }); - result.Outputs.ForEach(s => - { - if (scriptInfos2.TryGetValue(s.ScriptPubKey, out var si)) - { - var derivation = DerivationScheme.GetChild(si.Item2.Result.KeyPath).GetExtPubKeys().First().PubKey; - - var hdPubKey = new HdPubKey(derivation, kp.Derive(si.Item2.Result.KeyPath).KeyPath, - LabelsArray.Empty, - KeyState.Used); - - var coin = new SmartCoin(smartTx, si.txout.N, hdPubKey); - smartTx.TryAddWalletOutput(coin); - } - }); + //wait longer between attempts + await Task.Delay(TimeSpan.FromSeconds(attempts * 3)); + attempts++; try { - BlockchainAnalyzer.Analyze(smartTx); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var txHash = result.UnsignedCoinJoin.GetHash(); + var kp = await ExplorerClient.GetMetadataAsync(DerivationScheme, + WellknownMetadataKeys.AccountKeyPath); + + var storeIdForutxo = WabisabiStoreSettings.PlebMode || + string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet) + ? StoreId + : WabisabiStoreSettings.MixToOtherWallet; + var utxoDerivationScheme = DerivationScheme; + if (storeIdForutxo != StoreId) + { + var s = await _storeRepository.FindStore(storeIdForutxo); + var scheme = s.GetDerivationSchemeSettings(_btcPayNetworkProvider, "BTC"); + utxoDerivationScheme = scheme.AccountDerivation; + } + + List<(IndexedTxOut txout, Task)> scriptInfos = new(); + + + Dictionary indexToPayment = new(); + foreach (var script in result.Outputs) + { + var txout = result.UnsignedCoinJoin.Outputs.AsIndexedOutputs() + .Single(@out => + @out.TxOut.ScriptPubKey == script.ScriptPubKey && @out.TxOut.Value == script.Value); + + + //this was not a mix to self, but rather a payment + var isPayment = result.HandledPayments.Where(pair => + pair.Key.ScriptPubKey == txout.TxOut.ScriptPubKey && pair.Key.Value == txout.TxOut.Value); + if (isPayment.Any()) + { + indexToPayment.Add(txout, isPayment.First().Value); + continue; + } + + var privateEnough = + result.Coins.All(c => c.AnonymitySet >= WabisabiStoreSettings.AnonymitySetTarget); + scriptInfos.Add((txout, + ExplorerClient.GetKeyInformationAsync( + BlockchainAnalyzer.StdDenoms.Contains(txout.TxOut.Value) && privateEnough + ? utxoDerivationScheme + : DerivationScheme, script.ScriptPubKey))); + } + + await Task.WhenAll(scriptInfos.Select(t => t.Item2)); + var scriptInfos2 = scriptInfos.Where(tuple => tuple.Item2.Result is not null) + .ToDictionary(tuple => tuple.txout.TxOut.ScriptPubKey); + var smartTx = new SmartTransaction(result.UnsignedCoinJoin, new Height(HeightType.Unknown)); + result.Coins.ForEach(coin => + { + coin.HdPubKey.SetKeyState(KeyState.Used); + coin.SpenderTransaction = smartTx; + smartTx.TryAddWalletInput(SmartCoin.Clone(coin)); + }); + result.Outputs.ForEach(s => + { + if (scriptInfos2.TryGetValue(s.ScriptPubKey, out var si)) + { + var derivation = DerivationScheme.GetChild(si.Item2.Result.KeyPath).GetExtPubKeys().First() + .PubKey; + + var hdPubKey = new HdPubKey(derivation, kp.Derive(si.Item2.Result.KeyPath).KeyPath, + LabelsArray.Empty, + KeyState.Used); + + var coin = new SmartCoin(smartTx, si.txout.N, hdPubKey); + smartTx.TryAddWalletOutput(coin); + } + }); + + try + { + BlockchainAnalyzer.Analyze(smartTx); + } + catch (Exception e) + { + this.LogError($"Failed to analyze anonsets of tx {smartTx.GetHash()}"); + } + + + + var cjData = new CoinjoinData() + { + Round = result.RoundId.ToString(), + CoordinatorName = coordinatorName, + Transaction = txHash.ToString(), + CoinsIn = result.Coins.Select(coin => new CoinjoinData.CoinjoinDataCoin() + { + AnonymitySet = coin.AnonymitySet, + PayoutId = null, + Amount = coin.Amount.ToDecimal(MoneyUnit.BTC), + Outpoint = coin.Outpoint.ToString() + }).ToArray(), + CoinsOut = smartTx.WalletOutputs.Select(coin => new CoinjoinData.CoinjoinDataCoin() + { + AnonymitySet = coin.AnonymitySet, + PayoutId = null, + Amount = coin.Amount.ToDecimal(MoneyUnit.BTC), + Outpoint = coin.Outpoint.ToString() + }).Concat(indexToPayment.Select(pair => new CoinjoinData.CoinjoinDataCoin() + { + Amount = pair.Key.TxOut.Value.ToDecimal(MoneyUnit.BTC), + PayoutId = pair.Value.Identifier, + Outpoint = new OutPoint(result.UnsignedCoinJoin, pair.Key.N).ToString() + })).ToArray() + }; + foreach (var smartTxWalletOutput in smartTx.WalletOutputs) + { + Smartifier.SetIsSufficientlyDistancedFromExternalKeys(smartTxWalletOutput, cjData); + } + + var attachments = new List() + { + new("coinjoin", result.RoundId.ToString(), JObject.FromObject(cjData)), + new(coordinatorName, null, null) + + }; + + + if (result.HandledPayments.Any()) + { + attachments.AddRange(result.HandledPayments.Select(payment => + new Attachment("payout", payment.Value.Identifier))); + } + + List<(WalletId wallet, string id, IEnumerable attachments, string type )> objects = new(); + + objects.Add((new WalletId(StoreId, "BTC"), + result.UnsignedCoinJoin.GetHash().ToString(), + attachments, "tx")); + + var mixedCoins = smartTx.WalletOutputs.Where(coin => + coin.AnonymitySet > 1 && BlockchainAnalyzer.StdDenoms.Contains(coin.TxOut.Value.Satoshi)); + if (storeIdForutxo != StoreId) + { + + + objects.Add((new WalletId(storeIdForutxo, "BTC"), + txHash.ToString(), new List() + { + new Attachment("coinjoin", result.RoundId.ToString(), JObject.FromObject(new CoinjoinData() + { + Transaction = txHash.ToString(), + Round = result.RoundId.ToString(), + CoinsOut = mixedCoins.Select(coin => new CoinjoinData.CoinjoinDataCoin() + { + AnonymitySet = coin.AnonymitySet, + Amount = coin.Amount.ToDecimal(MoneyUnit.BTC), + Outpoint = coin.Outpoint.ToString() + }).ToArray(), + CoordinatorName = coordinatorName + })), + new Attachment(coordinatorName, null, null) + }, "tx")); + + } + + foreach (var mixedCoin in mixedCoins) + { + objects.Add((new WalletId(storeIdForutxo, "BTC"), + mixedCoin.Outpoint.ToString(), + new[] + { + new Attachment("anonset", mixedCoin.AnonymitySet.ToString(), JObject.FromObject(new + { + Tooltip = + $"This coin has an anonset score of {mixedCoin.AnonymitySet.ToString()} (anonset-{mixedCoin.AnonymitySet.ToString()})" + })) + }, "utxo")); + + } + + _smartifier.SmartTransactions.AddOrReplace(txHash, Task.FromResult(smartTx)); + smartTx.WalletOutputs.ForEach(coin => + { + _smartifier.Coins.AddOrReplace(coin.Outpoint, Task.FromResult(coin)); + }); + + await _walletRepository.AddWalletTransactionAttachments(objects.ToArray()); + + stopwatch.Stop(); + + this.LogInfo($"Registered coinjoin result for {StoreId} in {stopwatch.Elapsed}"); + _memoryCache.Remove(WabisabiService.GetCacheKey(StoreId) + "cjhistory"); + + break; } catch (Exception e) { - Logger.LogError($"Failed to analyze anonsets of tx {smartTx.GetHash()}"); - } - - - var cjData = new CoinjoinData() - { - Round = result.RoundId.ToString(), - CoordinatorName = coordinatorName, - Transaction = txHash.ToString(), - CoinsIn = result.Coins.Select(coin => new CoinjoinData.CoinjoinDataCoin() - { - AnonymitySet = coin.AnonymitySet, - PayoutId = null, - Amount = coin.Amount.ToDecimal(MoneyUnit.BTC), - Outpoint = coin.Outpoint.ToString() - }).ToArray(), - CoinsOut = smartTx.WalletOutputs.Select(coin => new CoinjoinData.CoinjoinDataCoin() - { - AnonymitySet = coin.AnonymitySet, - PayoutId = null, - Amount = coin.Amount.ToDecimal(MoneyUnit.BTC), - Outpoint = coin.Outpoint.ToString() - }).Concat(indexToPayment.Select(pair => new CoinjoinData.CoinjoinDataCoin() - { - Amount = pair.Key.TxOut.Value.ToDecimal(MoneyUnit.BTC), - PayoutId = pair.Value.Identifier, - Outpoint = new OutPoint(result.UnsignedCoinJoin, pair.Key.N).ToString() - })).ToArray() - }; - foreach (var smartTxWalletOutput in smartTx.WalletOutputs) - { - Smartifier.SetIsSufficientlyDistancedFromExternalKeys(smartTxWalletOutput, cjData); - } - - var attachments = new List() - { - new("coinjoin", result.RoundId.ToString(), JObject.FromObject(cjData)), - new(coordinatorName, null, null) - - }; - - - if (result.HandledPayments.Any()) - { - attachments.AddRange(result.HandledPayments.Select(payment => new Attachment("payout", payment.Value.Identifier))); - } - List<(WalletId wallet, string id, IEnumerable attachments, string type )> objects = new(); - - objects.Add((new WalletId(StoreId, "BTC"), - result.UnsignedCoinJoin.GetHash().ToString(), - attachments, "tx")); - - var mixedCoins = smartTx.WalletOutputs.Where(coin => - coin.AnonymitySet > 1 && BlockchainAnalyzer.StdDenoms.Contains(coin.TxOut.Value.Satoshi)); - if (storeIdForutxo != StoreId) - { - - - objects.Add((new WalletId(storeIdForutxo, "BTC"), - txHash.ToString(), new List() - { - new Attachment("coinjoin", result.RoundId.ToString(), JObject.FromObject(new CoinjoinData() - { - Transaction = txHash.ToString(), - Round = result.RoundId.ToString(), - CoinsOut = mixedCoins.Select(coin => new CoinjoinData.CoinjoinDataCoin() - { - AnonymitySet = coin.AnonymitySet, - Amount = coin.Amount.ToDecimal(MoneyUnit.BTC), - Outpoint = coin.Outpoint.ToString() - }).ToArray(), - CoordinatorName = coordinatorName - })), - new Attachment(coordinatorName, null, null) - }, "tx")); + this.LogError( "Could not save coinjoin progress! " + e.Message); + // ignored } - foreach (var mixedCoin in mixedCoins) - { - objects.Add((new WalletId(storeIdForutxo, "BTC"), - mixedCoin.Outpoint.ToString(), - new[] - { - new Attachment("anonset", mixedCoin.AnonymitySet.ToString(), JObject.FromObject(new - { - Tooltip = - $"This coin has an anonset score of {mixedCoin.AnonymitySet.ToString()} (anonset-{mixedCoin.AnonymitySet.ToString()})" - })) - }, "utxo")); - - } - - _smartifier.SmartTransactions.AddOrReplace(txHash, Task.FromResult(smartTx)); - smartTx.WalletOutputs.ForEach(coin => - { - _smartifier.Coins.AddOrReplace(coin.Outpoint, Task.FromResult(coin)); - }); - - await _walletRepository.AddWalletTransactionAttachments(objects.ToArray()); - - stopwatch.Stop(); - - Logger.LogInformation($"Registered coinjoin result for {StoreId} in {stopwatch.Elapsed}"); - _memoryCache.Remove(WabisabiService.GetCacheKey(StoreId) + "cjhistory"); - - } - catch (Exception e) - { - Logger.LogError(e, "Could not save coinjoin progress!"); - // ignored } } - - // public async Task UnlockUTXOs() - // { - // var client = await BtcPayServerClientFactory.Create(null, StoreId); - // var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); - // var unlocked = new List(); - // foreach (OnChainWalletUTXOData utxo in utxos) - // { - // - // if (await UtxoLocker.TryUnlock(utxo.Outpoint)) - // { - // unlocked.Add(utxo.Outpoint.ToString()); - // } - // } - // - // Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}"); - // } - public async Task> GetNextDestinationsAsync(int count, bool mixedOutputs, bool privateEnough) { if (!WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet) && mixedOutputs && privateEnough) diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml index 5dbb39a..147ca71 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml @@ -13,7 +13,6 @@ @inject WabisabiCoordinatorClientInstanceManager WabisabiCoordinatorClientInstanceManager -@inject IExplorerClientProvider ExplorerClientProvider @{ var available = true; @@ -21,6 +20,7 @@ { return; } + var storeId = ScopeProvider.GetCurrentStoreId(); Context.Items["cjlite"] = true; } @@ -34,7 +34,7 @@ @if (!enabledSettings.Any()) {
- +
} else @@ -65,7 +65,7 @@ var privacy = wallet.GetPrivacyPercentage(coins, wallet.AnonScoreTarget); var privacyPercentage = Math.Round(privacy * 100); - var data = new + var data = new { privacyProgress = privacyPercentage, targetScore = wallet.AnonScoreTarget, @@ -78,11 +78,9 @@ id = coin.Outpoint.ToString(), coinjoinInProgress = coin.CoinJoinInProgress }).OrderBy(coin => coin.isPrivate).ThenBy(coin => coin.score), - }; - @if(coins.Any()) + @if (coins.Any()) { - } +
@if (wallet is { }) @@ -289,10 +288,11 @@ updateInProgressAnimation(myChart); @if (coins.Any()) { -
- - } - +
+ +
+ } + +
} - + } diff --git a/submodules/walletwasabi b/submodules/walletwasabi index 847a659..cebb578 160000 --- a/submodules/walletwasabi +++ b/submodules/walletwasabi @@ -1 +1 @@ -Subproject commit 847a659227d2866ab18a221a4e9d610a88eea794 +Subproject commit cebb57875cecb60a527cbf1ca10097ab61ef1d83