diff --git a/WalletWasabi.Fluent/Models/Wallets/Address.cs b/WalletWasabi.Fluent/Models/Wallets/Address.cs index 46e08e1f12e..dae0a11e147 100644 --- a/WalletWasabi.Fluent/Models/Wallets/Address.cs +++ b/WalletWasabi.Fluent/Models/Wallets/Address.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Threading; using System.Threading.Tasks; using NBitcoin; @@ -12,13 +11,16 @@ namespace WalletWasabi.Fluent.Models.Wallets; public class Address : ReactiveObject, IAddress { - public Address(KeyManager keyManager, HdPubKey hdPubKey) + private readonly Action
_onHide; + + public Address(KeyManager keyManager, HdPubKey hdPubKey, Action
onHide) { KeyManager = keyManager; HdPubKey = hdPubKey; Network = keyManager.GetNetwork(); HdFingerprint = KeyManager.MasterFingerprint; BitcoinAddress = HdPubKey.GetAddress(Network); + _onHide = onHide; } public KeyManager KeyManager { get; } @@ -31,15 +33,9 @@ public Address(KeyManager keyManager, HdPubKey hdPubKey) public KeyPath FullKeyPath => HdPubKey.FullKeyPath; public string Text => BitcoinAddress.ToString(); - private bool IsUnused => Labels.Any() && !HdPubKey.IsInternal && HdPubKey.KeyState == KeyState.Clean; - - public bool IsUsed => !IsUnused; - public void Hide() { - KeyManager.SetKeyState(KeyState.Locked, HdPubKey); - KeyManager.ToFile(); - this.RaisePropertyChanged(nameof(IsUsed)); + _onHide(this); } public void SetLabels(LabelsArray labels) @@ -77,4 +73,13 @@ public async Task ShowOnHwWalletAsync() throw; } } + + public override int GetHashCode() => Text.GetHashCode(); + + public override bool Equals(object? obj) + { + return obj is IAddress address && Equals(address); + } + + protected bool Equals(IAddress other) => Text.Equals(other.Text); } diff --git a/WalletWasabi.Fluent/Models/Wallets/AddressesModel.cs b/WalletWasabi.Fluent/Models/Wallets/AddressesModel.cs index 8eaf74521cd..ca77b06e555 100644 --- a/WalletWasabi.Fluent/Models/Wallets/AddressesModel.cs +++ b/WalletWasabi.Fluent/Models/Wallets/AddressesModel.cs @@ -1,47 +1,75 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; -using System.Reactive; -using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; using DynamicData; using WalletWasabi.Blockchain.Keys; -using WalletWasabi.Fluent.Extensions; +using WalletWasabi.Blockchain.TransactionProcessing; +using WalletWasabi.Wallets; namespace WalletWasabi.Fluent.Models.Wallets; [AutoInterface] -public partial class AddressesModel : IDisposable +public partial class AddressesModel { - private readonly CompositeDisposable _disposable = new(); - private readonly KeyManager _keyManager; + private readonly ISubject _newAddressGenerated = new Subject(); + private readonly Wallet _wallet; + private readonly SourceList _source; - public AddressesModel(IObservable addressesUpdated, KeyManager keyManager) + public AddressesModel(Wallet wallet) { - _keyManager = keyManager; + _wallet = wallet; + _source = new SourceList(); + _source.AddRange(GetUnusedKeys()); - Cache = - addressesUpdated.Fetch(GetAddresses, address => address.Text) - .DisposeWith(_disposable); + Observable.FromEventPattern( + h => wallet.WalletRelevantTransactionProcessed += h, + h => wallet.WalletRelevantTransactionProcessed -= h) + .Do(_ => UpdateUnusedKeys()) + .Subscribe(); - UnusedAddressesCache = - Cache.Connect() - .AutoRefresh(x => x.IsUsed) - .Filter(x => !x.IsUsed) - .AsObservableCache() - .DisposeWith(_disposable); + _newAddressGenerated + .Do(address => _source.Add(address)) + .Subscribe(); - HasUnusedAddresses = UnusedAddressesCache.NotEmpty(); + _source.Connect() + .Transform(key => (IAddress) new Address(_wallet.KeyManager, key, Hide)) + .Bind(out var unusedAddresses) + .Subscribe(); + + Unused = unusedAddresses; } - public IObservableCache Cache { get; } + private IEnumerable GetUnusedKeys() => _wallet.KeyManager.GetKeys(x => x is { IsInternal: false, KeyState: KeyState.Clean, Labels.Count: > 0 }); - public IObservableCache UnusedAddressesCache { get; } + public IAddress NextReceiveAddress(IEnumerable destinationLabels) + { + var pubKey = _wallet.GetNextReceiveAddress(destinationLabels); + var nextReceiveAddress = new Address(_wallet.KeyManager, pubKey, Hide); + _newAddressGenerated.OnNext(pubKey); - public IObservable HasUnusedAddresses { get; } + return nextReceiveAddress; + } - public void Dispose() => _disposable.Dispose(); + public ReadOnlyObservableCollection Unused { get; } - private IEnumerable GetAddresses() => _keyManager - .GetKeys() - .Reverse() - .Select(x => new Address(_keyManager, x)); + public void Hide(Address address) + { + _wallet.KeyManager.SetKeyState(KeyState.Locked, address.HdPubKey); + _wallet.KeyManager.ToFile(); + _source.Remove(address.HdPubKey); + } + + private void UpdateUnusedKeys() + { + var itemsToRemove = _source.Items + .Where(item => item.KeyState != KeyState.Clean) + .ToList(); + + foreach (var item in itemsToRemove) + { + _source.Remove(item); + } + } } diff --git a/WalletWasabi.Fluent/Models/Wallets/IAddress.cs b/WalletWasabi.Fluent/Models/Wallets/IAddress.cs index ccbd658d7b3..e57594f6e0c 100644 --- a/WalletWasabi.Fluent/Models/Wallets/IAddress.cs +++ b/WalletWasabi.Fluent/Models/Wallets/IAddress.cs @@ -14,8 +14,6 @@ public interface IAddress : IReactiveObject LabelsArray Labels { get; } - bool IsUsed { get; } - void Hide(); void SetLabels(LabelsArray labels); diff --git a/WalletWasabi.Fluent/Models/Wallets/WalletModel.cs b/WalletWasabi.Fluent/Models/Wallets/WalletModel.cs index 1aae528bd95..0555cda0cd9 100644 --- a/WalletWasabi.Fluent/Models/Wallets/WalletModel.cs +++ b/WalletWasabi.Fluent/Models/Wallets/WalletModel.cs @@ -1,20 +1,16 @@ using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Reactive.Linq; using System.Reactive.Subjects; using NBitcoin; using ReactiveUI; -using WalletWasabi.Fluent.Extensions; using WalletWasabi.Fluent.Helpers; using WalletWasabi.Fluent.ViewModels.Wallets.Labels; using WalletWasabi.Wallets; namespace WalletWasabi.Fluent.Models.Wallets; -public partial interface IWalletModel : INotifyPropertyChanged -{ -} +public partial interface IWalletModel : INotifyPropertyChanged; [AutoInterface] public partial class WalletModel : ReactiveObject @@ -39,7 +35,7 @@ public WalletModel(Wallet wallet, IAmountProvider amountProvider) Transactions = new WalletTransactionsModel(this, wallet); - AddressesModel = new AddressesModel(Transactions.TransactionProcessed.ToSignal().Merge(_newAddressGenerated.ToSignal()), Wallet.KeyManager); + Addresses = new AddressesModel(Wallet); State = Observable.FromEventPattern(Wallet, nameof(Wallet.StateChanged)) @@ -69,7 +65,7 @@ public WalletModel(Wallet wallet, IAmountProvider amountProvider) this.WhenAnyValue(x => x.Auth.IsLoggedIn).BindTo(this, x => x.IsLoggedIn); } - public IAddressesModel AddressesModel { get; } + public IAddressesModel Addresses { get; } internal Wallet Wallet { get; } @@ -120,15 +116,6 @@ public IWalletInfoModel GetWalletInfo() return new WalletInfoModel(Wallet); } - public IAddress GetNextReceiveAddress(IEnumerable destinationLabels) - { - var pubKey = Wallet.GetNextReceiveAddress(destinationLabels); - var nextReceiveAddress = new Address(Wallet.KeyManager, pubKey); - _newAddressGenerated.OnNext(nextReceiveAddress); - - return nextReceiveAddress; - } - public void Rename(string newWalletName) { Services.WalletManager.RenameWallet(Wallet, newWalletName); diff --git a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/AddressViewModel.cs b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/AddressViewModel.cs index 9a75b16d569..f2e647119cf 100644 --- a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/AddressViewModel.cs +++ b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/AddressViewModel.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Reactive; using System.Threading.Tasks; using System.Windows.Input; diff --git a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressViewModel.cs b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressViewModel.cs index aff19531e8c..fcf0388a127 100644 --- a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressViewModel.cs +++ b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressViewModel.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using System.Windows.Input; using DynamicData; +using DynamicData.Binding; using ReactiveUI; using WalletWasabi.Blockchain.Analysis.Clustering; using WalletWasabi.Fluent.Extensions; @@ -63,12 +64,17 @@ public ReceiveAddressViewModel(UiContext uiContext, IWalletModel wallet, IAddres protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables) { - _wallet.AddressesModel.Cache - .Connect() - .AutoRefresh(x => x.IsUsed) - .Watch(Model.Text) - .Where(change => change.Current.IsUsed) - .Do(_ => Navigate().Back()) + _wallet.Addresses.Unused + .ToObservableChangeSet() + .ObserveOn(RxApp.MainThreadScheduler) + .OnItemRemoved( + address => + { + if (Equals(address, Model)) + { + Navigate().BackTo(); + } + }) .Subscribe() .DisposeWith(disposables); diff --git a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressesViewModel.cs b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressesViewModel.cs index 7f222def701..839c792944e 100644 --- a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressesViewModel.cs +++ b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveAddressesViewModel.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using Avalonia.Controls; using DynamicData; +using DynamicData.Binding; using WalletWasabi.Fluent.Models.Wallets; -using WalletWasabi.Fluent.ViewModels.Dialogs.Base; using WalletWasabi.Fluent.ViewModels.Navigation; namespace WalletWasabi.Fluent.ViewModels.Wallets.Receive; @@ -26,15 +26,14 @@ private ReceiveAddressesViewModel(IWalletModel wallet) protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables) { - _wallet - .AddressesModel.UnusedAddressesCache - .Connect() + _wallet.Addresses.Unused + .ToObservableChangeSet() .Transform(CreateAddressViewModel) - .Bind(out var addresses) + .Bind(out var unusedAddresses) .Subscribe() .DisposeWith(disposables); - var source = ReceiveAddressesDataGridSource.Create(addresses); + var source = ReceiveAddressesDataGridSource.Create(unusedAddresses); Source = source; Source.RowSelection!.SingleSelect = true; @@ -45,7 +44,7 @@ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disp private AddressViewModel CreateAddressViewModel(IAddress address) { - return new AddressViewModel(UiContext, OnEditAddressAsync, address1 => OnShowAddressAsync(address1), address); + return new AddressViewModel(UiContext, OnEditAddressAsync, OnShowAddressAsync, address); } private void OnShowAddressAsync(IAddress a) diff --git a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveViewModel.cs b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveViewModel.cs index 9ddb1720cef..5bdf4fafbfe 100644 --- a/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveViewModel.cs +++ b/WalletWasabi.Fluent/ViewModels/Wallets/Receive/ReceiveViewModel.cs @@ -1,5 +1,7 @@ using System.Reactive.Linq; using System.Windows.Input; +using DynamicData.Binding; +using DynamicData.Aggregation; using ReactiveUI; using WalletWasabi.Fluent.Extensions; using WalletWasabi.Fluent.Models.Wallets; @@ -41,9 +43,7 @@ private ReceiveViewModel(IWalletModel wallet) ShowExistingAddressesCommand = ReactiveCommand.Create(OnShowExistingAddresses); - AddressesModel = wallet.AddressesModel; - - HasUnusedAddresses = _wallet.AddressesModel.HasUnusedAddresses.StartWith(false); + AddressesModel = wallet.Addresses; } public IAddressesModel AddressesModel { get; } @@ -52,12 +52,12 @@ private ReceiveViewModel(IWalletModel wallet) public ICommand ShowExistingAddressesCommand { get; } - public IObservable HasUnusedAddresses { get; } + public IObservable HasUnusedAddresses => _wallet.Addresses.Unused.ToObservableChangeSet().Count().Select(i => i > 0); private void OnNext() { SuggestionLabels.ForceAdd = true; - var address = _wallet.GetNextReceiveAddress(SuggestionLabels.Labels); + var address = _wallet.Addresses.NextReceiveAddress(SuggestionLabels.Labels); SuggestionLabels.Labels.Clear(); Navigate().To().ReceiveAddress(_wallet, address, Services.UiConfig.Autocopy); diff --git a/WalletWasabi.Fluent/Views/Wallets/Receive/ReceiveView.axaml b/WalletWasabi.Fluent/Views/Wallets/Receive/ReceiveView.axaml index 867e9f3884d..bb7ee67bc4c 100644 --- a/WalletWasabi.Fluent/Views/Wallets/Receive/ReceiveView.axaml +++ b/WalletWasabi.Fluent/Views/Wallets/Receive/ReceiveView.axaml @@ -16,7 +16,7 @@ EnableBack="{Binding EnableBack}">