Compare commits
6 Commits
c5290a1796
...
1f857ddf52
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f857ddf52 | |||
| 2953ba35ba | |||
| c0193ae440 | |||
| affeaccae8 | |||
| 4a97172346 | |||
| 5d6ff07f0b |
@@ -0,0 +1,84 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to coding agents when working with code in this repository.
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
Operate as an **expert in cryptocurrencies and cryptography**: reason with the domain's rigor about UTXO consensus, HD key derivation (BIP32/39/SLIP-132), signature schemes and scripts (P2PKH/P2SH/P2WPKH, PSBT), address encoding (base58/bech32), Merkle/SPV proofs, and at-rest encryption. When a choice touches cryptographic correctness or fund safety, judge it through that lens and flag known risks and pitfalls (nonce reuse, missing validation, exposed keys/seed, wrong fee/coin-selection, unverified server responses). Explain trade-offs with technical precision; never take for granted what hasn't been verified.
|
||||||
|
|
||||||
|
## Language policy
|
||||||
|
|
||||||
|
- **Conversation with the user**: Italian.
|
||||||
|
- **All code, comments, commit messages, and documentation files**: English only.
|
||||||
|
|
||||||
|
## How to assist
|
||||||
|
|
||||||
|
On **every requested change**, before implementing, judge whether it makes sense and say so plainly: if a request is useful and consistent with the project, proceed; if it is useless, redundant, already covered elsewhere, or risks degrading the code, **say so** with a short rationale and propose the better alternative (or doing nothing). No automatic agreement -- an honest opinion is worth more than blind execution.
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
|
||||||
|
SPV wallet (Sparrow-style) for the **Palladium (PLM)** cryptocurrency, a Bitcoin-derived UTXO chain. Targets desktop (Windows/Linux) and Android, from the same source. Lightning is excluded from the first release.
|
||||||
|
|
||||||
|
## Stack and structure
|
||||||
|
|
||||||
|
.NET 10 + Avalonia UI 12 + NBitcoin.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Core/ Chain/ Crypto/ Wallet/ Spv/ Net/ Storage/ (no UI dependency)
|
||||||
|
src/App/ shared Avalonia UI library (App, Views, ViewModels, Loc, Assets)
|
||||||
|
src/App.Desktop/ desktop head (WinExe): Program.cs, app.manifest, .ico -> runnable
|
||||||
|
src/App.Android/ Android head (net10.0-android): MainApplication/MainActivity -> apk
|
||||||
|
src/Cli/ CLI on the same Core tests/ xUnit
|
||||||
|
```
|
||||||
|
|
||||||
|
The Avalonia UI lives **once** in `src/App` (a library); the two heads only carry the
|
||||||
|
per-platform entry point and packages. `MainView` (UserControl) is the shared root, hosted
|
||||||
|
by `MainWindow` on desktop and as the single-view root on Android.
|
||||||
|
|
||||||
|
**Non-negotiable dependency rule:** `App`/`Cli` depend only on `Core`; the UI goes through the wallet domain, never directly through network or cryptography. `Core` knows nothing about the UI.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
.NET 10 SDK lives in `~/.dotnet10`: in non-interactive shells, before any `dotnet` command run
|
||||||
|
`export PATH="$HOME/.dotnet10:$PATH" DOTNET_ROOT="$HOME/.dotnet10"`.
|
||||||
|
|
||||||
|
- Build: `dotnet build`
|
||||||
|
- Tests (headless, the primary verification layer): `dotnet test` -- single: `dotnet test --filter "FullyQualifiedName~TestName"`; property-based tests (CsCheck, `PropertyTests.cs`) run in the same command and take ~30 s
|
||||||
|
- GUI hot reload: `dotnet watch --project src/App.Desktop` (on WSL2/WSLg the window shows on the Windows desktop, no graphics dependencies to install)
|
||||||
|
- CLI: `dotnet run --project src/Cli -- <command>` (no args -> usage)
|
||||||
|
- Windows publish: `dotnet publish src/App.Desktop -r win-x64 -p:PublishSingleFile=true --self-contained`
|
||||||
|
- Linux publish: `dotnet publish src/App.Desktop -r linux-x64 --self-contained` (then AppImage via PupNet Deploy)
|
||||||
|
|
||||||
|
**Android (apk).** Needs the `android` workload (`dotnet workload install android`), a JDK
|
||||||
|
(`JAVA_HOME`), and the Android SDK. To provision the SDK once:
|
||||||
|
`dotnet build src/App.Android -t:InstallAndroidDependencies -p:AndroidSdkDirectory=$HOME/android-sdk -p:AcceptAndroidSDKLicenses=true`.
|
||||||
|
Then build a debug apk (output in `src/App.Android/bin/Debug/net10.0-android/*-Signed.apk`):
|
||||||
|
`JAVA_HOME=<jdk> dotnet build src/App.Android -c Debug -t:SignAndroidPackage -p:AndroidSdkDirectory=$HOME/android-sdk`
|
||||||
|
(set `ANDROID_HOME` to skip the `-p:AndroidSdkDirectory` flag). The head is an application,
|
||||||
|
not a library, because it sets `<OutputType>Exe</OutputType>`; min SDK 23 (AndroidX requirement).
|
||||||
|
Note: a plain `dotnet build` at the solution level needs the Android SDK path for the Android head.
|
||||||
|
|
||||||
|
**CLI** (`src/Cli`): `create`/`restore`/`restore-xpub`/`info`; `sync`/`send`/`servers`/`reset-certs` (`--server host:port [--ssl]`); `newseed`/`addresses`. Default wallet file `~/.palladium-wallet/<network>/wallets/default.wallet.json` (`--file` to change it).
|
||||||
|
|
||||||
|
## Architecture (points that require reading multiple files)
|
||||||
|
|
||||||
|
- **Layers:** GUI -> wallet domain -> SPV/Sync -> Network -> Cryptography -> Persistence; each layer depends only downward.
|
||||||
|
- **Network profile:** all chain constants (address prefixes, BIP32 headers, bech32 HRP, genesis, ports, coin_type 746) **centralized in `Core/Chain`** (`ChainProfiles`/`PalladiumNetworks`), selectable per network (mainnet/testnet/regtest). No scattered magic numbers.
|
||||||
|
- **LWMA / skip PoW:** LWMA difficulty, 2-minute blocks; an SPV client cannot recompute it -> `SkipPowValidation = true`, trust anchored to **hardcoded checkpoints**. Custom layer: NBitcoin assumes Bitcoin's retargeting.
|
||||||
|
- **NBitcoin vs custom:** NBitcoin covers the custom network, BIP32/39, addresses, transactions, PSBT, signing, encoding, hashing -- **do not reimplement these**. Hand-written custom code: JSON-RPC client for the indexing server (ElectrumX-like); SPV sync with Merkle verification; header/checkpoint validation; coin selection and fee policy; versioned encrypted JSON wallet file.
|
||||||
|
- **PSBT-centric:** every signing flow goes through PSBT (offline/air-gapped/multisig/hardware).
|
||||||
|
- **Ports:** 50001/50002 = indexing server (what the SPV wallet talks to), **not** the node's P2P port (2333).
|
||||||
|
|
||||||
|
## GUI conventions (`src/App`)
|
||||||
|
|
||||||
|
- **Shared `MainView` + heads:** the whole UI is a single `MainView` (UserControl), so it works both as a desktop window's content and as Android's single-view root. Top-level APIs (file/folder picker, clipboard) are reached via `TopLevel.GetTopLevel(this)` since a UserControl doesn't expose them. `MainWindowViewModel.IsDesktop` (from `OperatingSystem.IsAndroid()`) hides filesystem-only features (open-from-file; the data-location wizard step auto-skips on Android because the head sets `AppPaths.OverrideDataRoot`).
|
||||||
|
- **Single ViewModel** `MainWindowViewModel` (CommunityToolkit.Mvvm: `[ObservableProperty]`, `[RelayCommand]`); `Core` is driven directly from here.
|
||||||
|
- **In-app overlays, not OS windows:** details (address, transaction), settings, and help are full-screen `Border`s gated by an `IsXxxOpen` flag, not separate `Window`s -- instant open/close, mobile-friendly, and popups/top-levels are slow on WSLg. Pattern: bool property + Open/Close commands + backdrop handler and Esc key in `MainView`'s code-behind; overlay close buttons bind via `$parent[UserControl]` (not `$parent[Window]`, absent on mobile). Heavy network work runs off the UI thread (`Task.Run`) so the overlay never freezes.
|
||||||
|
- **Localization:** `Localization/Loc.cs`, key->6 languages dictionary (it/en/es/fr/pt/de); in XAML `{Binding Loc[key]}`, in C# `Loc.Tr("key")`. On language change the `Loc` instance is replaced.
|
||||||
|
- **App version:** single source = `<Version>` in `src/App/PalladiumWallet.App.csproj`; read at runtime (`MainWindowViewModel.AppVersion`) and shown in the title.
|
||||||
|
- **Storage paths:** `Core/Storage/AppPaths` resolves data locations; `AppPaths.OverrideDataRoot` (top priority) is the per-platform seam -- the Android head sets it to the app sandbox (`Context.FilesDir`), desktop leaves it null.
|
||||||
|
|
||||||
|
## Working rules
|
||||||
|
|
||||||
|
- **Cross-implementation tests:** compare addresses, txids, and PSBTs against a reference wallet (golden vectors). A different address or txid is a blocking bug.
|
||||||
|
- **Security:** seed and private keys never in plaintext on disk/logs/network; every server response validated with Merkle + checkpoints; watch-only truly read-only.
|
||||||
@@ -55,8 +55,8 @@ For desktop and the CLI you only need the **.NET 10 SDK**. The core and crypto a
|
|||||||
|
|
||||||
1. Install the .NET 10 SDK through your distro's package manager, or without root via the official script:
|
1. Install the .NET 10 SDK through your distro's package manager, or without root via the official script:
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0
|
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir "$HOME/.dotnet10"
|
||||||
export PATH="$HOME/.dotnet:$PATH" DOTNET_ROOT="$HOME/.dotnet"
|
export PATH="$HOME/.dotnet10:$PATH" DOTNET_ROOT="$HOME/.dotnet10"
|
||||||
```
|
```
|
||||||
(add the two `export` lines to your `~/.bashrc` to make them permanent)
|
(add the two `export` lines to your `~/.bashrc` to make them permanent)
|
||||||
2. Clone and restore:
|
2. Clone and restore:
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ public sealed class Loc
|
|||||||
// Menu it en es fr pt de
|
// Menu it en es fr pt de
|
||||||
["menu.file"] = ["_File", "_File", "_Archivo", "_Fichier", "_Arquivo", "_Datei"],
|
["menu.file"] = ["_File", "_File", "_Archivo", "_Fichier", "_Arquivo", "_Datei"],
|
||||||
["menu.file.new"] = ["Nuovo / ripristina wallet…", "New / restore wallet…", "Nuevo / restaurar wallet…", "Nouveau / restaurer le wallet…", "Novo / restaurar carteira…", "Neu / Wallet wiederherstellen…"],
|
["menu.file.new"] = ["Nuovo / ripristina wallet…", "New / restore wallet…", "Nuevo / restaurar wallet…", "Nouveau / restaurer le wallet…", "Novo / restaurar carteira…", "Neu / Wallet wiederherstellen…"],
|
||||||
["menu.file.open"] = ["Apri wallet da file…", "Open wallet from file…", "Abrir wallet desde archivo…", "Ouvrir le wallet depuis un fichier…", "Abrir carteira de arquivo…", "Wallet aus Datei öffnen…"],
|
["menu.file.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
|
||||||
["menu.file.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
|
["menu.file.quit"] = ["Esci", "Quit", "Salir", "Quitter", "Sair", "Beenden"],
|
||||||
["menu.net"] = ["_Rete", "_Network", "_Red", "_Réseau", "_Rede", "_Netzwerk"],
|
["menu.net"] = ["_Rete", "_Network", "_Red", "_Réseau", "_Rede", "_Netzwerk"],
|
||||||
["menu.net.discover"] = ["Cerca altri server (peer)", "Discover servers (peers)", "Buscar otros servidores (peers)", "Rechercher d'autres serveurs (pairs)", "Procurar outros servidores (peers)", "Weitere Server suchen (Peers)"],
|
["menu.net.discover"] = ["Cerca altri server (peer)", "Discover servers (peers)", "Buscar otros servidores (peers)", "Rechercher d'autres serveurs (pairs)", "Procurar outros servidores (peers)", "Weitere Server suchen (Peers)"],
|
||||||
["menu.net.resetcerts"] = ["Reset certificati SSL", "Reset SSL certificates", "Restablecer certificados SSL", "Réinitialiser les certificats SSL", "Redefinir certificados SSL", "SSL-Zertifikate zurücksetzen"],
|
["menu.net.resetcerts"] = ["Reset certificati SSL", "Reset SSL certificates", "Restablecer certificados SSL", "Réinitialiser les certificats SSL", "Redefinir certificados SSL", "SSL-Zertifikate zurücksetzen"],
|
||||||
@@ -195,6 +195,8 @@ public sealed class Loc
|
|||||||
["addr.pubkey"] = ["Chiave pubblica:", "Public key:", "Clave pública:", "Clé publique :", "Chave pública:", "Öffentlicher Schlüssel:"],
|
["addr.pubkey"] = ["Chiave pubblica:", "Public key:", "Clave pública:", "Clé publique :", "Chave pública:", "Öffentlicher Schlüssel:"],
|
||||||
["addr.privkey"] = ["Chiave privata (WIF):", "Private key (WIF):", "Clave privada (WIF):", "Clé privée (WIF) :", "Chave privada (WIF):", "Privater Schlüssel (WIF):"],
|
["addr.privkey"] = ["Chiave privata (WIF):", "Private key (WIF):", "Clave privada (WIF):", "Clé privée (WIF) :", "Chave privada (WIF):", "Privater Schlüssel (WIF):"],
|
||||||
["addr.show.privkey"] = ["Mostra", "Show", "Mostrar", "Afficher", "Mostrar", "Anzeigen"],
|
["addr.show.privkey"] = ["Mostra", "Show", "Mostrar", "Afficher", "Mostrar", "Anzeigen"],
|
||||||
|
["addr.privkey.prompt.title"] = ["Conferma identità", "Confirm identity", "Confirmar identidad", "Confirmer l'identité", "Confirmar identidade", "Identität bestätigen"],
|
||||||
|
["addr.privkey.prompt.desc"] = ["Inserisci la password del wallet per visualizzare la chiave privata.", "Enter the wallet password to reveal the private key.", "Ingresa la contraseña del wallet para ver la clave privada.", "Entrez le mot de passe du wallet pour afficher la clé privée.", "Digite a senha da carteira para ver a chave privada.", "Geben Sie das Wallet-Passwort ein, um den privaten Schlüssel anzuzeigen."],
|
||||||
["addr.hide.privkey"] = ["Nascondi", "Hide", "Ocultar", "Masquer", "Ocultar", "Ausblenden"],
|
["addr.hide.privkey"] = ["Nascondi", "Hide", "Ocultar", "Masquer", "Ocultar", "Ausblenden"],
|
||||||
["addr.receive"] = ["ricezione", "receive", "recepción", "réception", "recebimento", "Empfang"],
|
["addr.receive"] = ["ricezione", "receive", "recepción", "réception", "recebimento", "Empfang"],
|
||||||
["addr.change"] = ["change", "change", "cambio", "monnaie", "troco", "Wechselgeld"],
|
["addr.change"] = ["change", "change", "cambio", "monnaie", "troco", "Wechselgeld"],
|
||||||
@@ -223,8 +225,7 @@ public sealed class Loc
|
|||||||
["tx.size.total"] = ["Dimensione totale", "Total size", "Tamaño total", "Taille totale", "Tamanho total", "Gesamtgröße"],
|
["tx.size.total"] = ["Dimensione totale", "Total size", "Tamaño total", "Taille totale", "Tamanho total", "Gesamtgröße"],
|
||||||
["tx.size.virtual"] = ["Dimensione virtuale", "Virtual size", "Tamaño virtual", "Taille virtuelle", "Tamanho virtual", "Virtuelle Größe"],
|
["tx.size.virtual"] = ["Dimensione virtuale", "Virtual size", "Tamaño virtual", "Taille virtuelle", "Tamanho virtual", "Virtuelle Größe"],
|
||||||
["tx.rbf"] = ["Sostituibile (RBF)", "Replaceable (RBF)", "Reemplazable (RBF)", "Remplaçable (RBF)", "Substituível (RBF)", "Ersetzbar (RBF)"],
|
["tx.rbf"] = ["Sostituibile (RBF)", "Replaceable (RBF)", "Reemplazable (RBF)", "Remplaçable (RBF)", "Substituível (RBF)", "Ersetzbar (RBF)"],
|
||||||
["tx.verified"] = ["Verifica SPV", "SPV verification", "Verificación SPV", "Vérification SPV", "Verificação SPV", "SPV-Prüfung"],
|
["tx.inputs"] = ["Input", "Inputs", "Entradas", "Entrées", "Entradas", "Eingänge"],
|
||||||
["tx.inputs"] = ["Input", "Inputs", "Entradas", "Entrées", "Entradas", "Eingänge"],
|
|
||||||
["tx.outputs"] = ["Output", "Outputs", "Salidas", "Sorties", "Saídas", "Ausgänge"],
|
["tx.outputs"] = ["Output", "Outputs", "Salidas", "Sorties", "Saídas", "Ausgänge"],
|
||||||
["tx.yes"] = ["Sì", "Yes", "Sí", "Oui", "Sim", "Ja"],
|
["tx.yes"] = ["Sì", "Yes", "Sí", "Oui", "Sim", "Ja"],
|
||||||
["tx.no"] = ["No", "No", "No", "Non", "Não", "Nein"],
|
["tx.no"] = ["No", "No", "No", "Non", "Não", "Nein"],
|
||||||
@@ -248,6 +249,35 @@ public sealed class Loc
|
|||||||
["send.summary"] = ["Riepilogo", "Summary", "Resumen", "Résumé", "Resumo", "Zusammenfassung"],
|
["send.summary"] = ["Riepilogo", "Summary", "Resumen", "Résumé", "Resumo", "Zusammenfassung"],
|
||||||
["receive.your.address"] = ["Il tuo indirizzo", "Your address", "Tu dirección", "Votre adresse", "Seu endereço", "Deine Adresse"],
|
["receive.your.address"] = ["Il tuo indirizzo", "Your address", "Tu dirección", "Votre adresse", "Seu endereço", "Deine Adresse"],
|
||||||
|
|
||||||
|
// Wallet info overlay
|
||||||
|
["menu.wallet"] = ["_Wallet", "_Wallet", "_Wallet", "_Wallet", "_Wallet", "_Wallet"],
|
||||||
|
["walletinfo.title"] = ["Informazioni Wallet", "Wallet Information", "Información del Wallet", "Informations Wallet", "Informações da Carteira", "Wallet-Informationen"],
|
||||||
|
["walletinfo.file"] = ["File", "File", "Archivo", "Fichier", "Arquivo", "Datei"],
|
||||||
|
["walletinfo.network"] = ["Rete", "Network", "Red", "Réseau", "Rede", "Netzwerk"],
|
||||||
|
["walletinfo.type"] = ["Tipo wallet", "Wallet type", "Tipo de wallet", "Type de wallet", "Tipo de carteira", "Wallet-Typ"],
|
||||||
|
["walletinfo.type.seed"] = ["HD (seed BIP39)", "HD (BIP39 seed)", "HD (seed BIP39)", "HD (graine BIP39)", "HD (semente BIP39)", "HD (BIP39-Seed)"],
|
||||||
|
["walletinfo.type.xprv"] = ["HD (xprv importato)", "HD (imported xprv)", "HD (xprv importado)", "HD (xprv importé)", "HD (xprv importado)", "HD (importierter xprv)"],
|
||||||
|
["walletinfo.type.wif"] = ["Chiave WIF importata", "Imported WIF key", "Clave WIF importada", "Clé WIF importée", "Chave WIF importada", "Importierter WIF-Schlüssel"],
|
||||||
|
["walletinfo.type.watchonly"] = ["Watch-only (xpub)", "Watch-only (xpub)", "Watch-only (xpub)", "Watch-only (xpub)", "Watch-only (xpub)", "Watch-only (xpub)"],
|
||||||
|
["walletinfo.script"] = ["Script", "Script", "Script", "Script", "Script", "Script"],
|
||||||
|
["walletinfo.derivpath"] = ["Percorso derivazione", "Derivation path", "Ruta de derivación", "Chemin de dérivation", "Caminho de derivação", "Ableitungspfad"],
|
||||||
|
["walletinfo.xpub"] = ["Chiave pubblica estesa", "Extended public key", "Clave pública extendida", "Clé publique étendue", "Chave pública estendida", "Erweiterter öffentlicher Schlüssel"],
|
||||||
|
["walletinfo.fingerprint"] = ["Master fingerprint", "Master fingerprint", "Huella maestra", "Empreinte maître", "Impressão digital mestre", "Master-Fingerprint"],
|
||||||
|
["walletinfo.seed.section"] = ["Seed (mnemonica BIP39)", "Seed (BIP39 mnemonic)", "Semilla (mnemónico BIP39)", "Graine (mnémonique BIP39)", "Semente (mnemônico BIP39)", "Seed (BIP39-Mnemonic)"],
|
||||||
|
["walletinfo.seed.noseed"] = ["Questo wallet non ha una seed (watch-only o importato).", "This wallet has no seed (watch-only or imported).", "Este wallet no tiene seed (watch-only o importado).", "Ce wallet n'a pas de graine (watch-only ou importé).", "Esta carteira não tem semente (watch-only ou importada).", "Dieses Wallet hat keinen Seed (watch-only oder importiert)."],
|
||||||
|
["walletinfo.seed.password"] = ["Password del file wallet per sbloccare il seed:", "Wallet file password to unlock the seed:", "Contraseña del archivo para desbloquear la seed:", "Mot de passe du fichier pour déverrouiller la graine :", "Senha do arquivo para desbloquear a semente:", "Dateipasswort zum Entsperren des Seeds:"],
|
||||||
|
["walletinfo.seed.reveal"] = ["Mostra seed", "Show seed", "Mostrar seed", "Afficher la graine", "Mostrar semente", "Seed anzeigen"],
|
||||||
|
["walletinfo.seed.hide"] = ["Nascondi", "Hide", "Ocultar", "Masquer", "Ocultar", "Ausblenden"],
|
||||||
|
["walletinfo.seed.warning"] = [
|
||||||
|
"Non condividere mai queste parole. Chi le possiede controlla i fondi.",
|
||||||
|
"Never share these words. Whoever holds them controls the funds.",
|
||||||
|
"Nunca compartas estas palabras. Quien las tenga controla los fondos.",
|
||||||
|
"Ne partagez jamais ces mots. Celui qui les possède contrôle les fonds.",
|
||||||
|
"Nunca compartilhe essas palavras. Quem as tiver controla os fundos.",
|
||||||
|
"Teilen Sie diese Wörter niemals. Wer sie hat, kontrolliert die Gelder."],
|
||||||
|
["walletinfo.passphrase"] = ["Passphrase BIP39", "BIP39 passphrase", "Frase de contraseña BIP39", "Phrase de passe BIP39", "Frase-senha BIP39", "BIP39-Passphrase"],
|
||||||
|
["walletinfo.passphrase.set"] = ["(impostata)", "(set)", "(establecida)", "(définie)", "(definida)", "(gesetzt)"],
|
||||||
|
|
||||||
// Connection status
|
// Connection status
|
||||||
["conn.none"] = ["non connesso", "not connected", "no conectado", "non connecté", "não conectado", "nicht verbunden"],
|
["conn.none"] = ["non connesso", "not connected", "no conectado", "non connecté", "não conectado", "nicht verbunden"],
|
||||||
["conn.disconnected"] = ["disconnesso", "disconnected", "desconectado", "déconnecté", "desconectado", "getrennt"],
|
["conn.disconnected"] = ["disconnesso", "disconnected", "desconectado", "déconnecté", "desconectado", "getrennt"],
|
||||||
|
|||||||
@@ -68,11 +68,81 @@ public partial class MainWindowViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private AddressInfo? addressInfo;
|
private AddressInfo? addressInfo;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isAddressPrivKeyRevealed;
|
||||||
|
|
||||||
|
// ---- private key password prompt ----
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isPrivKeyPromptOpen;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string privKeyPromptPassword = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string privKeyPromptError = "";
|
||||||
|
|
||||||
|
partial void OnAddressInfoChanged(AddressInfo? value)
|
||||||
|
{
|
||||||
|
IsAddressPrivKeyRevealed = false;
|
||||||
|
ClosePrivKeyPromptInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClosePrivKeyPromptInternal()
|
||||||
|
{
|
||||||
|
IsPrivKeyPromptOpen = false;
|
||||||
|
PrivKeyPromptPassword = "";
|
||||||
|
PrivKeyPromptError = "";
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowAddressInfo(AddressRow row) =>
|
public void ShowAddressInfo(AddressRow row) =>
|
||||||
AddressInfo = new AddressInfo(Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
AddressInfo = new AddressInfo(Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void CloseAddressInfo() => AddressInfo = null;
|
private void CloseAddressInfo()
|
||||||
|
{
|
||||||
|
ClosePrivKeyPromptInternal();
|
||||||
|
AddressInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RequestPrivKeyReveal()
|
||||||
|
{
|
||||||
|
if (AddressInfo is null || !AddressInfo.HasPrivKey) return;
|
||||||
|
if (string.IsNullOrEmpty(_password))
|
||||||
|
{
|
||||||
|
// No encryption: reveal directly
|
||||||
|
IsAddressPrivKeyRevealed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Encrypted: open password prompt
|
||||||
|
PrivKeyPromptPassword = "";
|
||||||
|
PrivKeyPromptError = "";
|
||||||
|
IsPrivKeyPromptOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ConfirmPrivKeyPassword()
|
||||||
|
{
|
||||||
|
if (PrivKeyPromptPassword != _password)
|
||||||
|
{
|
||||||
|
PrivKeyPromptError = Loc.Tr("msg.wrongpassword");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IsAddressPrivKeyRevealed = true;
|
||||||
|
ClosePrivKeyPromptInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CancelPrivKeyPrompt() => ClosePrivKeyPromptInternal();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void HideAddressPrivKey()
|
||||||
|
{
|
||||||
|
IsAddressPrivKeyRevealed = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- transaction detail overlay ----
|
// ---- transaction detail overlay ----
|
||||||
|
|
||||||
@@ -202,8 +272,7 @@ public partial class MainWindowViewModel
|
|||||||
History.Add(new HistoryRow(
|
History.Add(new HistoryRow(
|
||||||
tx.Height > 0 ? tx.Height.ToString() : "mempool",
|
tx.Height > 0 ? tx.Height.ToString() : "mempool",
|
||||||
(tx.DeltaSats >= 0 ? "+" : "") + Fmt(tx.DeltaSats, withLabel: false),
|
(tx.DeltaSats >= 0 ? "+" : "") + Fmt(tx.DeltaSats, withLabel: false),
|
||||||
tx.Txid,
|
tx.Txid));
|
||||||
tx.Verified ? "✓ SPV" : "—"));
|
|
||||||
|
|
||||||
Addresses.Clear();
|
Addresses.Clear();
|
||||||
foreach (var a in cache.Addresses)
|
foreach (var a in cache.Addresses)
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ public partial class MainWindowViewModel
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenHelp() => IsHelpOpen = true;
|
private void OpenHelp() => IsHelpOpen = true;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private static void QuitApp() =>
|
||||||
|
(Avalonia.Application.Current?.ApplicationLifetime
|
||||||
|
as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)
|
||||||
|
?.Shutdown();
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void CloseHelp() { IsHelpOpen = false; ResetDonate(); }
|
private void CloseHelp() { IsHelpOpen = false; ResetDonate(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System.IO;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using PalladiumWallet.App.Localization;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.App.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainWindowViewModel
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isWalletInfoOpen;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string walletInfoSeedPasswordInput = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isWalletInfoSeedRevealed;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string walletInfoSeedError = "";
|
||||||
|
|
||||||
|
// ---- computed wallet info (populated when overlay opens) ----
|
||||||
|
|
||||||
|
public string WalletInfoFileName =>
|
||||||
|
_walletPath is null ? "" :
|
||||||
|
Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(_walletPath));
|
||||||
|
|
||||||
|
public string WalletInfoNetwork => _doc?.Network ?? "";
|
||||||
|
|
||||||
|
public string WalletInfoType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_doc is null) return "";
|
||||||
|
if (_doc.Mnemonic is not null) return Loc.Tr("walletinfo.type.seed");
|
||||||
|
if (_doc.AccountXprv is not null) return Loc.Tr("walletinfo.type.xprv");
|
||||||
|
if (_doc.WifKeys is { Count: > 0 }) return Loc.Tr("walletinfo.type.wif");
|
||||||
|
return Loc.Tr("walletinfo.type.watchonly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string WalletInfoScriptKind => _doc?.ScriptKind ?? "";
|
||||||
|
|
||||||
|
public string WalletInfoDerivPath => _doc?.AccountPath ?? "";
|
||||||
|
|
||||||
|
public string WalletInfoXpub => _doc?.AccountXpub ?? "";
|
||||||
|
|
||||||
|
public string WalletInfoFingerprint => _doc?.MasterFingerprint ?? "";
|
||||||
|
|
||||||
|
public bool WalletInfoHasSeed => _doc?.Mnemonic is not null;
|
||||||
|
|
||||||
|
public bool WalletInfoSeedNeedsPassword => !string.IsNullOrEmpty(_password);
|
||||||
|
|
||||||
|
public string WalletInfoSeedText => IsWalletInfoSeedRevealed ? (_doc?.Mnemonic ?? "") : "";
|
||||||
|
|
||||||
|
public bool WalletInfoHasPassphrase => !string.IsNullOrEmpty(_doc?.Passphrase);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenWalletInfo()
|
||||||
|
{
|
||||||
|
WalletInfoSeedPasswordInput = "";
|
||||||
|
IsWalletInfoSeedRevealed = false;
|
||||||
|
WalletInfoSeedError = "";
|
||||||
|
OnPropertyChanged(nameof(WalletInfoFileName));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoNetwork));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoType));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoScriptKind));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoDerivPath));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoXpub));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoFingerprint));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoHasSeed));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoSeedNeedsPassword));
|
||||||
|
OnPropertyChanged(nameof(WalletInfoHasPassphrase));
|
||||||
|
IsWalletInfoOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CloseWalletInfo()
|
||||||
|
{
|
||||||
|
IsWalletInfoOpen = false;
|
||||||
|
WalletInfoSeedPasswordInput = "";
|
||||||
|
IsWalletInfoSeedRevealed = false;
|
||||||
|
WalletInfoSeedError = "";
|
||||||
|
OnPropertyChanged(nameof(WalletInfoSeedText));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RevealSeed()
|
||||||
|
{
|
||||||
|
if (_doc?.Mnemonic is null) return;
|
||||||
|
|
||||||
|
if (WalletInfoSeedNeedsPassword)
|
||||||
|
{
|
||||||
|
if (WalletInfoSeedPasswordInput != _password)
|
||||||
|
{
|
||||||
|
WalletInfoSeedError = Loc.Tr("msg.wrongpassword");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WalletInfoSeedError = "";
|
||||||
|
IsWalletInfoSeedRevealed = true;
|
||||||
|
OnPropertyChanged(nameof(WalletInfoSeedText));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void HideSeed()
|
||||||
|
{
|
||||||
|
IsWalletInfoSeedRevealed = false;
|
||||||
|
WalletInfoSeedPasswordInput = "";
|
||||||
|
WalletInfoSeedError = "";
|
||||||
|
OnPropertyChanged(nameof(WalletInfoSeedText));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -477,41 +477,6 @@ public partial class MainWindowViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenFromPath(string path)
|
|
||||||
{
|
|
||||||
var newLock = WalletLock.TryAcquire(path);
|
|
||||||
if (newLock is null) { StatusMessage = Loc.Tr("msg.wallet.locked"); return; }
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var doc = WalletStore.Load(path);
|
|
||||||
if (IsWalletOpen)
|
|
||||||
CloseWallet();
|
|
||||||
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password: null, newLock);
|
|
||||||
}
|
|
||||||
catch (WrongPasswordException)
|
|
||||||
{
|
|
||||||
newLock.Dispose();
|
|
||||||
if (IsWalletOpen)
|
|
||||||
CloseWallet();
|
|
||||||
_pendingOpenPath = path;
|
|
||||||
WalletFileExists = true;
|
|
||||||
PasswordInput = "";
|
|
||||||
SetupStep = StepOpen;
|
|
||||||
StatusMessage = "";
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
newLock.Dispose();
|
|
||||||
StatusMessage = Loc.Tr("msg.wallet.noaccess");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
newLock.Dispose();
|
|
||||||
StatusMessage = $"Errore: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void NewWallet()
|
private void NewWallet()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ using PalladiumWallet.Core.Wallet;
|
|||||||
namespace PalladiumWallet.App.ViewModels;
|
namespace PalladiumWallet.App.ViewModels;
|
||||||
|
|
||||||
/// <summary>Transaction history row for the view.</summary>
|
/// <summary>Transaction history row for the view.</summary>
|
||||||
public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata);
|
public sealed record HistoryRow(string Conferma, string Importo, string Txid);
|
||||||
|
|
||||||
/// <summary>Address view row with pre-computed keys and derivation path.</summary>
|
/// <summary>Address view row with pre-computed keys and derivation path.</summary>
|
||||||
public sealed record AddressRow(
|
public sealed record AddressRow(
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ public sealed class TransactionDetailsViewModel
|
|||||||
VersionText = d.Version.ToString();
|
VersionText = d.Version.ToString();
|
||||||
LockTimeText = d.LockTime.ToString();
|
LockTimeText = d.LockTime.ToString();
|
||||||
RbfText = loc[d.RbfSignaled ? "tx.yes" : "tx.no"];
|
RbfText = loc[d.RbfSignaled ? "tx.yes" : "tx.no"];
|
||||||
VerifiedText = d.Verified ? "✓ SPV" : "—";
|
|
||||||
|
|
||||||
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
|
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
|
||||||
i.IsCoinbase ? loc["tx.coinbase"] : $"{Shorten(i.PrevTxid)}:{i.PrevIndex}",
|
i.IsCoinbase ? loc["tx.coinbase"] : $"{Shorten(i.PrevTxid)}:{i.PrevIndex}",
|
||||||
i.IsCoinbase ? loc["tx.coinbase.newcoins"] : i.Address ?? "—",
|
i.IsCoinbase ? loc["tx.coinbase.newcoins"] : i.Address ?? "—",
|
||||||
@@ -93,7 +91,6 @@ public sealed class TransactionDetailsViewModel
|
|||||||
public string VersionText { get; }
|
public string VersionText { get; }
|
||||||
public string LockTimeText { get; }
|
public string LockTimeText { get; }
|
||||||
public string RbfText { get; }
|
public string RbfText { get; }
|
||||||
public string VerifiedText { get; }
|
|
||||||
public ObservableCollection<TxIoRow> Inputs { get; }
|
public ObservableCollection<TxIoRow> Inputs { get; }
|
||||||
public ObservableCollection<TxIoRow> Outputs { get; }
|
public ObservableCollection<TxIoRow> Outputs { get; }
|
||||||
|
|
||||||
|
|||||||
+211
-31
@@ -31,12 +31,15 @@
|
|||||||
<Menu Grid.Row="0" IsVisible="{Binding IsDesktop}">
|
<Menu Grid.Row="0" IsVisible="{Binding IsDesktop}">
|
||||||
<MenuItem Header="{Binding Loc[menu.file]}">
|
<MenuItem Header="{Binding Loc[menu.file]}">
|
||||||
<MenuItem Header="{Binding Loc[menu.file.new]}" Command="{Binding NewWalletCommand}"/>
|
<MenuItem Header="{Binding Loc[menu.file.new]}" Command="{Binding NewWalletCommand}"/>
|
||||||
<MenuItem Header="{Binding Loc[menu.file.open]}" Click="OnOpenWalletFileClick"
|
|
||||||
IsVisible="{Binding IsDesktop}"/>
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="{Binding Loc[menu.file.close]}" Command="{Binding CloseWalletCommand}"
|
<MenuItem Header="{Binding Loc[menu.file.close]}" Command="{Binding CloseWalletCommand}"
|
||||||
IsEnabled="{Binding IsWalletOpen}"/>
|
IsEnabled="{Binding IsWalletOpen}"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="{Binding Loc[menu.file.quit]}" Command="{Binding QuitAppCommand}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem Header="{Binding Loc[menu.wallet]}"
|
||||||
|
Command="{Binding OpenWalletInfoCommand}"
|
||||||
|
IsVisible="{Binding IsWalletOpen}"/>
|
||||||
<MenuItem Header="{Binding Loc[menu.settings]}"
|
<MenuItem Header="{Binding Loc[menu.settings]}"
|
||||||
Command="{Binding OpenSettingsCommand}"/>
|
Command="{Binding OpenSettingsCommand}"/>
|
||||||
<MenuItem Header="{Binding Loc[menu.help]}"
|
<MenuItem Header="{Binding Loc[menu.help]}"
|
||||||
@@ -47,6 +50,9 @@
|
|||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8"
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8"
|
||||||
HorizontalAlignment="Right" Margin="8,4"
|
HorizontalAlignment="Right" Margin="8,4"
|
||||||
IsVisible="{Binding IsMobile}">
|
IsVisible="{Binding IsMobile}">
|
||||||
|
<Button Content="{Binding Loc[menu.wallet]}"
|
||||||
|
Command="{Binding OpenWalletInfoCommand}"
|
||||||
|
IsVisible="{Binding IsWalletOpen}"/>
|
||||||
<Button Content="{Binding Loc[menu.settings]}" Command="{Binding OpenSettingsCommand}"/>
|
<Button Content="{Binding Loc[menu.settings]}" Command="{Binding OpenSettingsCommand}"/>
|
||||||
<Button Content="{Binding Loc[menu.help]}" Command="{Binding OpenHelpCommand}"/>
|
<Button Content="{Binding Loc[menu.help]}" Command="{Binding OpenHelpCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -363,25 +369,20 @@
|
|||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:HistoryRow">
|
<DataTemplate x:DataType="vm:HistoryRow">
|
||||||
<Panel Cursor="Hand">
|
<Panel Cursor="Hand">
|
||||||
<!-- Desktop: 4 fixed columns -->
|
<!-- Desktop: 3 fixed columns -->
|
||||||
<Grid ColumnDefinitions="90,160,*,70"
|
<Grid ColumnDefinitions="90,160,*"
|
||||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
|
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Importo}" FontFamily="monospace"/>
|
<TextBlock Grid.Column="1" Text="{Binding Importo}" FontFamily="monospace"/>
|
||||||
<TextBlock Grid.Column="2" Text="{Binding Txid}"
|
<TextBlock Grid.Column="2" Text="{Binding Txid}"
|
||||||
FontFamily="monospace" FontSize="12"
|
FontFamily="monospace" FontSize="12"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
TextTrimming="CharacterEllipsis"/>
|
||||||
<TextBlock Grid.Column="3" Text="{Binding Verificata}" Foreground="{DynamicResource SuccessBrush}"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<!-- Mobile: vertical card -->
|
<!-- Mobile: vertical card -->
|
||||||
<StackPanel Spacing="2"
|
<StackPanel Spacing="2"
|
||||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsMobile}">
|
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsMobile}">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<TextBlock Text="{Binding Importo}"
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Importo}"
|
FontFamily="monospace" FontWeight="SemiBold"/>
|
||||||
FontFamily="monospace" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Verificata}"
|
|
||||||
Foreground="{DynamicResource SuccessBrush}" FontSize="11"/>
|
|
||||||
</Grid>
|
|
||||||
<TextBlock Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="11"/>
|
<TextBlock Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="11"/>
|
||||||
<TextBlock Text="{Binding Txid}" FontFamily="monospace" FontSize="11"
|
<TextBlock Text="{Binding Txid}" FontFamily="monospace" FontSize="11"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
TextTrimming="CharacterEllipsis"/>
|
||||||
@@ -943,12 +944,25 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Private key (only when available) -->
|
<!-- Private key (only when available) -->
|
||||||
<StackPanel Spacing="4" IsVisible="{Binding HasPrivKey}">
|
<StackPanel Spacing="8" IsVisible="{Binding HasPrivKey}">
|
||||||
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11" Foreground="{DynamicResource DangerBrush}"/>
|
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11"
|
||||||
<SelectableTextBlock Text="{Binding PrivKey}"
|
Foreground="{DynamicResource DangerBrush}"/>
|
||||||
FontFamily="monospace" FontSize="12"
|
|
||||||
Foreground="{DynamicResource DangerBrush}"
|
<!-- Not yet revealed: single reveal button -->
|
||||||
TextWrapping="Wrap"/>
|
<Button IsVisible="{Binding !$parent[UserControl].((vm:MainWindowViewModel)DataContext).IsAddressPrivKeyRevealed}"
|
||||||
|
Content="{Binding Loc[walletinfo.seed.reveal]}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).RequestPrivKeyRevealCommand}"/>
|
||||||
|
|
||||||
|
<!-- Revealed: key text + hide button -->
|
||||||
|
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsAddressPrivKeyRevealed}"
|
||||||
|
Spacing="6">
|
||||||
|
<SelectableTextBlock Text="{Binding PrivKey}"
|
||||||
|
FontFamily="monospace" FontSize="12"
|
||||||
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
<Button Content="{Binding Loc[walletinfo.seed.hide]}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).HideAddressPrivKeyCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Content="{Binding Loc[addr.close]}"
|
<Button Content="{Binding Loc[addr.close]}"
|
||||||
@@ -959,6 +973,38 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- ============ PRIVATE KEY PASSWORD PROMPT ============ -->
|
||||||
|
<!-- Appears on top of the address detail overlay when the wallet is encrypted. -->
|
||||||
|
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||||
|
Background="{DynamicResource ScrimBrush}"
|
||||||
|
IsVisible="{Binding IsPrivKeyPromptOpen}">
|
||||||
|
<Border Background="{DynamicResource OverlayCardBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="8"
|
||||||
|
MaxWidth="360" Margin="16"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<StackPanel Margin="24" Spacing="14">
|
||||||
|
<TextBlock Text="{Binding Loc[addr.privkey.prompt.title]}"
|
||||||
|
FontSize="16" FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="{Binding Loc[addr.privkey.prompt.desc]}"
|
||||||
|
FontSize="12" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox Text="{Binding PrivKeyPromptPassword}"
|
||||||
|
PasswordChar="●"
|
||||||
|
PlaceholderText="{Binding Loc[wiz.open.placeholder]}"/>
|
||||||
|
<TextBlock Text="{Binding PrivKeyPromptError}"
|
||||||
|
Foreground="{DynamicResource DangerBrush}" FontSize="12"
|
||||||
|
IsVisible="{Binding PrivKeyPromptError,
|
||||||
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
|
||||||
|
<Button Content="{Binding Loc[addr.close]}"
|
||||||
|
Command="{Binding CancelPrivKeyPromptCommand}"/>
|
||||||
|
<Button Content="{Binding Loc[walletinfo.seed.reveal]}" Classes="accent"
|
||||||
|
Command="{Binding ConfirmPrivKeyPasswordCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- ============ TRANSACTION DETAIL OVERLAY ============ -->
|
<!-- ============ TRANSACTION DETAIL OVERLAY ============ -->
|
||||||
<!-- In-app overlay (like the address overlay): appears immediately with a
|
<!-- In-app overlay (like the address overlay): appears immediately with a
|
||||||
spinner; data arrives from the server in the background; instant close. -->
|
spinner; data arrives from the server in the background; instant close. -->
|
||||||
@@ -1016,20 +1062,6 @@
|
|||||||
FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<Border Classes="chip" Margin="0,0,8,6"
|
|
||||||
Background="{DynamicResource PrimarySoftBrush}">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
||||||
<Viewbox Width="14" Height="14" VerticalAlignment="Center">
|
|
||||||
<Canvas Width="24" Height="24">
|
|
||||||
<Path Fill="{DynamicResource SuccessBrush}"
|
|
||||||
Data="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
|
|
||||||
</Canvas>
|
|
||||||
</Viewbox>
|
|
||||||
<TextBlock Text="{Binding VerifiedText}"
|
|
||||||
Foreground="{DynamicResource SuccessBrush}"
|
|
||||||
FontSize="12" FontWeight="Medium" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Classes="chip" Margin="0,0,8,6">
|
<Border Classes="chip" Margin="0,0,8,6">
|
||||||
<TextBlock Text="{Binding StatusText}" FontSize="12"
|
<TextBlock Text="{Binding StatusText}" FontSize="12"
|
||||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
@@ -1267,6 +1299,154 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- ============ WALLET INFO OVERLAY ============ -->
|
||||||
|
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||||
|
Background="{DynamicResource ScrimBrush}"
|
||||||
|
Tapped="OnWalletInfoOverlayBackdropTapped"
|
||||||
|
IsVisible="{Binding IsWalletInfoOpen}">
|
||||||
|
<Border Background="{DynamicResource OverlayCardBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="8"
|
||||||
|
MaxWidth="500" Margin="16"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<ScrollViewer MaxHeight="620">
|
||||||
|
<StackPanel Margin="24" Spacing="14">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.title]}"
|
||||||
|
FontSize="18" FontWeight="Bold"/>
|
||||||
|
|
||||||
|
<!-- File name -->
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.file]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<SelectableTextBlock Text="{Binding WalletInfoFileName}"
|
||||||
|
FontFamily="monospace" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Network -->
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.network]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Text="{Binding WalletInfoNetwork}" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Wallet type -->
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.type]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Text="{Binding WalletInfoType}" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Script kind -->
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.script]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Text="{Binding WalletInfoScriptKind}" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Derivation path (only if present) -->
|
||||||
|
<StackPanel Spacing="3"
|
||||||
|
IsVisible="{Binding WalletInfoDerivPath,
|
||||||
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.derivpath]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<SelectableTextBlock Text="{Binding WalletInfoDerivPath}"
|
||||||
|
FontFamily="monospace" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Master fingerprint (only if present) -->
|
||||||
|
<StackPanel Spacing="3"
|
||||||
|
IsVisible="{Binding WalletInfoFingerprint,
|
||||||
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.fingerprint]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<SelectableTextBlock Text="{Binding WalletInfoFingerprint}"
|
||||||
|
FontFamily="monospace" FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Account xpub (only if present) -->
|
||||||
|
<StackPanel Spacing="3"
|
||||||
|
IsVisible="{Binding WalletInfoXpub,
|
||||||
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.xpub]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<SelectableTextBlock Text="{Binding WalletInfoXpub}"
|
||||||
|
FontFamily="monospace" FontSize="11"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- BIP39 passphrase indicator -->
|
||||||
|
<StackPanel Spacing="3" IsVisible="{Binding WalletInfoHasPassphrase}">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.passphrase]}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.passphrase.set]}"
|
||||||
|
FontSize="13" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<Border Height="1" Background="{DynamicResource BorderSubtleBrush}" Margin="0,4"/>
|
||||||
|
|
||||||
|
<!-- Seed section -->
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.seed.section]}"
|
||||||
|
FontSize="13" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<!-- No seed wallet -->
|
||||||
|
<TextBlock IsVisible="{Binding !WalletInfoHasSeed}"
|
||||||
|
Text="{Binding Loc[walletinfo.seed.noseed]}"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"
|
||||||
|
FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<!-- Seed wallet: needs password -->
|
||||||
|
<StackPanel IsVisible="{Binding WalletInfoHasSeed}" Spacing="10">
|
||||||
|
|
||||||
|
<!-- Not yet revealed + password needed -->
|
||||||
|
<StackPanel IsVisible="{Binding WalletInfoSeedNeedsPassword}" Spacing="8">
|
||||||
|
<StackPanel IsVisible="{Binding !IsWalletInfoSeedRevealed}" Spacing="8">
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.seed.password]}"
|
||||||
|
FontSize="12" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox Text="{Binding WalletInfoSeedPasswordInput}"
|
||||||
|
PasswordChar="●"
|
||||||
|
PlaceholderText="{Binding Loc[wiz.open.placeholder]}"/>
|
||||||
|
<TextBlock Text="{Binding WalletInfoSeedError}"
|
||||||
|
Foreground="{DynamicResource DangerBrush}" FontSize="12"
|
||||||
|
IsVisible="{Binding WalletInfoSeedError,
|
||||||
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Content="{Binding Loc[walletinfo.seed.reveal]}"
|
||||||
|
Command="{Binding RevealSeedCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Not yet revealed + no password needed -->
|
||||||
|
<Button IsVisible="{Binding !WalletInfoSeedNeedsPassword}"
|
||||||
|
Content="{Binding Loc[walletinfo.seed.reveal]}"
|
||||||
|
Command="{Binding RevealSeedCommand}"
|
||||||
|
IsEnabled="{Binding !IsWalletInfoSeedRevealed}"/>
|
||||||
|
|
||||||
|
<!-- Revealed seed -->
|
||||||
|
<StackPanel IsVisible="{Binding IsWalletInfoSeedRevealed}" Spacing="8">
|
||||||
|
<Border BorderBrush="{DynamicResource DangerBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="14"
|
||||||
|
Background="{DynamicResource SurfaceAltBrush}">
|
||||||
|
<SelectableTextBlock Text="{Binding WalletInfoSeedText}"
|
||||||
|
FontFamily="monospace" FontSize="15"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding Loc[walletinfo.seed.warning]}"
|
||||||
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
|
FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
<Button Content="{Binding Loc[walletinfo.seed.hide]}"
|
||||||
|
Command="{Binding HideSeedCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Content="{Binding Loc[addr.close]}"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Command="{Binding CloseWalletInfoCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- ============ SETTINGS OVERLAY ============ -->
|
<!-- ============ SETTINGS OVERLAY ============ -->
|
||||||
<!-- In-app instead of nested submenus: avoids slow OS popups on WSLg. -->
|
<!-- In-app instead of nested submenus: avoids slow OS popups on WSLg. -->
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||||
|
|||||||
@@ -23,25 +23,6 @@ public partial class MainView : UserControl
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnOpenWalletFileClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not MainWindowViewModel vm) return;
|
|
||||||
if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storage) return;
|
|
||||||
|
|
||||||
var files = await storage.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
||||||
{
|
|
||||||
Title = "Apri file wallet",
|
|
||||||
AllowMultiple = false,
|
|
||||||
FileTypeFilter =
|
|
||||||
[
|
|
||||||
new FilePickerFileType("Wallet Palladium") { Patterns = ["*.wallet.json", "*.json"] },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
|
|
||||||
vm.OpenFromPath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnHistoryRowDoubleTapped(object? sender, TappedEventArgs e)
|
private void OnHistoryRowDoubleTapped(object? sender, TappedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
|
if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
|
||||||
@@ -126,6 +107,13 @@ public partial class MainView : UserControl
|
|||||||
vm.IsServerSettingsOpen = true;
|
vm.IsServerSettingsOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnWalletInfoOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(e.Source, sender)) return;
|
||||||
|
if (DataContext is MainWindowViewModel vm)
|
||||||
|
vm.CloseWalletInfoCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSettingsOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
private void OnSettingsOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(e.Source, sender)) return;
|
if (!ReferenceEquals(e.Source, sender)) return;
|
||||||
@@ -146,8 +134,10 @@ public partial class MainView : UserControl
|
|||||||
if ((e.Key == Key.Escape || e.Key == Key.Back) && DataContext is MainWindowViewModel vm)
|
if ((e.Key == Key.Escape || e.Key == Key.Back) && DataContext is MainWindowViewModel vm)
|
||||||
{
|
{
|
||||||
if (vm.IsTxDetailsOpen) { vm.CloseTransactionDetailsCommand.Execute(null); e.Handled = true; return; }
|
if (vm.IsTxDetailsOpen) { vm.CloseTransactionDetailsCommand.Execute(null); e.Handled = true; return; }
|
||||||
if (vm.AddressInfo is not null) { vm.AddressInfo = null; e.Handled = true; return; }
|
if (vm.IsPrivKeyPromptOpen) { vm.CancelPrivKeyPromptCommand.Execute(null); e.Handled = true; return; }
|
||||||
|
if (vm.AddressInfo is not null) { vm.CloseAddressInfoCommand.Execute(null); e.Handled = true; return; }
|
||||||
if (vm.IsServerSettingsOpen) { vm.IsServerSettingsOpen = false; e.Handled = true; return; }
|
if (vm.IsServerSettingsOpen) { vm.IsServerSettingsOpen = false; e.Handled = true; return; }
|
||||||
|
if (vm.IsWalletInfoOpen) { vm.CloseWalletInfoCommand.Execute(null); e.Handled = true; return; }
|
||||||
if (vm.IsSettingsOpen) { vm.IsSettingsOpen = false; e.Handled = true; return; }
|
if (vm.IsSettingsOpen) { vm.IsSettingsOpen = false; e.Handled = true; return; }
|
||||||
if (vm.IsHelpOpen) { vm.IsHelpOpen = false; e.Handled = true; return; }
|
if (vm.IsHelpOpen) { vm.IsHelpOpen = false; e.Handled = true; return; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user