diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs index 9933efa..778a7aa 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs @@ -128,7 +128,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector //still good to have a chance to proceed with a join to reduce timing analysis var rand = Random.Shared.Next(1, 101); - if (rand > 5) + if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability) { _logger.LogInformation($"All coins are private and we have no pending payments. Skipping join."); return solution; diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj index c2836aa..bb781f2 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj @@ -44,8 +44,8 @@ - - + + StaticWebAssetsEnabled=false true @@ -53,12 +53,12 @@ - + - + - + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs index c05ebd3..ff4f215 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Threading; @@ -9,27 +8,40 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Payments; using BTCPayServer.Payments.PayJoin; +using BTCPayServer.Services; +using BTCPayServer.Services.Wallets; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; using NBXplorer.DerivationStrategy; using NBXplorer.Models; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletWasabi.Blockchain.Analysis; using WalletWasabi.Blockchain.Analysis.Clustering; using WalletWasabi.Blockchain.Keys; using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.Blockchain.Transactions; +using WalletWasabi.Extensions; using WalletWasabi.Models; +using WalletWasabi.WabiSabi.Backend.Rounds; using WalletWasabi.WabiSabi.Client; using WalletWasabi.Wallets; namespace BTCPayServer.Plugins.Wabisabi; -public class BTCPayWallet : IWallet +public class BTCPayWallet : IWallet, IDestinationProvider { + private readonly WalletRepository _walletRepository; + private readonly BitcoinLikePayoutHandler _bitcoinLikePayoutHandler; + private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; + private readonly Services.Wallets.BTCPayWallet _btcPayWallet; + private readonly PullPaymentHostedService _pullPaymentHostedService; public OnChainPaymentMethodData OnChainPaymentMethodData; public readonly DerivationStrategyBase DerivationScheme; public readonly ExplorerClient ExplorerClient; @@ -39,15 +51,30 @@ public class BTCPayWallet : IWallet public readonly ILogger Logger; private static readonly BlockchainAnalyzer BlockchainAnalyzer = new(); - public BTCPayWallet(OnChainPaymentMethodData onChainPaymentMethodData, DerivationStrategyBase derivationScheme, - ExplorerClient explorerClient, BTCPayKeyChain keyChain, - IDestinationProvider destinationProvider, IBTCPayServerClientFactory btcPayServerClientFactory, string storeId, - WabisabiStoreSettings wabisabiStoreSettings, IUTXOLocker utxoLocker, - ILoggerFactory loggerFactory, Smartifier smartifier, + public BTCPayWallet( + WalletRepository walletRepository, + BitcoinLikePayoutHandler bitcoinLikePayoutHandler, + BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, + Services.Wallets.BTCPayWallet btcPayWallet, + PullPaymentHostedService pullPaymentHostedService, + OnChainPaymentMethodData onChainPaymentMethodData, + DerivationStrategyBase derivationScheme, + ExplorerClient explorerClient, + BTCPayKeyChain keyChain, + IBTCPayServerClientFactory btcPayServerClientFactory, + string storeId, + WabisabiStoreSettings wabisabiStoreSettings, + IUTXOLocker utxoLocker, + ILoggerFactory loggerFactory, + Smartifier smartifier, ConcurrentDictionary> bannedCoins) { KeyChain = keyChain; - DestinationProvider = destinationProvider; + _walletRepository = walletRepository; + _bitcoinLikePayoutHandler = bitcoinLikePayoutHandler; + _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; + _btcPayWallet = btcPayWallet; + _pullPaymentHostedService = pullPaymentHostedService; OnChainPaymentMethodData = onChainPaymentMethodData; DerivationScheme = derivationScheme; ExplorerClient = explorerClient; @@ -72,7 +99,7 @@ public class BTCPayWallet : IWallet } public IKeyChain KeyChain { get; } - public IDestinationProvider DestinationProvider { get; } + public IDestinationProvider DestinationProvider => this; public int AnonymitySetTarget => WabisabiStoreSettings.PlebMode? 2: WabisabiStoreSettings.AnonymitySetTarget; public bool ConsolidationMode => !WabisabiStoreSettings.PlebMode && WabisabiStoreSettings.ConsolidationMode; @@ -93,10 +120,11 @@ public class BTCPayWallet : IWallet public async Task GetAllCoins() { await _savingProgress; - var client = await BtcPayServerClientFactory.Create(null, StoreId); - var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); - await _smartifier.LoadCoins(utxos.ToList()); - var coins = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.Outpoint == pair.Key)) + + var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme); + var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos); + 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)); return new CoinsView(coins); @@ -140,31 +168,40 @@ public class BTCPayWallet : IWallet { return Array.Empty(); } - - var client = await BtcPayServerClientFactory.Create(null, StoreId); - var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); - var objs = client.GetOnChainWalletObjects(StoreId, "BTC", - new GetWalletObjectsRequest() - { - Type = "utxo", Ids = utxos.Select(data => data.Outpoint.ToString()).ToArray() - }); + + var utxos = await _btcPayWallet.GetUnspentCoins(DerivationScheme, true, CancellationToken.None); + var utxoLabels = await GetUtxoLabels(_walletRepository, StoreId,utxos); if (!WabisabiStoreSettings.PlebMode) { if (WabisabiStoreSettings.InputLabelsAllowed?.Any() is true) { + utxos = utxos.Where(data => - !WabisabiStoreSettings.InputLabelsAllowed.Any(s => data.Labels.ContainsKey(s))); + utxoLabels.TryGetValue(data.OutPoint, out var opLabels) && + opLabels.Any( + attachment => WabisabiStoreSettings.InputLabelsAllowed.Any(s => attachment.Id == s))).ToArray(); } if (WabisabiStoreSettings.InputLabelsExcluded?.Any() is true) { + utxos = utxos.Where(data => - WabisabiStoreSettings.InputLabelsExcluded.All(s => !data.Labels.ContainsKey(s))); + !utxoLabels.TryGetValue(data.OutPoint, out var opLabels) || + opLabels.All( + attachment => WabisabiStoreSettings.InputLabelsExcluded.All(s => attachment.Id != s))).ToArray(); } } - var locks = await UtxoLocker.FindLocks(utxos.Select(data => data.Outpoint).ToArray()); - utxos = utxos.Where(data => !locks.Contains(data.Outpoint)).Where(data => data.Confirmations > 0); + if (WabisabiStoreSettings.PlebMode || !WabisabiStoreSettings.CrossMixBetweenCoordinators) + { + utxos = utxos.Where(data => + !utxoLabels.TryGetValue(data.OutPoint, out var opLabels) || + !opLabels.Any(attachment => attachment.Type == "coinjoin" && attachment.Data?.Value("CoordinatorName") == coordinatorName)) + .ToArray(); + } + + var locks = await UtxoLocker.FindLocks(utxos.Select(data => data.OutPoint).ToArray()); + utxos = utxos.Where(data => !locks.Contains(data.OutPoint)).Where(data => data.Confirmations > 0).ToArray(); if (_bannedCoins.TryGetValue(coordinatorName, out var bannedCoins)) { var expired = bannedCoins.Where(pair => pair.Value < DateTimeOffset.Now).ToArray(); @@ -174,16 +211,16 @@ public class BTCPayWallet : IWallet } - utxos = utxos.Where(data => !bannedCoins.ContainsKey(data.Outpoint)); + utxos = utxos.Where(data => !bannedCoins.ContainsKey(data.OutPoint)).ToArray(); } - await _smartifier.LoadCoins(utxos.Where(data => data.Confirmations>0).ToList()); + await _smartifier.LoadCoins(utxos.Where(data => data.Confirmations>0).ToList(), 1, utxoLabels); - var resultX = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.Outpoint == pair.Key)) + var resultX = await Task.WhenAll(_smartifier.Coins.Where(pair => utxos.Any(data => data.OutPoint == pair.Key)) .Select(pair => pair.Value)); 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); } @@ -196,6 +233,28 @@ public class BTCPayWallet : IWallet } } + public static async Task>> 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 => + { + walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1); + walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2); + walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.ToString(), out var info3); + var info = walletRepository.Merge(info1, info2, info3); + if (info is null) + { + return (coin.OutPoint, null); + } + + return (coin.OutPoint, info.Attachments); + }).Where(tuple => tuple.Attachments is not null) + .ToDictionary(tuple => tuple.OutPoint, tuple => tuple.Attachments); + return utxoLabels; + } + public async Task> GetTransactionsAsync() { @@ -471,4 +530,141 @@ public class BTCPayWallet : IWallet Logger.LogInformation($"unlocked utxos: {string.Join(',', unlocked)}"); } +public async Task> GetNextDestinationsAsync(int count, bool preferTaproot, bool mixedOutputs) + { + if (!WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet) && mixedOutputs) + { + try + { + var mixClient = await BtcPayServerClientFactory.Create(null, WabisabiStoreSettings.MixToOtherWallet); + var pm = await mixClient.GetStoreOnChainPaymentMethod(WabisabiStoreSettings.MixToOtherWallet, + "BTC"); + + var deriv = ExplorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); + if (deriv.ScriptPubKeyType() == DerivationScheme.ScriptPubKeyType()) + { + return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ => + _btcPayWallet.ReserveAddressAsync(WabisabiStoreSettings.MixToOtherWallet, deriv, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address)); + } + } + + catch (Exception e) + { + WabisabiStoreSettings.MixToOtherWallet = null; + } + } + return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ => + _btcPayWallet.ReserveAddressAsync(StoreId ,DerivationScheme, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address)); + } + + public async Task> GetPendingPaymentsAsync( UtxoSelectionParameters roundParameters) + { + + + try + { + var payouts = (await _pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery() + { + States = new [] {PayoutState.AwaitingPayment}, + Stores = new []{StoreId}, + PaymentMethods = new []{"BTC"} + })).Select(async data => + { + + var claim = await _bitcoinLikePayoutHandler.ParseClaimDestination(new PaymentMethodId("BTC", PaymentTypes.BTCLike), + data.Destination, CancellationToken.None); + + if (!string.IsNullOrEmpty(claim.error) || claim.destination is not IBitcoinLikeClaimDestination bitcoinLikeClaimDestination ) + { + return null; + } + + var payoutBlob = data.GetBlob(_btcPayNetworkJsonSerializerSettings); + var value = new Money(payoutBlob.CryptoAmount.Value, MoneyUnit.BTC); + if (!roundParameters.AllowedOutputAmounts.Contains(value) || + !roundParameters.AllowedOutputScriptTypes.Contains(bitcoinLikeClaimDestination.Address.ScriptPubKey.GetScriptType())) + { + return null; + } + return new PendingPayment() + { + Identifier = data.Id, + Destination = bitcoinLikeClaimDestination.Address, + Value =value, + PaymentStarted = PaymentStarted(data.Id), + PaymentFailed = PaymentFailed(data.Id), + PaymentSucceeded = PaymentSucceeded(data.Id), + }; + }).Where(payment => payment is not null).ToArray(); + return await Task.WhenAll(payouts); + } + catch (Exception e) + { + Console.WriteLine(e); + return Array.Empty(); + } + } + + private Action<(uint256 roundId, uint256 transactionId, int outputIndex)> PaymentSucceeded(string payoutId) + { + + return tuple => + _pullPaymentHostedService.MarkPaid( new HostedServices.MarkPayoutRequest() + { + PayoutId = payoutId, + State = PayoutState.InProgress, + Proof = JObject.FromObject(new PayoutTransactionOnChainBlob() + { + Candidates = new HashSet() + { + tuple.transactionId + }, + TransactionId = tuple.transactionId + }) + }); + } + + private Action PaymentFailed(string payoutId) + { + return () => + { + _pullPaymentHostedService.MarkPaid(new HostedServices.MarkPayoutRequest() + { + PayoutId = payoutId, + State = PayoutState.AwaitingPayment + }); + }; + } + + private Func> PaymentStarted(string payoutId) + { + return async () => + { + try + { + await _pullPaymentHostedService.MarkPaid( new HostedServices.MarkPayoutRequest() + { + PayoutId = payoutId, + State = PayoutState.InProgress, + Proof = JObject.FromObject(new WabisabiPaymentProof()) + }); + return true; + } + catch (Exception e) + { + return false; + } + }; + } + + public class WabisabiPaymentProof + { + [JsonProperty("proofType")] + public string ProofType { get; set; } = "Wabisabi"; + [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] + public uint256 TransactionId { get; set; } + [JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)] + public HashSet Candidates { get; set; } = new HashSet(); + public string Link { get; set; } + } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/CoinjoinsViewModel.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/CoinjoinsViewModel.cs new file mode 100644 index 0000000..d93d424 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/CoinjoinsViewModel.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using BTCPayServer.Models; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class CoinjoinsViewModel : BasePagingViewModel +{ + public List Coinjoins { get; set; } = new(); + public override int CurrentPageCount => Coinjoins.Count; +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs index 12ad1a3..4cba256 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs @@ -1,8 +1,7 @@ -using System; -using System.Net.Http; -using System.Reflection; -using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Services; +using BTCPayServer.Configuration; +using BTCPayServer.Services; using Microsoft.Extensions.DependencyInjection; using WalletWasabi.WabiSabi.Models.Serialization; @@ -24,20 +23,19 @@ public static class CoordinatorExtensions services.AddSingleton(new UIExtension("Wabisabi/WabisabiServerNavvExtension", "server-nav")); - Type t = Assembly.GetEntryAssembly().GetType("BTCPayServer.Services.Socks5HttpClientHandler"); services.AddHttpClient("wabisabi-coordinator-scripts-no-redirect.onion") .ConfigurePrimaryHttpMessageHandler(provider => { - var handler = (HttpClientHandler)ActivatorUtilities.CreateInstance(provider, t); + + var handler = new Socks5HttpClientHandler(provider.GetRequiredService()); handler.AllowAutoRedirect = false; return handler; }); - services.AddHttpClient("wabisabi-coordinator-scripts.onion") .ConfigurePrimaryHttpMessageHandler(provider => { - var handler = (HttpClientHandler)ActivatorUtilities.CreateInstance(provider, t); + var handler = new Socks5HttpClientHandler(provider.GetRequiredService()); handler.AllowAutoRedirect = false; return handler; }); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/NBXInternalDestinationProvider.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/NBXInternalDestinationProvider.cs deleted file mode 100644 index a163ba3..0000000 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/NBXInternalDestinationProvider.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Client; -using BTCPayServer.Client.Models; -using NBitcoin; -using NBitcoin.Payment; -using NBXplorer; -using NBXplorer.DerivationStrategy; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using WalletWasabi.Extensions; -using WalletWasabi.WabiSabi.Backend.Rounds; -using WalletWasabi.WabiSabi.Client; - -namespace BTCPayServer.Plugins.Wabisabi; - -public class NBXInternalDestinationProvider : IDestinationProvider -{ - private readonly ExplorerClient _explorerClient; - private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; - private readonly DerivationStrategyBase _derivationStrategy; - private readonly BTCPayServerClient _client; - private readonly string _storeId; - private readonly WabisabiStoreSettings _wabisabiStoreSettings; - - public NBXInternalDestinationProvider(ExplorerClient explorerClient, IBTCPayServerClientFactory btcPayServerClientFactory, - DerivationStrategyBase derivationStrategy, BTCPayServerClient client, string storeId, WabisabiStoreSettings wabisabiStoreSettings) - { - _explorerClient = explorerClient; - _btcPayServerClientFactory = btcPayServerClientFactory; - _derivationStrategy = derivationStrategy; - _client = client; - _storeId = storeId; - _wabisabiStoreSettings = wabisabiStoreSettings; - } - - public async Task> GetNextDestinationsAsync(int count, bool preferTaproot, bool mixedOutputs) - { - if (!_wabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(_wabisabiStoreSettings.MixToOtherWallet) && mixedOutputs) - { - try - { - var mixClient = await _btcPayServerClientFactory.Create(null, _wabisabiStoreSettings.MixToOtherWallet); - var pm = await mixClient.GetStoreOnChainPaymentMethod(_wabisabiStoreSettings.MixToOtherWallet, - "BTC"); - - var deriv = _explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); - if (deriv.ScriptPubKeyType() == _derivationStrategy.ScriptPubKeyType()) - { - - return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ => - _explorerClient.GetUnusedAsync(deriv, DerivationFeature.Deposit, 0, true))).ContinueWith(task => task.Result.Select(information => information.Address)); - } - } - - catch (Exception e) - { - _wabisabiStoreSettings.MixToOtherWallet = null; - } - } - return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ => - _explorerClient.GetUnusedAsync(_derivationStrategy, DerivationFeature.Deposit, 0, true))).ContinueWith(task => task.Result.Select(information => information.Address)); - } - - public async Task> GetPendingPaymentsAsync( UtxoSelectionParameters roundParameters) - { - try - { - var payouts = await _client.GetStorePayouts(_storeId, false); - return payouts.Where(data => - data.State == PayoutState.AwaitingPayment && - data.PaymentMethod.Equals("BTC", StringComparison.InvariantCultureIgnoreCase)).Select(data => - { - IDestination destination = null; - try - { - - var bip21 = new BitcoinUrlBuilder(data.Destination, _explorerClient.Network.NBitcoinNetwork); - destination = bip21.Address; - } - catch (Exception e) - { - destination = BitcoinAddress.Create(data.Destination, _explorerClient.Network.NBitcoinNetwork); - } - - var value = new Money((decimal)data.PaymentMethodAmount, MoneyUnit.BTC); - if (!roundParameters.AllowedOutputAmounts.Contains(value) || - !roundParameters.AllowedOutputScriptTypes.Contains(destination.ScriptPubKey.GetScriptType())) - { - return null; - } - return new PendingPayment() - { - Identifier = data.Id, - Destination = destination, - Value =value, - PaymentStarted = PaymentStarted(_storeId, data.Id), - PaymentFailed = PaymentFailed(_storeId, data.Id), - PaymentSucceeded = PaymentSucceeded(_storeId, data.Id, - _explorerClient.Network.NBitcoinNetwork.ChainName == ChainName.Mainnet), - }; - }).Where(payment => payment is not null).ToArray(); - } - catch (Exception e) - { - Console.WriteLine(e); - return Array.Empty(); - } - } - - private Action<(uint256 roundId, uint256 transactionId, int outputIndex)> PaymentSucceeded(string storeId, string payoutId, bool mainnet) - { - return tuple => - _client.MarkPayout(storeId, payoutId, new MarkPayoutRequest() - { - State = PayoutState.InProgress, - PaymentProof = JObject.FromObject(new WabisabiPaymentProof() - { - ProofType = "PayoutTransactionOnChainBlob", - Candidates = new HashSet() - { - tuple.transactionId - }, - TransactionId = tuple.transactionId, - Link = ComputeTxUrl(mainnet, tuple.transactionId.ToString(), tuple.outputIndex.ToString()) - }) - }); - - } - - public static string ComputeTxUrl(bool mainnet, string tx, string outputIndex = null) - { - var path = $"tx/{(outputIndex is null ? tx : $"{outputIndex}:{tx}#flow")}"; - return mainnet - ? $"https://mempool.space/{path}" - : $"https://mempool.space/testnet/{path}"; - } - private Action PaymentFailed(string storeId, string payoutId) - { - return () => - _client.MarkPayout(storeId, payoutId, new MarkPayoutRequest() {State = PayoutState.AwaitingPayment}); - } - - private Func> PaymentStarted(string storeId, string payoutId) - { - return async () => - { - try - { - await _client.MarkPayout(storeId, payoutId, - new MarkPayoutRequest() - { - State = PayoutState.InProgress, - PaymentProof = JObject.FromObject(new WabisabiPaymentProof()) - }); - return true; - } - catch (Exception e) - { - return false; - } - }; - } - - public class WabisabiPaymentProof - { - [JsonProperty("proofType")] - public string ProofType { get; set; } = "Wabisabi"; - [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] - public uint256 TransactionId { get; set; } - [JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)] - public HashSet Candidates { get; set; } = new HashSet(); - public string Link { get; set; } - } -} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs index 37c003b..02838e9 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client.Models; +using BTCPayServer.Services; +using BTCPayServer.Services.Wallets; using NBitcoin; using NBXplorer; using NBXplorer.DerivationStrategy; @@ -20,16 +22,20 @@ namespace BTCPayServer.Plugins.Wabisabi; public class Smartifier { + private readonly WalletRepository _walletRepository; private readonly ExplorerClient _explorerClient; public DerivationStrategyBase DerivationScheme { get; } private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; private readonly string _storeId; private readonly Action _coinOnPropertyChanged; - public Smartifier(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, + public Smartifier( + WalletRepository walletRepository, + ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, IBTCPayServerClientFactory btcPayServerClientFactory, string storeId, Action coinOnPropertyChanged) { + _walletRepository = walletRepository; _explorerClient = explorerClient; DerivationScheme = derivationStrategyBase; _btcPayServerClientFactory = btcPayServerClientFactory; @@ -45,25 +51,26 @@ public class Smartifier public readonly ConcurrentDictionary> Coins = new(); private readonly Task _accountKeyPath; - public async Task LoadCoins(List coins, int current = 1) + public async Task LoadCoins(List coins, int current , + Dictionary> utxoLabels) { coins = coins.Where(data => data is not null).ToList(); if (current > 3) { return; } - var txs = coins.Select(data => data.Outpoint.Hash).Distinct(); + var txs = coins.Select(data => data.OutPoint.Hash).Distinct(); foreach (uint256 tx in txs) { cached.TryAdd(tx, _explorerClient.GetTransactionAsync(DerivationScheme, tx)); } - foreach (OnChainWalletUTXOData coin in coins) + foreach (var coin in coins) { var client = await _btcPayServerClientFactory.Create(null, _storeId); - var tx = await Transactions.GetOrAdd(coin.Outpoint.Hash, async uint256 => + var tx = await Transactions.GetOrAdd(coin.OutPoint.Hash, async uint256 => { - var unsmartTx = await cached[coin.Outpoint.Hash]; + var unsmartTx = await cached[coin.OutPoint.Hash]; if (unsmartTx is null) { return null; @@ -71,9 +78,6 @@ public class Smartifier var smartTx = new SmartTransaction(unsmartTx.Transaction, unsmartTx.Height is null ? Height.Mempool : new Height((uint)unsmartTx.Height.Value), unsmartTx.BlockHash, firstSeen: unsmartTx.Timestamp); - //var indexesOfOurSpentInputs = unsmartTx.Inputs.Select(output => (uint)output.Inputndex).ToArray(); - // var ourSpentUtxos = unsmartTx.Transaction.Inputs.AsIndexedInputs() - // .Where(@in => indexesOfOurSpentInputs.Contains(@in.Index)).ToDictionary(@in=> @in.Index,@in => @in); var ourSpentUtxos = new Dictionary(); @@ -105,43 +109,29 @@ public class Smartifier } } } - } - var utxoObjects = await client.GetOnChainWalletObjects(_storeId, "BTC", - new GetWalletObjectsRequest() - { - Ids = ourSpentUtxos.Select(point => point.Value.PrevOut.ToString()).ToArray(), - Type = "utxo", - IncludeNeighbourData = true - }); - var labelsOfOurSpentUtxos =utxoObjects.ToDictionary(data => data.Id, - data => data.Links.Where(link => link.Type == "label")); - - - await LoadCoins(unsmartTx.Inputs.Select(output => + } + var inputsToLoad = unsmartTx.Inputs.Select(output => { if (!ourSpentUtxos.TryGetValue(output, out var outputtxin)) { return null; } + var outpoint = outputtxin.PrevOut; - var labels = labelsOfOurSpentUtxos - .GetValueOrDefault(outpoint.ToString(), - new List()) - .ToDictionary(link => link.Id, link => new LabelData()); - return new OnChainWalletUTXOData() + return new ReceivedCoin() { Timestamp = DateTimeOffset.Now, Address = - output.Address?.ToString() ?? _explorerClient.Network - .CreateAddress(DerivationScheme, output.KeyPath, output.ScriptPubKey) - .ToString(), + output.Address ?? _explorerClient.Network + .CreateAddress(DerivationScheme, output.KeyPath, output.ScriptPubKey), KeyPath = output.KeyPath, - Amount = ((Money)output.Value).ToDecimal(MoneyUnit.BTC), - Outpoint = outpoint, - Labels = labels, + Value = output.Value, + OutPoint = outpoint, Confirmations = unsmartTx.Confirmations }; - }).ToList(),current+1); + }).ToList(); + + await LoadCoins(inputsToLoad,current+1, await BTCPayWallet.GetUtxoLabels(_walletRepository, _storeId, inputsToLoad.ToArray())); foreach (MatchedOutput input in unsmartTx.Inputs) { if (!ourSpentUtxos.TryGetValue(input, out var outputtxin)) @@ -159,19 +149,18 @@ public class Smartifier return smartTx; }); - var smartCoin = await Coins.GetOrAdd(coin.Outpoint, async point => + var smartCoin = await Coins.GetOrAdd(coin.OutPoint, async point => { - - var unsmartTx = await cached[coin.Outpoint.Hash]; + utxoLabels.TryGetValue(coin.OutPoint, out var labels); + var unsmartTx = await cached[coin.OutPoint.Hash]; var pubKey = DerivationScheme.GetChild(coin.KeyPath).GetExtPubKeys().First().PubKey; var kp = (await _accountKeyPath).Derive(coin.KeyPath).KeyPath; - var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(coin.Labels.Keys.ToList()), + var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(labels?.Select(attachment => attachment.Id).ToArray()?? Array.Empty()), current == 1 ? KeyState.Clean : KeyState.Used); - var anonsetLabel = coin.Labels.Keys.FirstOrDefault(s => s.StartsWith("anonset-")) - ?.Split("-", StringSplitOptions.RemoveEmptyEntries)?.ElementAtOrDefault(1) ?? "1"; + var anonsetLabel = labels?.FirstOrDefault(s => s.Id.StartsWith("anonset-"))?.Id.Split("-", StringSplitOptions.RemoveEmptyEntries)?.ElementAtOrDefault(1) ?? "1"; hdPubKey.SetAnonymitySet(double.Parse(anonsetLabel)); - var c = new SmartCoin(tx, coin.Outpoint.N, hdPubKey); + var c = new SmartCoin(tx, coin.OutPoint.N, hdPubKey); c.PropertyChanged += CoinPropertyChanged; return c; }); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/CoinjoinHistoryTable.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/CoinjoinHistoryTable.cshtml new file mode 100644 index 0000000..1106624 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/CoinjoinHistoryTable.cshtml @@ -0,0 +1,130 @@ +@using BTCPayServer +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Payments +@using BTCPayServer.Plugins.Wabisabi +@using NBitcoin +@using WalletWasabi.Blockchain.Analysis +@model List +@inject BTCPayNetworkProvider BtcPayNetworkProvider +@{ + var network = BtcPayNetworkProvider.BTC; + var mainnet = BtcPayNetworkProvider.NetworkType == ChainName.Mainnet; +} + +@functions +{ + void PrintCoin(BTCPayWallet.CoinjoinData.CoinjoinDataCoin coin) + { + var op = OutPoint.Parse(coin.Outpoint); + + + + @coin.Outpoint + + + + + @coin.Amount + + + @if (string.IsNullOrEmpty(coin.PayoutId)) + { + @coin.AnonymitySet + } + else + { + @($"Payment ({coin.PayoutId})") + } + + + } +} + + + + + + + Round + Timestamp + Coordinator + Transaction + In + Out + + + + @foreach (var cjData in Model) + { + var cjInWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsIn.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC)))); + var cjOutWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsOut.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC)))); + + + @cjData.Round + + + @cjData.Timestamp.ToTimeAgo() + + + @cjData.CoordinatorName + + + + @cjData.Transaction + + + + @cjData.CoinsIn.Length (@cjData.CoinsIn.Sum(coin => coin.Amount) BTC) (@cjInWeightedAverage anonset wavg) + + + @cjData.CoinsOut.Length (@cjData.CoinsOut.Sum(coin => coin.Amount) BTC) (@cjOutWeightedAverage anonset wavg) + + + + + + + + + + + Inputs + + + utxo + Amount + Anonset + + + @foreach (var c in cjData.CoinsIn) + { + PrintCoin(c); + } + + + + + + + + Outputs + + + utxo + Amount + Anonset + + + @foreach (var c in cjData.CoinsOut) + { + PrintCoin(c); + } + + + + + + } + + + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml index 2c1f8d8..6703c9f 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml @@ -39,42 +39,10 @@ } @if (available) { - @functions - { - void PrintCoin(BTCPayWallet.CoinjoinData.CoinjoinDataCoin coin, bool mainnet) - { - var op = OutPoint.Parse(coin.Outpoint); - - - - @coin.Outpoint - - - - - @coin.Amount - - - @if (string.IsNullOrEmpty(coin.PayoutId)) - { - @coin.AnonymitySet - } - else - { - @($"Payment ({coin.PayoutId})") - } - - - } - } { var settings = await WabisabiService.GetWabisabiForStore(storeId); var enabledSettings = settings.Settings.Where(coordinatorSettings => coordinatorSettings.Enabled); - var cjHistory = await Client.GetOnChainWalletObjects(storeId, "BTC", new GetWalletObjectsRequest() - { - Type = "coinjoin" - }).ContinueWith(task => task.Result.Select(data => (data, data.Data.ToObject())).OrderByDescending(tuple => tuple.Item2.Timestamp)); - + var cjHistory = (await WabisabiService.GetCoinjoinHistory(storeId)).Take(10); @if (!enabledSettings.Any()) { @@ -97,7 +65,11 @@ { - Coinjoin history + Recent Coinjoins + @if (cjHistory.Any()) + { + View All + } @if (!cjHistory.Any()) { @@ -109,93 +81,8 @@ { - - - - Round - Timestamp - Coordinator - Transaction - In - Out - - - - @foreach (var cj in cjHistory) - { - var cjData = cj.Item2; - var cjInWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsIn.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC)))); - var cjOutWeightedAverage = @CoinjoinAnalyzer.WeightedAverage.Invoke(@cjData.CoinsOut.Select(coin => new CoinjoinAnalyzer.AmountWithAnonymity(coin.AnonymitySet, new Money(coin.Amount, MoneyUnit.BTC)))); - - - @cjData.Round - - - @cjData.Timestamp - - - @cjData.CoordinatorName - - - - @cjData.Transaction - - - - @cjData.CoinsIn.Length (@cjData.CoinsIn.Sum(coin => coin.Amount) BTC) (@cjInWeightedAverage anonset wavg) - - - @cjData.CoinsOut.Length (@cjData.CoinsOut.Sum(coin => coin.Amount) BTC) (@cjOutWeightedAverage anonset wavg) - - - - - - - - - - - Inputs - - - utxo - Amount - Anonset - - - @foreach (var c in cjData.CoinsIn) - { - PrintCoin(c, mainnet); - } - - - - - - - - Outputs - - - utxo - Amount - Anonset - - - @foreach (var c in cjData.CoinsOut) - { - PrintCoin(c, mainnet); - } - - - - - - } - - + } @@ -404,8 +291,7 @@ @{ foreach (var setting in enabledSettings) { - - if (! WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator)) + if (!WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator)) { continue; } @@ -441,7 +327,7 @@ } } } - + } @@ -457,4 +343,4 @@ } } -} +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/ListCoinjoins.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/ListCoinjoins.cshtml new file mode 100644 index 0000000..779eb7b --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/ListCoinjoins.cshtml @@ -0,0 +1,11 @@ + +@using BTCPayServer.Components +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Views.Stores +@model BTCPayServer.Plugins.Wabisabi.CoinjoinsViewModel +@{ + ViewData.SetActivePage(StoreNavPages.Plugins); +} + + + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml index 73d2f40..760b17d 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml @@ -118,7 +118,7 @@ - Use Anon score model + Use Anon score model Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value. Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy. @@ -138,6 +138,16 @@ Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins. + + Mix funds between different coordinators + + Whether to allow mixed coins to be mixed within different coordinators for greater privacy (Warning: This will make your coins to lose the free remix within the same coordinator) + + + ExtraJoin Probability + + Percentage probability of joining a round even if you have no payments to batch and all coins are private (Warning: a high probability will quickly eat up your balance in mining fees) + Send to other wallet diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs index 9ce28e8..e9dc3fb 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs @@ -158,8 +158,7 @@ public class WabisabiCoordinatorClientInstance var roundStateUpdaterHttpClient = WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit); sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient); - CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater, WasabiHttpClientFactory, - WasabiCoordinatorStatusFetcher, coordinatorIdentifier); + } WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger); @@ -168,14 +167,12 @@ public class WabisabiCoordinatorClientInstance if (coordinatorName == "local") { CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater, - sharedWabisabiClient, + sharedWabisabiClient, null, WasabiCoordinatorStatusFetcher, coordinatorIdentifier); } else { - - CoinJoinManager = new CoinJoinManager(coordinatorName, WalletProvider, RoundStateUpdater, - WasabiHttpClientFactory, + CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater,null, WasabiHttpClientFactory, WasabiCoordinatorStatusFetcher, coordinatorIdentifier); } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs index 379019a..ca90e55 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs @@ -70,11 +70,13 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin provider.GetRequiredService()); applicationBuilder.AddSingleton(); applicationBuilder.AddSingleton(provider => new( + provider, provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - utxoLocker + utxoLocker, + provider.GetRequiredService() )); applicationBuilder.AddWabisabiCoordinator(); applicationBuilder.AddSingleton(provider => provider.GetRequiredService()); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs index 889cea7..cddf495 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; +using BTCPayServer.Services; using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json.Linq; using WalletWasabi.WabiSabi.Client; namespace BTCPayServer.Plugins.Wabisabi @@ -14,15 +16,18 @@ namespace BTCPayServer.Plugins.Wabisabi private readonly IStoreRepository _storeRepository; private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager; private readonly WalletProvider _walletProvider; + private readonly WalletRepository _walletRepository; private string[] _ids => _coordinatorClientInstanceManager.HostedServices.Keys.ToArray(); public WabisabiService( IStoreRepository storeRepository, WabisabiCoordinatorClientInstanceManager coordinatorClientInstanceManager, - WalletProvider walletProvider) + WalletProvider walletProvider, + WalletRepository walletRepository) { _storeRepository = storeRepository; _coordinatorClientInstanceManager = coordinatorClientInstanceManager; _walletProvider = walletProvider; + _walletRepository = walletRepository; } public async Task GetWabisabiForStore(string storeId) @@ -68,6 +73,18 @@ namespace BTCPayServer.Plugins.Wabisabi await _walletProvider.SettingsUpdated(storeId, wabisabiSettings); } + + + public async Task> GetCoinjoinHistory(string storeId) + { + return (await _walletRepository.GetWalletObjects( + new GetWalletObjectsQuery(new WalletId(storeId, "BTC")) + { + Type = "coinjoin" + })).Values.Where(data => !string.IsNullOrEmpty(data.Data)) + .Select(data => JObject.Parse(data.Data).ToObject()) + .OrderByDescending(tuple => tuple.Timestamp).ToList(); + } } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs index 661064e..b5b6aff 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs @@ -11,6 +11,7 @@ using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Common; +using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -176,7 +177,19 @@ namespace BTCPayServer.Plugins.Wabisabi return View(vm); } } + [Route("coinjoins")] + public async Task ListCoinjoins(string storeId, CoinjoinsViewModel viewModel, [FromServices] WalletRepository walletRepository) + { + var objects =await _WabisabiService.GetCoinjoinHistory(storeId); + viewModel ??= new CoinjoinsViewModel(); + viewModel.Coinjoins = objects + .Skip(viewModel.Skip) + .Take(viewModel.Count).ToList(); + viewModel.Total = objects.Count(); + return View(viewModel); + } + [HttpGet("spend")] public async Task Spend(string storeId) { diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs index 5e79a57..0ccfbb2 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs @@ -19,7 +19,8 @@ public class WabisabiStoreSettings public int AnonymitySetTarget { get; set; } = 5; public bool BatchPayments { get; set; } = true; - + public int ExtraJoinProbability { get; set; } = 0; + public bool CrossMixBetweenCoordinators { get; set; } = false; } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs index 886102a..1769ba9 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs @@ -8,7 +8,12 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client.Models; using BTCPayServer.Common; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.HostedServices; using BTCPayServer.Payments.PayJoin; +using BTCPayServer.Services; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; @@ -16,29 +21,39 @@ using WalletWasabi.Bases; using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.WabiSabi.Client; using WalletWasabi.Wallets; +using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest; +using PayoutData = BTCPayServer.Data.PayoutData; namespace BTCPayServer.Plugins.Wabisabi; public class WalletProvider : PeriodicRunner,IWalletProvider { private Dictionary? _cachedSettings; + private readonly IServiceProvider _serviceProvider; + private readonly IStoreRepository _storeRepository; private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; private readonly IExplorerClientProvider _explorerClientProvider; public IUTXOLocker UtxoLocker { get; set; } private readonly ILoggerFactory _loggerFactory; + private readonly EventAggregator _eventAggregator; - public WalletProvider(IStoreRepository storeRepository, IBTCPayServerClientFactory btcPayServerClientFactory, - IExplorerClientProvider explorerClientProvider, ILoggerFactory loggerFactory, IUTXOLocker utxoLocker ) : base(TimeSpan.FromMinutes(5)) + public WalletProvider( + IServiceProvider serviceProvider, + IStoreRepository storeRepository, + IBTCPayServerClientFactory btcPayServerClientFactory, + IExplorerClientProvider explorerClientProvider, + ILoggerFactory loggerFactory, + IUTXOLocker utxoLocker, + EventAggregator eventAggregator ) : base(TimeSpan.FromMinutes(5)) { UtxoLocker = utxoLocker; + _serviceProvider = serviceProvider; + _storeRepository = storeRepository; _btcPayServerClientFactory = btcPayServerClientFactory; _explorerClientProvider = explorerClientProvider; _loggerFactory = loggerFactory; - initialLoad = Task.Run(async () => - { - _cachedSettings = - await storeRepository.GetSettingsAsync(nameof(WabisabiStoreSettings)); - }); + _eventAggregator = eventAggregator; + } public readonly ConcurrentDictionary> LoadedWallets = new(); @@ -80,14 +95,17 @@ public class WalletProvider : PeriodicRunner,IWalletProvider var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey); - var destinationProvider = - new NBXInternalDestinationProvider(explorerClient, _btcPayServerClientFactory, derivationStrategy, client, name, - wabisabiStoreSettings); - var smartifier = new Smartifier(explorerClient, derivationStrategy, _btcPayServerClientFactory, name, + var smartifier = new Smartifier(_serviceProvider.GetRequiredService(),explorerClient, derivationStrategy, _btcPayServerClientFactory, name, CoinOnPropertyChanged); - return (IWallet)new BTCPayWallet(pm, derivationStrategy, explorerClient, keychain, destinationProvider, + return (IWallet)new BTCPayWallet( + _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService().GetWallet("BTC"), + _serviceProvider.GetRequiredService(), + pm, derivationStrategy, explorerClient, keychain, _btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker, _loggerFactory, smartifier, BannedCoins); @@ -96,6 +114,8 @@ public class WalletProvider : PeriodicRunner,IWalletProvider } private Task initialLoad = null; + private IEventAggregatorSubscription _subscription; + public async Task> GetWalletsAsync() { var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); @@ -135,26 +155,31 @@ public class WalletProvider : PeriodicRunner,IWalletProvider public async Task ResetWabisabiStuckPayouts() { var wallets = await GetWalletsAsync(); - foreach (BTCPayWallet wallet in wallets) + + var pullPaymentHostedService = _serviceProvider.GetRequiredService(); + var payouts = await pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery() { - var client = await _btcPayServerClientFactory.Create(null, wallet.StoreId); - var payouts = await client.GetStorePayouts(wallet.StoreId); - var inProgressPayouts = payouts.Where(data => - data.State == PayoutState.InProgress && data.PaymentMethod == "BTC" && - data.PaymentProof?.Value("proofType") == "Wabisabi"); - foreach (PayoutData payout in inProgressPayouts) + States = new PayoutState[] { - try + PayoutState.InProgress + }, + PaymentMethods = new[] {"BTC"}, + Stores = wallets.Select(wallet => ((BTCPayWallet) wallet).StoreId).ToArray() + }); + var inProgressPayouts = payouts + .Where(data => data.GetProofBlobJson()?.Value("proofType") == "Wabisabi").ToArray(); + foreach (PayoutData payout in inProgressPayouts) + { + try + { + await pullPaymentHostedService.MarkPaid(new HostedServices.MarkPayoutRequest() { - var paymentproof = - payout.PaymentProof.ToObject(); - if (paymentproof.Candidates?.Any() is not true) - await client.MarkPayout(wallet.StoreId, payout.Id, - new MarkPayoutRequest() {State = PayoutState.AwaitingPayment}); - } - catch (Exception e) - { - } + State = PayoutState.AwaitingPayment, + PayoutId = payout.Id + }); + } + catch (Exception e) + { } } } @@ -254,4 +279,22 @@ public class WalletProvider : PeriodicRunner,IWalletProvider return offsets; }); } + + public override Task StartAsync(CancellationToken cancellationToken) + { + initialLoad = Task.Run(async () => + { + _cachedSettings = + await _storeRepository.GetSettingsAsync(nameof(WabisabiStoreSettings)); + }, cancellationToken); + _subscription = _eventAggregator.SubscribeAsync(@event => + Check(@event.WalletId.StoreId, cancellationToken)); + return base.StartAsync(cancellationToken); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _subscription?.Dispose(); + return base.StopAsync(cancellationToken); + } } diff --git a/submodules/walletwasabi b/submodules/walletwasabi index 36b7fb4..9fa9947 160000 --- a/submodules/walletwasabi +++ b/submodules/walletwasabi @@ -1 +1 @@ -Subproject commit 36b7fb4566e5a4bc4c2931ab35c949b30d38c4b7 +Subproject commit 9fa9947c67f64dbf10b8b6feb497a4be67b5931b
Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value. Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy.
Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.
Whether to allow mixed coins to be mixed within different coordinators for greater privacy (Warning: This will make your coins to lose the free remix within the same coordinator)
Percentage probability of joining a round even if you have no payments to batch and all coins are private (Warning: a high probability will quickly eat up your balance in mining fees)