add address entry dialog
ADD
WalletWasabi.Fluent/ViewModels/Dialogs/AddressEntryDialogViewModel.cs
+using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Threading;
+using NBitcoin;
+using NBitcoin.Payment;
+using ReactiveUI;
+using WalletWasabi.Blockchain.Analysis.Clustering;
+using WalletWasabi.Fluent.Models;
+using WalletWasabi.Fluent.Validation;
+using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
+using WalletWasabi.Fluent.ViewModels.Wallets.Send;
+using WalletWasabi.Models;
+using WalletWasabi.Userfacing;
+
+namespace WalletWasabi.Fluent.ViewModels.Dialogs;
+
+[NavigationMetaData(Title = "Address")]
+public partial class AddressEntryDialogViewModel : DialogViewModelBase<BitcoinAddress?>
+{
+ private readonly Network _network;
+ private readonly TransactionInfo _info;
+ [AutoNotify] private string _to = "";
+
+ private bool _parsingUrl;
+ private bool _payJoinUrlFound;
+ private bool _amountUrlFound;
+
+ public AddressEntryDialogViewModel(Network network, TransactionInfo info)
+ {
+ _network = network;
+ _info = info;
+ IsQrButtonVisible = WebcamQrReader.IsOsPlatformSupported;
+
+ this.ValidateProperty(x => x.To, ValidateToField);
+
+ this.WhenAnyValue(x => x.To)
+ .Skip(1)
+ .Subscribe(ParseToField);
+
+ PasteCommand = ReactiveCommand.CreateFromTask(async () => await OnPasteAsync());
+ AutoPasteCommand = ReactiveCommand.CreateFromTask(async () => await OnAutoPasteAsync());
+ QrCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ ShowQrCameraDialogViewModel dialog = new(_network);
+ var result = await NavigateDialogAsync(dialog, NavigationTarget.CompactDialogScreen);
+ if (!string.IsNullOrWhiteSpace(result.Result))
+ {
+ To = result.Result;
+ }
+ });
+
+ var nextCommandCanExecute =
+ this.WhenAnyValue(x => x.To)
+ .Select(to =>
+ {
+ var addressFilled = !string.IsNullOrEmpty(to);
+ var hasError = Validations.Any;
+
+ return addressFilled && !hasError;
+ });
+
+ NextCommand = ReactiveCommand.Create(() => Close(DialogResultKind.Normal, BitcoinAddress.Create(To, _network)), nextCommandCanExecute);
+ }
+
+ public bool IsQrButtonVisible { get; }
+
+ public ICommand PasteCommand { get; }
+
+ public ICommand AutoPasteCommand { get; }
+
+ public ICommand QrCommand { get; }
+
+ private void ParseToField(string s)
+ {
+ if (_parsingUrl)
+ {
+ return;
+ }
+
+ _parsingUrl = true;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ TryParseUrl(s);
+ _parsingUrl = false;
+ });
+ }
+
+ private async Task OnAutoPasteAsync()
+ {
+ var isAutoPasteEnabled = Services.UiConfig.AutoPaste;
+
+ if (string.IsNullOrEmpty(To) && isAutoPasteEnabled)
+ {
+ await OnPasteAsync(pasteIfInvalid: false);
+ }
+ }
+
+ private async Task OnPasteAsync(bool pasteIfInvalid = true)
+ {
+ if (Application.Current is { Clipboard: { } clipboard })
+ {
+ var text = await clipboard.GetTextAsync();
+
+ _parsingUrl = true;
+
+ if (!TryParseUrl(text) && pasteIfInvalid)
+ {
+ To = text;
+ }
+
+ _parsingUrl = false;
+ }
+ }
+
+ private bool TryParseUrl(string? text)
+ {
+ if (text is null || text.IsTrimmable())
+ {
+ return false;
+ }
+
+ bool result = false;
+
+ if (AddressStringParser.TryParse(text, _network, out BitcoinUrlBuilder? url))
+ {
+ result = true;
+ if (url.Label is { } label)
+ {
+ _info.UserLabels = new SmartLabel(label);
+ }
+
+ _payJoinUrlFound = url.UnknownParameters.TryGetValue("pj", out _);
+
+ if (url.Address is { })
+ {
+ To = url.Address.ToString();
+ }
+
+ _amountUrlFound = url.Amount is { };
+ }
+ else
+ {
+ _payJoinUrlFound = false;
+ _amountUrlFound = false;
+ }
+
+ return result;
+ }
+
+ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
+ {
+ base.OnNavigatedTo(isInHistory, disposables);
+
+ RxApp.MainThreadScheduler.Schedule(async () => await OnAutoPasteAsync());
+ }
+
+ private void ValidateToField(IValidationErrors errors)
+ {
+ if (!string.IsNullOrEmpty(To) && (To.IsTrimmable() || !AddressStringParser.TryParse(To, _network, out _)))
+ {
+ errors.Add(ErrorSeverity.Error, "Input a valid BTC address or URL.");
+ }
+ else if (_payJoinUrlFound)
+ {
+ errors.Add(ErrorSeverity.Error, "Payjoin is not possible.");
+ }
+ else if (_amountUrlFound)
+ {
+ errors.Add(ErrorSeverity.Error, "Setting the amount is not possible.");
+ }
+ }
+}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Advanced/WalletCoins/WalletCoinsViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Advanced/WalletCoins/WalletCoinsViewModel.cs
@@ -47,8 +47,12 @@ public partial class WalletCoinsViewModel : RoutableViewModel
var selectedSmartCoins = _coins.Where(x => x.IsSelected).Select(x => x.Coin).ToImmutableArray();
var info = new TransactionInfo(wallet.KeyManager.AnonScoreTarget);
- // TODO: Ask for address
- var address = BitcoinAddress.Create("tb1qmmsk4y4guxl0q2f5e7t485ssq4f7ju899tk6pj", wallet.Network);
+ var addressDialog = new AddressEntryDialogViewModel(wallet.Network, info);
+ var addressResult = await NavigateDialogAsync(addressDialog, NavigationTarget.CompactDialogScreen);
+ if (addressResult.Result is not { } address)
+ {
+ return;
+ }
var labelDialog = new LabelEntryDialogViewModel(wallet, info);
var result = await NavigateDialogAsync(labelDialog, NavigationTarget.CompactDialogScreen);

ADD
WalletWasabi.Fluent/Views/Dialogs/AddressEntryDialogView.axaml
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:c="using:WalletWasabi.Fluent.Controls"
+ xmlns:behaviors="using:WalletWasabi.Fluent.Behaviors"
+ xmlns:i="using:Avalonia.Xaml.Interactivity"
+ xmlns:dialogs="clr-namespace:WalletWasabi.Fluent.ViewModels.Dialogs"
+ mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="270"
+ x:DataType="dialogs:AddressEntryDialogViewModel"
+ x:CompileBindings="True"
+ x:Class="WalletWasabi.Fluent.Views.Dialogs.AddressEntryDialogView">
+ <c:ContentArea Width="515" Height="300"
+ Title="{Binding Title}"
+ CancelContent="Cancel"
+ EnableCancel="{Binding EnableCancel}"
+ EnableBack="{Binding EnableBack}"
+ EnableNext="True" NextContent="Continue">
+ <c:ContentArea.Styles>
+ <Style Selector="c|AnimatedButton.flash">
+ <Setter Property="InitialOpacity" Value="0.8" />
+ </Style>
+ <Style Selector="c|AnimatedButton.flash /template/ PathIcon#PART_IconNormal">
+ <Setter Property="Foreground" Value="{DynamicResource SystemAccentColor}" />
+ <Style.Animations>
+ <Animation Duration="0.25" IterationCount="6"
+ PlaybackDirection="AlternateReverse"
+ Easing="{StaticResource FluentEasing}">
+ <KeyFrame KeyTime="0:0:0.0">
+ <Setter Property="Opacity" Value="0.1" />
+ </KeyFrame>
+ <KeyFrame KeyTime="0:0:0.25">
+ <Setter Property="Opacity" Value="1" />
+ </KeyFrame>
+ </Animation>
+ </Style.Animations>
+ </Style>
+ </c:ContentArea.Styles>
+ <TextBox x:Name="toTb" MaxLength="250" Text="{Binding To}" Classes="monoSpaced"
+ Watermark="Enter the destination bitcoin address"
+ AcceptsReturn="False"
+ VerticalAlignment="Top">
+ <i:Interaction.Behaviors>
+ <behaviors:FocusOnAttachedBehavior />
+ <behaviors:ExecuteCommandOnActivated Command="{Binding AutoPasteCommand}" />
+ </i:Interaction.Behaviors>
+ <TextBox.InnerRightContent>
+ <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="10" Margin="10 0">
+ <c:AnimatedButton KeyboardNavigation.IsTabStop="False"
+ Focusable="False"
+ Command="{Binding PasteCommand}"
+ ToolTip.Tip="Paste"
+ NormalIcon="{StaticResource paste_regular}"
+ ClickIcon="{StaticResource copy_confirmed}">
+ <i:Interaction.Behaviors>
+ <behaviors:PasteButtonFlashBehavior FlashAnimation="flash" CurrentAddress="{Binding To}" />
+ </i:Interaction.Behaviors>
+ </c:AnimatedButton>
+ <c:AnimatedButton IsVisible="{Binding IsQrButtonVisible}"
+ KeyboardNavigation.IsTabStop="False"
+ Focusable="False"
+ Command="{Binding QrCommand}"
+ ToolTip.Tip="Scan QR Code"
+ NormalIcon="{StaticResource qr_code_regular}" />
+ </StackPanel>
+ </TextBox.InnerRightContent>
+ </TextBox>
+ </c:ContentArea>
+</UserControl>

ADD
WalletWasabi.Fluent/Views/Dialogs/AddressEntryDialogView.axaml.cs
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace WalletWasabi.Fluent.Views.Dialogs;
+
+public class AddressEntryDialogView : UserControl
+{
+ public AddressEntryDialogView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}

add address entry dialog