using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Common; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.HostedServices; using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payouts; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Stores; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; using WalletWasabi.Bases; using WalletWasabi.WabiSabi.Client; using WalletWasabi.Wallets; using PayoutData = BTCPayServer.Data.PayoutData; namespace BTCPayServer.Plugins.Wabisabi; public class WalletProvider : PeriodicRunner, IWalletProvider { private ConcurrentDictionary? _cachedSettings; private readonly IServiceProvider _serviceProvider; private readonly StoreRepository _storeRepository; private readonly IExplorerClientProvider _explorerClientProvider; public IUTXOLocker UtxoLocker { get; set; } private readonly ILoggerFactory _loggerFactory; private readonly EventAggregator _eventAggregator; private readonly ILogger _logger; private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; private readonly IMemoryCache _memoryCache; public readonly ConcurrentDictionary>> LoadedWallets = new(); private readonly TaskCompletionSource _initialLoad = new(); private readonly CompositeDisposable _disposables = new(); public WalletProvider( IServiceProvider serviceProvider, StoreRepository storeRepository, IExplorerClientProvider explorerClientProvider, ILoggerFactory loggerFactory, IUTXOLocker utxoLocker, EventAggregator eventAggregator, ILogger logger, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, IMemoryCache memoryCache) : base(TimeSpan.FromMinutes(5)) { UtxoLocker = utxoLocker; _serviceProvider = serviceProvider; _storeRepository = storeRepository; _explorerClientProvider = explorerClientProvider; _loggerFactory = loggerFactory; _eventAggregator = eventAggregator; _logger = logger; _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _memoryCache = memoryCache; } public async Task GetWalletAsync(string name) { await _initialLoad.Task; return await Smartifier.GetOrCreate(LoadedWallets, name, async () => { if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings)) { return null; } var store = await _storeRepository.FindStore(name); var paymentMethod = store?.GetDerivationSchemeSettings(_paymentMethodHandlerDictionary, "BTC"); if (paymentMethod is null) { return null; } var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId("BTC"); var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); var isHotWallet = paymentMethod.IsHotWallet; var enabled = store.GetEnabledPaymentIds().Contains(pmi); var derivationStrategy = paymentMethod.AccountDerivation; BTCPayKeyChain keychain; var accountKeyPath = paymentMethod.AccountKeySettings.FirstOrDefault()?.GetRootedKeyPath(); if (isHotWallet && enabled) { var masterKey = await explorerClient.GetMetadataAsync(derivationStrategy, WellknownMetadataKeys.MasterHDKey); var accountKey = await explorerClient.GetMetadataAsync(derivationStrategy, WellknownMetadataKeys.AccountHDKey); var accountKeyPath2 = await explorerClient.GetMetadataAsync(derivationStrategy, WellknownMetadataKeys.AccountKeyPath); accountKeyPath = accountKeyPath2 ?? accountKeyPath; var smartifier = new Smartifier(_memoryCache, _logger, _serviceProvider.GetRequiredService(), explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath); if (masterKey is null || accountKey is null || accountKeyPath is null) { keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, smartifier); } else keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey, smartifier); } else { var smartifier = new Smartifier(_memoryCache, _logger, _serviceProvider.GetRequiredService(), explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath); keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, smartifier); } var payoutMethodId = PayoutTypes.CHAIN.GetPayoutMethodId("BTC"); return new BTCPayWallet( _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService()[payoutMethodId], _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService().GetWallet("BTC"), _serviceProvider.GetRequiredService(), derivationStrategy, explorerClient, keychain, name, wabisabiStoreSettings, UtxoLocker, _loggerFactory, _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService() ); }, _logger); } public async Task> GetWalletsAsync() { var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); var status = await explorerClient.GetStatusAsync(); if (!status.IsFullySynched) { return Array.Empty(); } await _initialLoad.Task; return (await Task.WhenAll(_cachedSettings .Select(pair => GetWalletAsync(pair.Key)))) .Where(wallet => wallet is not null); } public async Task ResetWabisabiStuckPayouts(string[] storeIds) { await _initialLoad.Task; storeIds ??= _cachedSettings?.Keys.ToArray() ?? Array.Empty(); if (!storeIds.Any()) { return; } var pullPaymentHostedService = _serviceProvider.GetRequiredService(); var payouts = await pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery() { States = new[] { PayoutState.InProgress }, PayoutMethods = new[] {PayoutTypes.CHAIN.GetPayoutMethodId("BTC").ToString()}, Stores = storeIds }); var inProgressPayouts = payouts .Where(data => data.GetProofBlobJson()?.Value("proofType") == "Wabisabi").ToArray(); if (!inProgressPayouts.Any()) return; _logger.LogInformation("Moving {count} stuck coinjoin payouts to AwaitingPayment", inProgressPayouts.Length); 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) { try { if (LoadedWallets.TryGetValue(storeId, out var currentWallet)) { await UnloadWallet(storeId); if (_cachedSettings.TryGetValue(storeId, out var settings) && settings.Settings.Any(coordinatorSettings => coordinatorSettings.Enabled)) await GetWalletAsync(storeId); await GetWalletAsync(storeId); } } catch (Exception e) { await UnloadWallet(storeId); } } private async Task UnloadWallet(string name) { LoadedWallets.TryRemove(name, out var walletTask); } public async Task SettingsUpdated(string storeId, WabisabiStoreSettings wabisabiSettings) { _cachedSettings.AddOrReplace(storeId, wabisabiSettings); if (!wabisabiSettings.Active || wabisabiSettings.Settings.All(settings => !settings.Enabled)) { await UnloadWallet(storeId); } if (LoadedWallets.TryGetValue(storeId, out var existingWallet)) { var btcpayWallet = await existingWallet.Value; if (btcpayWallet is null) { LoadedWallets.TryRemove(storeId, out _); } else { btcpayWallet.WabisabiStoreSettings = wabisabiSettings; } } var w = await GetWalletAsync(storeId); if (w is null) { await UnloadWallet(storeId); } } public override Task StartAsync(CancellationToken cancellationToken) { Task.Run(async () => { _cachedSettings = new ConcurrentDictionary( (await _storeRepository.GetSettingsAsync(nameof(WabisabiStoreSettings)))); _initialLoad.SetResult(); }, cancellationToken); _disposables.Add(_eventAggregator.SubscribeAsync(async @event => { await _initialLoad.Task; await UnloadWallet(@event.StoreId); })); _disposables.Add(_eventAggregator.SubscribeAsync(async @event => { if (@event.WalletId.CryptoCode == "BTC") { await _initialLoad.Task; await Check(@event.WalletId.StoreId, cancellationToken); } })); return base.StartAsync(cancellationToken); } public override Task StopAsync(CancellationToken cancellationToken) { _disposables?.Dispose(); return base.StopAsync(cancellationToken); } }