feat(ui): wizard flows for xpub/xprv and WIF import

Add two new import paths to the setup wizard:

- "Importa xpub / xprv": accepts any SLIP-132 key (xpub/ypub/zpub or
  xprv/yprv/zprv), auto-detects script type from the version prefix,
  and saves a watch-only or fully-spendable HD account accordingly.

- "Importa chiave WIF": accepts one or more WIF private keys (one per
  line), goes through script-type selection, and saves an ImportedKeyAccount.

WizardFlowKind enum (New, Restore, ImportXkey, ImportWif) governs which
steps are shown; WizardBack() updated for all four flows. OpenLoaded()
updated to accept IWalletAccount and display wallet kind tags
(watch-only / imported) in the network info line.

Localization keys added for all six languages (it/en/es/fr/pt/de).
ViewModel._account changed from HdAccount? to IWalletAccount?.
This commit is contained in:
2026-06-15 14:23:03 +02:00
parent 47b6064964
commit 4dc37f1b42
5 changed files with 222 additions and 20 deletions
@@ -186,7 +186,7 @@ public partial class MainWindowViewModel
Addresses.Add(new AddressRow(_loc["addr.receive"], i,
_account.GetReceiveAddress(i).ToString(), "—", "—",
false,
_account.GetPublicKey(false, i).ToHex(),
_account.GetPublicKey(false, i)?.ToHex() ?? "",
KeyWif(false, i),
$"m/{_doc!.AccountPath}/0/{i}"));
return;
@@ -214,7 +214,7 @@ public partial class MainWindowViewModel
a.BalanceSats > 0 ? Fmt(a.BalanceSats, withLabel: false) : (a.TxCount > 0 ? "0" : "—"),
a.TxCount > 0 ? a.TxCount.ToString() : "—",
a.IsChange,
_account.GetPublicKey(a.IsChange, a.Index).ToHex(),
_account.GetPublicKey(a.IsChange, a.Index)?.ToHex() ?? "",
KeyWif(a.IsChange, a.Index),
$"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}"));
}
+148 -14
View File
@@ -24,8 +24,13 @@ public partial class MainWindowViewModel
public const string StepWords = "words";
public const string StepPassphrase = "passphrase";
public const string StepScriptType = "script-type";
public const string StepImportXkey = "import-xkey";
public const string StepImportWif = "import-wif";
public const string StepPassword = "password";
private enum WizardFlowKind { New, Restore, ImportXkey, ImportWif }
private WizardFlowKind _wizardFlow;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsStepDataLocation))]
[NotifyPropertyChangedFor(nameof(IsStepStart))]
@@ -36,6 +41,8 @@ public partial class MainWindowViewModel
[NotifyPropertyChangedFor(nameof(IsStepWords))]
[NotifyPropertyChangedFor(nameof(IsStepPassphrase))]
[NotifyPropertyChangedFor(nameof(IsStepScriptType))]
[NotifyPropertyChangedFor(nameof(IsStepImportXkey))]
[NotifyPropertyChangedFor(nameof(IsStepImportWif))]
[NotifyPropertyChangedFor(nameof(IsStepPassword))]
private string setupStep = StepStart;
@@ -48,6 +55,8 @@ public partial class MainWindowViewModel
public bool IsStepWords => SetupStep == StepWords;
public bool IsStepPassphrase => SetupStep == StepPassphrase;
public bool IsStepScriptType => SetupStep == StepScriptType;
public bool IsStepImportXkey => SetupStep == StepImportXkey;
public bool IsStepImportWif => SetupStep == StepImportWif;
public bool IsStepPassword => SetupStep == StepPassword;
[ObservableProperty]
@@ -69,6 +78,16 @@ public partial class MainWindowViewModel
public string DefaultDataPath => AppPaths.DefaultDataRoot();
[ObservableProperty]
private string importXkeyInput = "";
[ObservableProperty]
private string importWifInput = "";
// Tipo di script rilevato durante la decodifica dell'xkey (per mostrarlo all'utente)
[ObservableProperty]
private string importXkeyDetectedKind = "";
[ObservableProperty]
private string mnemonicInput = "";
@@ -157,7 +176,7 @@ public partial class MainWindowViewModel
[RelayCommand]
private void WizardStartNew()
{
_isRestoreFlow = false;
_wizardFlow = WizardFlowKind.New;
MnemonicInput = Bip39.Generate(MnemonicLength.Twelve).ToString();
SetupStep = StepShowSeed;
StatusMessage = "";
@@ -166,12 +185,31 @@ public partial class MainWindowViewModel
[RelayCommand]
private void WizardStartRestore()
{
_isRestoreFlow = true;
_wizardFlow = WizardFlowKind.Restore;
MnemonicInput = "";
SetupStep = StepWords;
StatusMessage = "";
}
[RelayCommand]
private void WizardStartImportXkey()
{
_wizardFlow = WizardFlowKind.ImportXkey;
ImportXkeyInput = "";
ImportXkeyDetectedKind = "";
SetupStep = StepImportXkey;
StatusMessage = "";
}
[RelayCommand]
private void WizardStartImportWif()
{
_wizardFlow = WizardFlowKind.ImportWif;
ImportWifInput = "";
SetupStep = StepImportWif;
StatusMessage = "";
}
[RelayCommand]
private void WizardNextFromShowSeed()
{
@@ -218,6 +256,58 @@ public partial class MainWindowViewModel
StatusMessage = "";
}
[RelayCommand]
private void WizardNextFromImportXkey()
{
if (string.IsNullOrWhiteSpace(ImportXkeyInput))
{
StatusMessage = Loc.Tr("msg.xkey.required");
return;
}
// Valida e rileva il tipo: prova prima come xpub, poi come xprv.
if (Slip132.TryDecodePublic(ImportXkeyInput.Trim(), Profile, out _, out var pubKind))
{
SelectedScriptKind = pubKind;
ImportXkeyDetectedKind = $"{pubKind} (watch-only)";
}
else if (Slip132.TryDecodePrivate(ImportXkeyInput.Trim(), Profile, out _, out var prvKind))
{
SelectedScriptKind = prvKind;
ImportXkeyDetectedKind = prvKind.ToString();
}
else
{
StatusMessage = Loc.Tr("msg.xkey.invalid");
return;
}
PasswordInput = ConfirmPasswordInput = "";
EncryptWallet = true;
SetupStep = StepPassword;
StatusMessage = "";
}
[RelayCommand]
private void WizardNextFromImportWif()
{
if (string.IsNullOrWhiteSpace(ImportWifInput))
{
StatusMessage = Loc.Tr("msg.wif.required");
return;
}
// Valida il WIF con un parsing anticipato.
try
{
_ = new NBitcoin.BitcoinSecret(ImportWifInput.Trim(), PalladiumNetworks.For(Net));
}
catch
{
StatusMessage = Loc.Tr("msg.wif.invalid");
return;
}
SetupStep = StepScriptType;
StatusMessage = "";
}
[RelayCommand]
private void WizardNextFromScriptType()
{
@@ -233,11 +323,11 @@ public partial class MainWindowViewModel
SetupStep = SetupStep switch
{
StepOpen => WalletList.Count > 1 ? StepChooseWallet : StepStart,
StepChooseWallet or StepShowSeed or StepWords => StepStart,
StepChooseWallet or StepShowSeed or StepWords or StepImportXkey or StepImportWif => StepStart,
StepConfirmSeed => StepShowSeed,
StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed,
StepScriptType => StepPassphrase,
StepPassword => StepScriptType,
StepPassphrase => _wizardFlow == WizardFlowKind.Restore ? StepWords : StepConfirmSeed,
StepScriptType => _wizardFlow == WizardFlowKind.ImportWif ? StepImportWif : StepPassphrase,
StepPassword => _wizardFlow == WizardFlowKind.ImportXkey ? StepImportXkey : StepScriptType,
_ => StepStart,
};
if (SetupStep == StepStart)
@@ -269,11 +359,46 @@ public partial class MainWindowViewModel
try
{
var (doc, account) = WalletLoader.NewFromMnemonic(
MnemonicInput,
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
SelectedScriptKind,
Profile);
WalletDocument doc;
IWalletAccount account;
switch (_wizardFlow)
{
case WizardFlowKind.ImportXkey:
{
var input = ImportXkeyInput.Trim();
if (Slip132.TryDecodePublic(input, Profile, out _, out _))
{
var (d, a) = WalletLoader.NewFromXpub(input, Profile, SelectedScriptKind);
(doc, account) = (d, a);
}
else
{
var (d, a) = WalletLoader.NewFromXprv(input, Profile, SelectedScriptKind);
(doc, account) = (d, a);
}
break;
}
case WizardFlowKind.ImportWif:
{
var wifLines = ImportWifInput.Split('\n',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var (d, a) = WalletLoader.NewFromWif(wifLines, SelectedScriptKind, Profile);
(doc, account) = (d, a);
break;
}
default:
{
var (d, a) = WalletLoader.NewFromMnemonic(
MnemonicInput,
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
SelectedScriptKind,
Profile);
(doc, account) = (d, a);
break;
}
}
var path = AppPaths.DefaultWalletPath(Net);
for (var n = 2; WalletStore.Exists(path); n++)
path = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json");
@@ -365,7 +490,7 @@ public partial class MainWindowViewModel
}
private void OpenLoaded(
WalletDocument doc, HdAccount account, string path, string? password, WalletLock walletLock)
WalletDocument doc, IWalletAccount account, string path, string? password, WalletLock walletLock)
{
_walletLock?.Dispose();
_walletLock = walletLock;
@@ -375,14 +500,23 @@ public partial class MainWindowViewModel
_walletPath = path;
_password = password;
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
ImportXkeyInput = ImportWifInput = ImportXkeyDetectedKind = "";
SetupStep = StepStart;
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
+ (doc.IsWatchOnly ? " · watch-only" : "");
var walletKindTag = account switch
{
ImportedKeyAccount => " · imported",
{ IsWatchOnly: true } => " · watch-only",
_ => ""
};
var pathTag = !string.IsNullOrEmpty(doc.AccountPath) ? $" · m/{doc.AccountPath}" : "";
NetworkInfo = $"{doc.Network} · {doc.ScriptKind}{pathTag}{walletKindTag}";
LoadContacts();
ApplyCache(doc.Cache);
IsWalletOpen = true;
StatusMessage = Loc.Tr("msg.opened");
_autoReconnect = true; // keepalive riprova la connessione anche se il primo tentativo fallisce
_syncFailed = true; // forza la prima sync automatica
_ = ConnectAndSync();
}
}
+11 -4
View File
@@ -49,7 +49,7 @@ public partial class MainWindowViewModel : ViewModelBase
{
// ---- stato sessione wallet ----
private WalletDocument? _doc;
private HdAccount? _account;
private IWalletAccount? _account;
private string? _walletPath;
private string? _password;
private WalletLock? _walletLock;
@@ -73,10 +73,10 @@ public partial class MainWindowViewModel : ViewModelBase
// ---- wizard ----
private string? _pendingOpenPath;
private bool _isRestoreFlow;
// ---- keep-alive ----
private bool _autoReconnect;
private bool _syncFailed;
private readonly DispatcherTimer _keepAliveTimer;
// ---- server UI sync ----
@@ -135,8 +135,8 @@ public partial class MainWindowViewModel : ViewModelBase
if (_account is null or { IsWatchOnly: true }) return "";
try
{
return _account.GetExtPrivateKey(isChange, index)
.PrivateKey.GetWif(PalladiumNetworks.For(Net)).ToString();
return _account.GetPrivateKey(isChange, index)
?.GetWif(PalladiumNetworks.For(Net)).ToString() ?? "";
}
catch { return ""; }
}
@@ -161,6 +161,12 @@ public partial class MainWindowViewModel : ViewModelBase
return;
if (_client is { IsConnected: true })
{
// Se il wallet è aperto e l'ultima sync è fallita, riprova automaticamente.
if (_syncFailed && _account is not null)
{
await ConnectAndSync();
return;
}
try { await _client.PingAsync(); }
catch { }
}
@@ -183,6 +189,7 @@ public partial class MainWindowViewModel : ViewModelBase
_synchronizer = null;
_autoReconnect = false;
_resyncRequested = false;
_syncFailed = false;
_doc = null;
_account = null;
_lastTransactions = null;