using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading; 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 BTCPayServer.Services.Stores; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; using NBXplorer.Models; using WalletWasabi.Bases; using WalletWasabi.Blockchain.Analysis; 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 StoreRepository _storeRepository; private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; private readonly IExplorerClientProvider _explorerClientProvider; public IUTXOLocker UtxoLocker { get; set; } private readonly ILoggerFactory _loggerFactory; private readonly EventAggregator _eventAggregator; public WalletProvider( IServiceProvider serviceProvider, StoreRepository 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; _eventAggregator = eventAggregator; } public readonly ConcurrentDictionary> LoadedWallets = new(); public ConcurrentDictionary> BannedCoins = new(); public class WalletUnloadEventArgs : EventArgs { public string Wallet { get; } public WalletUnloadEventArgs(string wallet) { Wallet = wallet; } } public event EventHandler? WalletUnloaded; public async Task GetWalletAsync(string name) { await initialLoad; return await LoadedWallets.GetOrAddAsync(name, async s => { if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings)) { return null; } var client = await _btcPayServerClientFactory.Create(null, name); var pm = await client.GetStoreOnChainPaymentMethod(name, "BTC"); var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); var derivationStrategy = explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); var masterKey = await explorerClient.GetMetadataAsync(derivationStrategy, WellknownMetadataKeys.MasterHDKey); var accountKey = await explorerClient.GetMetadataAsync(derivationStrategy, WellknownMetadataKeys.AccountHDKey); var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey); var smartifier = new Smartifier(_serviceProvider.GetRequiredService(),explorerClient, derivationStrategy, _btcPayServerClientFactory, name, CoinOnPropertyChanged); return (IWallet)new BTCPayWallet( _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService().GetWallet("BTC"), _serviceProvider.GetRequiredService(), pm, derivationStrategy, explorerClient, keychain, _btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker, _loggerFactory, smartifier, _serviceProvider.GetRequiredService(), BannedCoins); }); } private Task initialLoad = null; private IEventAggregatorSubscription _subscription; private IEventAggregatorSubscription _subscription2; public async Task> GetWalletsAsync() { var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); var status = await explorerClient.GetStatusAsync(); if (!status.IsFullySynched) { return Array.Empty(); } await initialLoad; return (await Task.WhenAll(_cachedSettings .Select(pair => GetWalletAsync(pair.Key)))) .Where(wallet => wallet is not null); } private void CoinOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (sender is SmartCoin smartCoin) { if (e.PropertyName == nameof(SmartCoin.CoinJoinInProgress)) { // _logger.LogInformation($"{smartCoin.Outpoint}.CoinJoinInProgress = {smartCoin.CoinJoinInProgress}"); if (UtxoLocker is not null) { _ = (smartCoin.CoinJoinInProgress ? UtxoLocker.TryLock(smartCoin.Outpoint) : UtxoLocker.TryUnlock(smartCoin.Outpoint)).ContinueWith(task => { // _logger.LogInformation( // $"{(task.Result ? "Success" : "Fail")}: {(smartCoin.CoinJoinInProgress ? "" : "un")}locking coin for coinjoin: {smartCoin.Outpoint} "); }); } } } } public async Task ResetWabisabiStuckPayouts() { var wallets = await GetWalletsAsync(); var pullPaymentHostedService = _serviceProvider.GetRequiredService(); var payouts = await pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery() { States = new PayoutState[] { 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() { State = PayoutState.AwaitingPayment, PayoutId = payout.Id }); } catch (Exception e) { } } } protected override async Task ActionAsync(CancellationToken cancel) { var toCheck = LoadedWallets.Keys.ToList(); while (toCheck.Any()) { var storeid = toCheck.First(); await Check(storeid, cancel); toCheck.Remove(storeid); } } public async Task Check(string storeId, CancellationToken cancellationToken) { var client = await _btcPayServerClientFactory.Create(null, storeId); try { if (LoadedWallets.TryGetValue(storeId, out var currentWallet)) { var wallet = (BTCPayWallet)await currentWallet; var kc = (BTCPayKeyChain)wallet.KeyChain; var pm = await client.GetStoreOnChainPaymentMethod(storeId, "BTC", cancellationToken); if (pm.DerivationScheme != wallet.OnChainPaymentMethodData.DerivationScheme) { await UnloadWallet(storeId); } else { wallet.OnChainPaymentMethodData = pm; } if (!kc.KeysAvailable) { await UnloadWallet(storeId); } } } catch (Exception e) { await UnloadWallet(storeId); } } private async Task UnloadWallet(string name) { LoadedWallets.TryRemove(name, out _); WalletUnloaded?.Invoke(this, new WalletUnloadEventArgs(name)); } public async Task SettingsUpdated(string storeId, WabisabiStoreSettings wabisabiSettings) { if (wabisabiSettings.Settings.All(settings => !settings.Enabled)) { _cachedSettings?.Remove(storeId); await UnloadWallet(storeId); }else if (LoadedWallets.TryGetValue(storeId, out var existingWallet)) { _cachedSettings.AddOrReplace(storeId, wabisabiSettings); var btcpayWalet = (BTCPayWallet) await existingWallet; if (btcpayWalet is null) { LoadedWallets.TryRemove(storeId, out _); var w = await GetWalletAsync(storeId); if (w is null) { await UnloadWallet(storeId); } } else { btcpayWalet.WabisabiStoreSettings = wabisabiSettings; } } else { _cachedSettings.AddOrReplace(storeId, wabisabiSettings); await GetWalletAsync(storeId); } } public void OnBan(string coordinatorName, BannedCoinEventArgs args) { BannedCoins.AddOrUpdate(coordinatorName, s => new Dictionary() {{args.Utxo, args.BannedTime}}, (s, offsets) => { offsets.TryAdd(args.Utxo, args.BannedTime); 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)); _subscription2 = _eventAggregator.SubscribeAsync(HandleNewTransaction); return base.StartAsync(cancellationToken); } private Task HandleNewTransaction(NewTransactionEvent arg) { throw new NotImplementedException(); } public override Task StopAsync(CancellationToken cancellationToken) { _subscription?.Dispose(); _subscription2?.Dispose(); return base.StopAsync(cancellationToken); } }