feat(app): label watch-only send flow and export unsigned PSBT
Send already refused to enable Confirm for watch-only accounts (Signed false), but the App had no way to get the resulting PSBT out to sign elsewhere — only the CLI printed it. Adds a base64 PSBT box + copy button in the Send summary card, and a visible warning banner explaining the wallet cannot sign, on both desktop and mobile layouts.
This commit is contained in:
@@ -275,6 +275,10 @@ public sealed class Loc
|
||||
["send.sect.recipient"] = ["Destinatario", "Recipient", "Destinatario", "Destinataire", "Destinatário", "Empfänger", "收款人"],
|
||||
["send.sect.amount"] = ["Importo e commissione", "Amount & fee", "Importe y comisión", "Montant et frais", "Valor e taxa", "Betrag & Gebühr", "金额与手续费"],
|
||||
["send.summary"] = ["Riepilogo", "Summary", "Resumen", "Résumé", "Resumo", "Zusammenfassung", "摘要"],
|
||||
["send.psbt.label"] = ["PSBT non firmata (base64) — da firmare altrove", "Unsigned PSBT (base64) — sign it elsewhere", "PSBT sin firmar (base64) — fírmala en otro lugar", "PSBT non signée (base64) — à signer ailleurs", "PSBT não assinada (base64) — assine em outro lugar", "Unsignierte PSBT (base64) — anderswo signieren", "未签名 PSBT(base64)— 请在别处签名"],
|
||||
["send.psbt.copy"] = ["Copia PSBT", "Copy PSBT", "Copiar PSBT", "Copier la PSBT", "Copiar PSBT", "PSBT kopieren", "复制 PSBT"],
|
||||
["send.watchonly.hint"] = ["Wallet in sola lettura: non può firmare. Esporta la PSBT e firmala con un wallet che possiede le chiavi.", "Watch-only wallet: it cannot sign. Export the PSBT and sign it with a wallet that holds the keys.", "Wallet de solo lectura: no puede firmar. Exporta la PSBT y fírmala con un wallet que tenga las claves.", "Wallet en lecture seule : il ne peut pas signer. Exportez la PSBT et signez-la avec un wallet possédant les clés.", "Carteira somente leitura: não pode assinar. Exporte a PSBT e assine-a com uma carteira que tenha as chaves.", "Nur-Lese-Wallet: kann nicht signieren. Exportieren Sie die PSBT und signieren Sie sie mit einem Wallet, das die Schlüssel besitzt.", "仅观察钱包:无法签名。请导出 PSBT 并用持有密钥的钱包签名。"],
|
||||
["psbt.copied"] = ["PSBT copiata negli appunti", "PSBT copied to clipboard", "PSBT copiada al portapapeles", "PSBT copiée dans le presse-papiers", "PSBT copiada para a área de transferência", "PSBT in die Zwischenablage kopiert", "PSBT 已复制到剪贴板"],
|
||||
["receive.your.address"] = ["Il tuo indirizzo", "Your address", "Tu dirección", "Votre adresse", "Seu endereço", "Deine Adresse", "您的地址"],
|
||||
|
||||
// Wallet info overlay
|
||||
|
||||
@@ -31,6 +31,14 @@ public partial class MainWindowViewModel
|
||||
[ObservableProperty]
|
||||
private bool hasPendingSend;
|
||||
|
||||
[ObservableProperty]
|
||||
private string pendingPsbtBase64 = "";
|
||||
|
||||
/// <summary>True when the open account cannot sign — Send only prepares an unsigned PSBT to export.</summary>
|
||||
public bool IsWatchOnlyAccount => _account?.IsWatchOnly ?? false;
|
||||
|
||||
public void NotifyPsbtCopied() => StatusMessage = Loc.Tr("psbt.copied");
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ScanQr()
|
||||
{
|
||||
@@ -81,11 +89,13 @@ public partial class MainWindowViewModel
|
||||
$"({_pendingSend.Transaction.GetVirtualSize()} vB)" +
|
||||
(_pendingSend.Signed ? "" : Loc.Tr("msg.unsigned.watchonly"));
|
||||
HasPendingSend = _pendingSend.Signed;
|
||||
PendingPsbtBase64 = _pendingSend.Signed ? "" : _pendingSend.Psbt.ToBase64();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_pendingSend = null;
|
||||
HasPendingSend = false;
|
||||
PendingPsbtBase64 = "";
|
||||
SendPreview = $"{Loc.Tr("msg.error")}: {DescribeError(ex)}";
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
@@ -103,6 +113,7 @@ public partial class MainWindowViewModel
|
||||
SendTo = SendAmount = "";
|
||||
_pendingSend = null;
|
||||
HasPendingSend = false;
|
||||
PendingPsbtBase64 = "";
|
||||
await ConnectAndSync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -201,6 +201,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
_lastTransactions = null;
|
||||
_pendingSend = null;
|
||||
HasPendingSend = false;
|
||||
PendingPsbtBase64 = "";
|
||||
OnPropertyChanged(nameof(IsWatchOnlyAccount));
|
||||
History.Clear();
|
||||
Contacts.Clear();
|
||||
SelectedContactInList = null;
|
||||
|
||||
@@ -445,7 +445,7 @@
|
||||
|
||||
<!-- ── DESKTOP: Recipient | Amount side-by-side ── -->
|
||||
<Grid IsVisible="{Binding IsDesktop}"
|
||||
RowDefinitions="Auto,Auto,Auto" RowSpacing="14">
|
||||
RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="14">
|
||||
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,*" ColumnSpacing="14">
|
||||
<!-- Recipient card -->
|
||||
@@ -516,11 +516,29 @@
|
||||
<SelectableTextBlock Text="{Binding SendPreview}"
|
||||
TextWrapping="Wrap" FontSize="13" Classes="mono"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<StackPanel Spacing="6"
|
||||
IsVisible="{Binding PendingPsbtBase64, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Loc[send.psbt.label]}" Classes="label"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding PendingPsbtBase64}" IsReadOnly="True"
|
||||
FontFamily="monospace" FontSize="11" TextWrapping="Wrap"
|
||||
AcceptsReturn="True" Height="90"/>
|
||||
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||
Content="{Binding Loc[send.psbt.copy]}"
|
||||
Click="OnCopyPsbtClick"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Watch-only notice -->
|
||||
<TextBlock Grid.Row="2" Text="{Binding Loc[send.watchonly.hint]}"
|
||||
IsVisible="{Binding IsWatchOnlyAccount}"
|
||||
TextWrapping="Wrap" FontSize="12"
|
||||
Foreground="{DynamicResource WarningBrush}"/>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,*" ColumnSpacing="14">
|
||||
<Grid Grid.Row="3" ColumnDefinitions="*,*" ColumnSpacing="14">
|
||||
<Button Grid.Column="0" Content="{Binding Loc[send.prepare]}"
|
||||
Command="{Binding PrepareSendCommand}"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||
@@ -620,9 +638,26 @@
|
||||
<SelectableTextBlock Text="{Binding SendPreview}"
|
||||
TextWrapping="Wrap" FontSize="13" Classes="mono"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<StackPanel Spacing="6"
|
||||
IsVisible="{Binding PendingPsbtBase64, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Loc[send.psbt.label]}" Classes="label"/>
|
||||
<TextBox Text="{Binding PendingPsbtBase64}" IsReadOnly="True"
|
||||
FontFamily="monospace" FontSize="11" TextWrapping="Wrap"
|
||||
AcceptsReturn="True" Height="90"/>
|
||||
<Button Content="{Binding Loc[send.psbt.copy]}"
|
||||
Click="OnCopyPsbtClick"
|
||||
MinHeight="44"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Watch-only notice -->
|
||||
<TextBlock Text="{Binding Loc[send.watchonly.hint]}"
|
||||
IsVisible="{Binding IsWatchOnlyAccount}"
|
||||
TextWrapping="Wrap" FontSize="12"
|
||||
Foreground="{DynamicResource WarningBrush}"/>
|
||||
|
||||
<!-- Action buttons: primary (Confirm) prominent, secondary below -->
|
||||
<Button Content="{Binding Loc[send.confirm]}" Classes="accent"
|
||||
Command="{Binding ConfirmSendCommand}"
|
||||
|
||||
@@ -102,6 +102,17 @@ public partial class MainView : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCopyPsbtClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm || string.IsNullOrEmpty(vm.PendingPsbtBase64))
|
||||
return;
|
||||
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
|
||||
{
|
||||
await clipboard.SetTextAsync(vm.PendingPsbtBase64);
|
||||
vm.NotifyPsbtCopied();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConnectionStatusTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
|
||||
Reference in New Issue
Block a user