Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3570da3f4 |
@@ -91,6 +91,3 @@ settings.local.json
|
||||
# Local agent/tooling metadata
|
||||
.agents/
|
||||
.codex/
|
||||
CLAUDE.md
|
||||
|
||||
blueprint.md
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Cos'è questo progetto
|
||||
|
||||
Wallet desktop SPV (stile Sparrow) per la criptovaluta **Palladium (PLM)**, una catena UTXO derivata da Bitcoin. Solo Windows e Linux, niente mobile. **Lightning è esplicitamente escluso dal primo rilascio** (§11 del blueprint).
|
||||
|
||||
**La fonte di verità è [blueprint.md](blueprint.md)**: specifica completa con parametri di consenso verificati contro il nodo, algoritmi, protocollo di rete e sequenza di costruzione. Prima di implementare qualsiasi funzionalità, leggere la sezione corrispondente del blueprint. La sequenza di costruzione da seguire è quella del §16 (profilo rete → crypto/chiavi → persistenza → rete → SPV → transazioni → GUI → hardware/multisig).
|
||||
|
||||
## Stack
|
||||
|
||||
.NET 8 + Avalonia UI + NBitcoin (§19 del blueprint). Struttura della solution:
|
||||
|
||||
```
|
||||
PalladiumWallet.sln
|
||||
├─ src/Core/ Chain/ Crypto/ Wallet/ Spv/ Net/ Storage/ (nessuna dipendenza UI)
|
||||
├─ src/App/ Avalonia UI
|
||||
├─ src/Cli/ CLI sullo stesso Core
|
||||
└─ tests/ xUnit
|
||||
```
|
||||
|
||||
**Regola di dipendenza non negoziabile:** `App` e `Cli` dipendono solo da `Core`; la GUI parla solo con l'Application API, mai direttamente con rete o crittografia. `Core` non conosce la UI.
|
||||
|
||||
## Comandi
|
||||
|
||||
Il .NET 8 SDK è installato in `~/.dotnet` (via dotnet-install.sh, senza root): nelle shell non interattive serve `export PATH="$HOME/.dotnet:$PATH" DOTNET_ROOT="$HOME/.dotnet"` prima dei comandi `dotnet`.
|
||||
|
||||
- Build: `dotnet build`
|
||||
- Test (headless, è il livello principale di verifica): `dotnet test`
|
||||
- Singolo test: `dotnet test --filter "FullyQualifiedName~NomeTest"`
|
||||
- CLI contro testnet/regtest: `dotnet run --project src/Cli -- <comando>`
|
||||
- GUI con hot reload: `dotnet watch --project src/App`
|
||||
- Su questa macchina (WSL2 con WSLg) la finestra appare direttamente sul desktop Windows: niente X server o librerie da installare, è già tutto verificato funzionante.
|
||||
- Publish Windows: `dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained`
|
||||
- Publish Linux: `dotnet publish -r linux-x64 --self-contained` (poi AppImage via PupNet Deploy)
|
||||
|
||||
La logica core e la crypto si testano senza GUI né rete reale; la GUI serve solo per rifinire l'interfaccia (§19.7).
|
||||
|
||||
## Comandi CLI principali (src/Cli)
|
||||
|
||||
`create`/`restore`/`restore-xpub`/`info` per i wallet; `sync`/`send`/`reset-certs` contro il server di indicizzazione (`--server host:porta [--ssl]`); `newseed`/`addresses` come strumenti. Eseguire senza argomenti per l'usage completo. Il file wallet di default è `~/.palladium-wallet/<rete>/wallets/default.wallet.json` (sovrascrivibile con `--file`).
|
||||
|
||||
## Architettura — punti che richiedono più file per essere capiti
|
||||
|
||||
- **Livelli (§2):** GUI → Application API → Dominio wallet → SPV/Sync → Rete → Crittografia → Persistenza. Ogni livello dipende solo verso il basso.
|
||||
- **Profilo di rete (§3):** tutte le costanti di catena (prefissi indirizzi, header BIP32, HRP bech32, genesi, porte, coin_type 746) vanno **centralizzate in `Core/Chain`** via `NetworkBuilder` e selezionabili per rete (mainnet/testnet/regtest). Nessun magic number sparso nel codice.
|
||||
- **LWMA / skip PoW (§3, §7):** la catena usa difficoltà LWMA con blocchi da 2 minuti; un client SPV non può ricalcolarla, quindi `skip_pow_validation = true` e la fiducia è ancorata ai **checkpoint hardcoded** (§7.3). Questo strato è custom: NBitcoin assume il retargeting di Bitcoin.
|
||||
- **Cosa fornisce NBitcoin vs cosa è custom (§19.2):** NBitcoin copre rete custom, BIP32/39, indirizzi, transazioni, PSBT, firma, encoding, hashing — **non reimplementarli**. Va scritto a mano: client JSON-RPC del server di indicizzazione (protocollo ElectrumX-like, §10) con pool, TLS pinning TOFU e proxy/Tor; sincronizzazione SPV con verifica Merkle (§7.4); validazione header/checkpoint; coin selection e fee policy; file wallet JSON cifrato versionato.
|
||||
- **PSBT-centrico (§6.5):** ogni flusso di firma passa per PSBT (offline, air-gapped, multisig, hardware).
|
||||
- **Le porte di default (50001/50002) sono del server di indicizzazione**, non la porta P2P del nodo (2333): il wallet SPV parla solo col server di indicizzazione.
|
||||
- **Stato implementato (passi 1–7 del §16):** `Core/Chain` profili rete; `Core/Crypto` BIP39/32/SLIP-132/HdAccount; `Core/Storage` file wallet JSON v1 + AES-GCM (PBKDF2-SHA512) + percorsi dati; `Core/Net` ElectrumClient (JSON-RPC newline su TCP/TLS, TOFU in `server-certs.json`); `Core/Spv` scripthash, verifica Merkle obbligatoria su ogni tx confermata, sincronizzazione con gap limit; `Core/Wallet` TransactionFactory (RBF on, send-all, PSBT per watch-only) e WalletLoader (doc→account). GUI Avalonia a pannello unico in `MainWindowViewModel`. Mancano (TODO §16 passi 8-9): multisig, hardware wallet, coin control UI, fee ETA/mempool, RBF/CPFP UI, header chain completa su disco, pool multi-server, proxy/Tor.
|
||||
|
||||
## Regole di lavoro
|
||||
|
||||
- **Test cross-implementazione obbligatori (§16):** ad ogni passo confrontare indirizzi, txid e PSBT con un wallet di riferimento (golden vectors). Un indirizzo o txid diverso è un bug bloccante.
|
||||
- **Sicurezza (§17):** seed e chiavi private mai in chiaro su disco, mai nei log, mai in rete; ogni risposta dei server va validata con prove di Merkle + checkpoint; watch-only realmente read-only.
|
||||
- Le funzionalità marcate *(opzionale)* nel blueprint possono essere rimandate ma vanno comunque considerate nella progettazione.
|
||||
+670
@@ -0,0 +1,670 @@
|
||||
# Blueprint — Wallet desktop per criptovaluta UTXO/SPV
|
||||
|
||||
> Specifica **autonoma e indipendente dal linguaggio** per costruire da zero un wallet
|
||||
> **solo desktop** (Windows e Linux), orientato al power-user come Sparrow Wallet.
|
||||
>
|
||||
> Il documento è pensato per essere letto e implementato **passo per passo**: contiene
|
||||
> tutto il necessario — algoritmi, strutture dati, parametri, protocollo di rete e
|
||||
> sequenza di costruzione — senza presupporre un framework, un linguaggio o un codice
|
||||
> sorgente preesistente. Ogni funzionalità elencata va considerata **parte del prodotto
|
||||
> completo**; quelle marcate *(opzionale)* possono essere rimandate a release successive
|
||||
> ma sono comunque documentate.
|
||||
|
||||
---
|
||||
|
||||
## 0. Glossario rapido
|
||||
|
||||
| Termine | Significato |
|
||||
|---|---|
|
||||
| **SPV** | Simplified Payment Verification: il wallet non scarica la catena, verifica le transazioni tramite prove di Merkle sugli header dei blocchi. |
|
||||
| **UTXO** | Unspent Transaction Output: una "moneta" non spesa; il saldo è la somma degli UTXO controllati dal wallet. |
|
||||
| **HD** | Hierarchical Deterministic: tutte le chiavi derivano da un seed unico (BIP32). |
|
||||
| **PSBT** | Partially Signed Bitcoin Transaction: formato standard per transazioni firmate parzialmente (offline, multisig, hardware). |
|
||||
| **Watch-only** | Wallet che conosce solo le chiavi pubbliche: vede saldo e storico ma non può firmare. |
|
||||
| **Server di indicizzazione** | Server che indicizza la catena e risponde alle query del client (protocollo §10). |
|
||||
| **Scripthash** | SHA-256 dello scriptPubKey con byte invertiti: chiave con cui il server indicizza gli indirizzi. |
|
||||
|
||||
---
|
||||
|
||||
## 1. Visione del prodotto
|
||||
|
||||
Un wallet **desktop nativo** con queste qualità target (modello Sparrow):
|
||||
|
||||
- **Solo desktop**: nessuna UI mobile. UX densa, orientata a trasparenza e controllo.
|
||||
- **Single-sig e multisig** di prima classe, con supporto hardware wallet.
|
||||
- **Controllo totale su monete e fee**: coin control (UTXO), selezione manuale, etichette,
|
||||
controllo fee, RBF/CPFP.
|
||||
- **PSBT-centrico**: ogni flusso di firma passa per PSBT, abilitando firma offline,
|
||||
air-gapped e collaborativa.
|
||||
- **Leggero (SPV)**: avvio immediato, nessun full node richiesto (ma con possibilità di
|
||||
collegare un server proprio).
|
||||
- **Privacy-aware**: coin selection che preserva la privacy, supporto proxy/Tor,
|
||||
possibilità di server privato.
|
||||
- **Eseguibile distribuibile**: binario per Windows e Linux, firma del codice, build
|
||||
riproducibili.
|
||||
|
||||
L'applicazione è strutturata in due grandi blocchi: un **core** (logica pura, senza UI) e
|
||||
una **GUI desktop** sopra di esso. Tutta la logica di seguito appartiene al core, tranne
|
||||
dove indicato.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architettura a livelli (target)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ GUI desktop — viste, wizard, coin control, dialog di firma │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Application API — casi d'uso: crea/apri wallet, invia, ricevi, │
|
||||
│ firma, storico, gestione canali, ecc. │
|
||||
│ Esposta anche come CLI + RPC locale (§13). │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Dominio wallet — wallet, keystore, indirizzi, UTXO, contatti, │
|
||||
│ fatture, richieste, costruzione/firma tx, │
|
||||
│ coin selection, fee policy │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ SPV / Sincronizz. — synchronizer, verifier (Merkle), gestione │
|
||||
│ header/checkpoint, saldo e storico │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Rete — pool di connessioni, selezione server, TLS │
|
||||
│ con pinning, proxy, protocollo di query (§10) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Crittografia — secp256k1, hash, BIP32/39/SLIP39, base58, │
|
||||
│ bech32, cifratura file wallet, ECIES │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Persistenza — file wallet JSON cifrato, config, percorsi │
|
||||
│ Lightning (⏳ poi) — sottosistema separato, fase successiva (§11) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Regola di dipendenza:** ogni livello dipende solo verso il basso. La GUI parla solo con
|
||||
l'Application API, mai direttamente con rete o crittografia. Questo permette di avere
|
||||
CLI, test automatici e GUI sullo stesso core.
|
||||
|
||||
### Requisiti di componenti (astratti, non legati al linguaggio)
|
||||
Indipendentemente dallo stack scelto, servono librerie/moduli che forniscano:
|
||||
1. **Curva ellittica secp256k1** (firma/verifica ECDSA + Schnorr, tweak chiavi). *Non
|
||||
reimplementare a mano la matematica della curva: usare una libreria auditata.*
|
||||
2. **Funzioni hash**: SHA-256, doppio SHA-256, RIPEMD-160, HASH160 (SHA256→RIPEMD160),
|
||||
SHA-512, HMAC-SHA512, PBKDF2-HMAC-SHA512.
|
||||
3. **Encoding**: Base58Check, Bech32 e Bech32m.
|
||||
4. **Cifratura simmetrica** (AES-256) e **ECIES** per messaggi.
|
||||
5. **TLS** con accesso al certificato per il pinning.
|
||||
6. **JSON** per file wallet e protocollo.
|
||||
7. **Generatore di numeri casuali crittografico** per seed e nonce.
|
||||
|
||||
---
|
||||
|
||||
## 3. Profilo di rete / catena (parametri da definire PRIMA di tutto)
|
||||
|
||||
Un wallet è legato a una catena specifica tramite un insieme di costanti. **Vanno fissate
|
||||
all'inizio**: un valore sbagliato produce indirizzi non validi o fa rifiutare la catena.
|
||||
Definire un oggetto/struct di configurazione con i seguenti campi.
|
||||
|
||||
> I valori del profilo di riferimento qui sotto sono stati **verificati contro il sorgente
|
||||
> del nodo** (`chainparams.cpp` / `pow.cpp`): sono i parametri di consenso autoritativi.
|
||||
|
||||
| Campo | Descrizione | Esempio (profilo di riferimento mainnet) |
|
||||
|---|---|---|
|
||||
| `net_name` | nome rete / sottocartella dati | `mainnet` |
|
||||
| `coin_unit` | simbolo unità | `PLM` |
|
||||
| `wif_prefix` | prefisso chiavi private WIF | `0x80` |
|
||||
| `addr_p2pkh` | byte versione indirizzi legacy | `55` → indirizzi che iniziano con `P` |
|
||||
| `addr_p2sh` | byte versione indirizzi P2SH | `5` → iniziano con `3` |
|
||||
| `segwit_hrp` | prefisso human-readable bech32 | `plm` → indirizzi `plm1...` |
|
||||
| `bolt11_hrp` | prefisso fatture Lightning | `plm` |
|
||||
| `genesis_hash` | hash del blocco genesi (mainnet riusa la genesi di Bitcoin) | `000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f` |
|
||||
| `default_ports` | porte del **server di indicizzazione** usato dal wallet | `{tcp: 50001, ssl: 50002}` |
|
||||
| `bip44_coin_type` | coin type SLIP-0044 nel derivation path (*convenzione wallet, non parametro di consenso del nodo*) | `746` |
|
||||
| `uri_scheme` | schema URI pagamenti (BIP21) | `palladium:` |
|
||||
| `explorer_url` | block explorer di default | `https://explorer.palladium-coin.com/` |
|
||||
| `skip_pow_validation` | salta la verifica difficoltà — **obbligatorio**: la catena usa LWMA (vedi nota sotto) | `true` |
|
||||
|
||||
> **Nota sulle porte.** Le `default_ports` sopra sono quelle del **server di indicizzazione**
|
||||
> (es. ElectrumX-like) a cui si connette il wallet, **non** la porta P2P del nodo (che nel
|
||||
> sorgente è `2333` mainnet / `12333` testnet / `28444` regtest). Sono due cose distinte: il
|
||||
> wallet SPV parla solo col server di indicizzazione.
|
||||
>
|
||||
> **Nota sulla difficoltà (LWMA).** Il nodo calcola la difficoltà con **LWMA** (Linearly
|
||||
> Weighted Moving Average) e un **tempo di blocco di 2 minuti** (`nPowTargetSpacingV2 = 120s`;
|
||||
> il vecchio `nPowTargetTimespan` di 14 giorni è mantenuto solo per la validazione storica).
|
||||
> Un client SPV non è in grado di ricalcolare LWMA, quindi `skip_pow_validation` **deve**
|
||||
> essere `true` e la fiducia sulla catena va ancorata ai **checkpoint** (§7.3).
|
||||
|
||||
**Header chiavi estese BIP32** (mainnet di riferimento) — servono per serializzare/parsare
|
||||
xprv/xpub per ciascun tipo di indirizzo:
|
||||
|
||||
| Tipo indirizzo | prefisso priv | header priv | prefisso pub | header pub |
|
||||
|---|---|---|---|---|
|
||||
| standard (P2PKH) | xprv | `0x0488ade4` | xpub | `0x0488b21e` |
|
||||
| segwit wrapped (P2WPKH-P2SH) | yprv | `0x049d7878` | ypub | `0x049d7cb2` |
|
||||
| multisig wrapped (P2WSH-P2SH) | Yprv | `0x0295b005` | Ypub | `0x0295b43f` |
|
||||
| native segwit (P2WPKH) | zprv | `0x04b2430c` | zpub | `0x04b24746` |
|
||||
| native segwit multisig (P2WSH) | Zprv | `0x02aa7a99` | Zpub | `0x02aa7ed3` |
|
||||
|
||||
**Testnet** (profilo di riferimento): `wif_prefix=0xff`, `addr_p2pkh=127`, `addr_p2sh=115`,
|
||||
`segwit_hrp=tplm`, `bip44_coin_type=1`, header tprv/tpub `0x04358394`/`0x043587cf`, ecc.
|
||||
**Regtest**: `segwit_hrp=rplm`.
|
||||
|
||||
**Server iniziali** (bootstrap, profilo di riferimento): forniti come lista
|
||||
`host:porta_tcp:porta_ssl`; il pool li usa per il primo contatto e poi scopre altri peer
|
||||
via protocollo (`server.peers.subscribe`).
|
||||
|
||||
> Tutte le costanti devono essere **centralizzate** in un solo punto e selezionabili per
|
||||
> rete (mainnet/testnet/regtest), così da non disperdere magic number nel codice.
|
||||
|
||||
---
|
||||
|
||||
## 4. Crittografia, seed e gestione chiavi
|
||||
|
||||
### 4.1 Seed e mnemoniche — tutti gli schemi supportati
|
||||
Il wallet deve supportare **più schemi di seed** e saperli riconoscere automaticamente:
|
||||
|
||||
1. **Seed nativo versionato**: mnemonica con un prefisso di versione codificato via HMAC
|
||||
(distingue i sottotipi `standard`, `segwit`, `2fa`, `2fa-segwit`). In creazione si
|
||||
garantisce che la mnemonica generata **non** sia contemporaneamente un valido BIP39.
|
||||
2. **BIP39**: import/restore di seed standard a **12 o 24 parole**, con verifica del
|
||||
checksum.
|
||||
3. **SLIP39 (Shamir Secret Sharing)** *(opzionale)*: seed suddiviso in più *share*; servono
|
||||
K share su N per ricostruirlo.
|
||||
4. **Formato seed legacy** *(opzionale, retrocompatibilità)*: vecchio schema pre-BIP32.
|
||||
|
||||
**Wordlist multilingua**: inglese, spagnolo, giapponese, portoghese, cinese. Le parole
|
||||
vanno normalizzate (NFKD) prima della derivazione.
|
||||
|
||||
**Passphrase / extension word (opzionale ma obbligatoria da implementare):** parola/e
|
||||
aggiuntive combinate col seed tramite **PBKDF2-HMAC-SHA512, 2048 round**, con salt
|
||||
`"<schema>" + passphrase` (per il seed nativo il prefisso del salt è una costante dello
|
||||
schema; per BIP39 è `"mnemonic" + passphrase`). Cambia completamente il wallet derivato.
|
||||
Avvisi UI obbligatori: se persa, i fondi sono **irrecuperabili**; va annotata separatamente
|
||||
dal seed; è case-sensitive e gli spazi contano.
|
||||
|
||||
### 4.2 Derivazione gerarchica (BIP32/BIP44/49/84)
|
||||
- Da seed → **root key** (BIP32). Da root → chiavi estese per account.
|
||||
- Path standard con il `coin_type` del profilo:
|
||||
- Legacy P2PKH: `m/44'/<coin>'/account'/change/index`
|
||||
- Segwit wrapped P2SH-P2WPKH: `m/49'/<coin>'/account'/change/index`
|
||||
- Native segwit P2WPKH: `m/84'/<coin>'/account'/change/index`
|
||||
- Multisig: `m/48'/<coin>'/account'/script_type'/...`
|
||||
- Supportare **derivation path personalizzati** (Sparrow-like): l'utente può specificare il
|
||||
path manualmente in import.
|
||||
- Catena `change=0` (receiving) e `change=1` (change). Derivazione per indice on-demand.
|
||||
|
||||
### 4.3 Tipi di indirizzo (tutti)
|
||||
| Tipo | script | prefisso (profilo rif.) | uso |
|
||||
|---|---|---|---|
|
||||
| Native SegWit | P2WPKH | `plm1...` | **default consigliato**: fee minime |
|
||||
| Legacy | P2PKH | `P...` | massima compatibilità |
|
||||
| SegWit wrapped | P2SH-P2WPKH | `3...` | compatibilità intermedia |
|
||||
| Multisig native | P2WSH | `plm1...` | M-di-N moderno |
|
||||
| Multisig wrapped | P2SH / P2SH-P2WSH | `3...` | M-di-N legacy |
|
||||
|
||||
### 4.4 Tipi di keystore (sorgenti di chiavi)
|
||||
- **HD da seed** (caso principale): seed → root → xprv/xpub.
|
||||
- **HD da master key importata**: import di xprv/xpub (o y/z varianti). Solo xpub = watch-only.
|
||||
- **Chiavi private importate**: lista di chiavi WIF singole.
|
||||
- **Hardware wallet**: la chiave privata non lascia mai il dispositivo (vedi §4.6).
|
||||
- **Keystore "split/2FA"** *(opzionale)*: parte della firma delegata a un servizio remoto.
|
||||
- **Keystore legacy** *(opzionale)*.
|
||||
|
||||
### 4.5 Tipi di wallet
|
||||
- **Standard** (single-sig HD) — caso d'uso principale.
|
||||
- **Multisig M-di-N** — combinazione di più keystore (seed/xpub/hardware misti).
|
||||
- **Importato** — indirizzi o chiavi private importate; può essere watch-only o spendibile.
|
||||
- **Watch-only** — solo chiavi pubbliche; costruisce ma non firma (esporta PSBT).
|
||||
|
||||
Una factory legge il tipo dal file wallet e istanzia la classe corretta.
|
||||
|
||||
### 4.6 Hardware wallet (tutti i modelli supportati)
|
||||
Integrazione con dispositivi hardware via il loro protocollo USB/HID/seriale. Modelli da
|
||||
supportare: **Trezor, Ledger, KeepKey, Coldcard, BitBox02, Digital BitBox, Safe-T, Jade**.
|
||||
Funzioni: import dell'xpub dal dispositivo, conferma indirizzo sullo schermo del device,
|
||||
firma PSBT sul device (la chiave privata non viene mai esposta), gestione PIN/passphrase.
|
||||
Per i dispositivi air-gapped (es. Coldcard): scambio PSBT via file/QR/microSD.
|
||||
|
||||
### 4.7 Backup e sicurezza delle chiavi
|
||||
- **Cifratura del file wallet** con password (vedi §8); cambio password.
|
||||
- **Cifratura/decifratura messaggi** e firma/verifica messaggi con una chiave.
|
||||
- **Export**: seed, master private key, master public key, chiavi private (per indirizzo o
|
||||
per path), in modo protetto (richiesta password).
|
||||
- **Backup su carta con visual one-time-pad** *(opzionale, "revealer")*: genera un foglio
|
||||
cifrato che, sovrapposto a una griglia segreta stampata, rivela il seed.
|
||||
- **Recupero a timelock** *(opzionale)*: predisposizione di transazioni di recupero che
|
||||
diventano spendibili dopo un timelock, per ereditarietà/dead-man-switch.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ricezione fondi
|
||||
|
||||
- **Generazione indirizzi**: prossimo indirizzo non usato; lista indirizzi receiving e
|
||||
change; rispetto del **gap limit** (numero massimo di indirizzi vuoti consecutivi
|
||||
scansionati; configurabile, con comando per aumentarlo).
|
||||
- **Richieste di pagamento**: creare una richiesta con importo, scadenza, descrizione;
|
||||
elencarle, eliminarle, segnarne lo stato (in attesa/pagata/scaduta).
|
||||
- **URI BIP21**: generare e parsare `«scheme»:«indirizzo»?amount=...&label=...&message=...`.
|
||||
- **QR code**: generazione per indirizzi/URI/richieste; scansione da webcam o immagine.
|
||||
- **Risoluzione nomi**: OpenAlias/DNS, LNURL, e richieste firmate BIP70 *(opzionale)*.
|
||||
- **Etichette** sugli indirizzi e sulle richieste.
|
||||
|
||||
---
|
||||
|
||||
## 6. Invio fondi e costruzione transazioni
|
||||
|
||||
### 6.1 Composizione
|
||||
- **Pay-to** singolo e **pay-to-many** (più output in una sola transazione).
|
||||
- Input destinatario: indirizzo, URI, nome OpenAlias/LNURL, o richiesta scansionata.
|
||||
- Importo in unità coin o in fiat (con conversione al tasso corrente).
|
||||
- Opzione **"invia tutto"** (max), con sottrazione fee dall'importo.
|
||||
- **Locktime** e **sequence** impostabili (per RBF/timelock).
|
||||
- Etichetta della transazione.
|
||||
|
||||
### 6.2 Coin control (cuore di un wallet desktop)
|
||||
- Vista UTXO completa con importi, indirizzo, conferme, etichetta.
|
||||
- **Selezione manuale** degli UTXO da spendere (coin control).
|
||||
- **Freeze/unfreeze** di indirizzi e di singoli UTXO (esclusi dalla spesa automatica).
|
||||
- Visualizzazione del *change* previsto e dell'indirizzo di change.
|
||||
|
||||
### 6.3 Strategie di selezione monete (coin selection)
|
||||
Implementare almeno due strategie selezionabili:
|
||||
- **Privacy-preserving**: raggruppa gli UTXO per indirizzo (evita di unire monete di
|
||||
indirizzi diversi), riduce la perdita di privacy futura e il bloat di UTXO.
|
||||
- **Random**: selezione casuale con arrotondamento del change per offuscare gli importi.
|
||||
Obiettivi comuni: minimizzare fee, evitare output *dust*, gestire il change correttamente.
|
||||
|
||||
### 6.4 Politiche di fee (tutte)
|
||||
Modalità di calcolo fee, selezionabili:
|
||||
- **Fissa** (importo assoluto).
|
||||
- **Fee rate fisso** (sat/vByte).
|
||||
- **Dinamica ETA-based**: stima dal server per un target di conferma (es. 1, 2, 5, 10, 25,
|
||||
144, 1008 blocchi).
|
||||
- **Dinamica mempool-based**: in base allo stato della mempool.
|
||||
Mostrare sempre fee totale, fee rate effettivo e dimensione virtuale stimata. Avviso se la
|
||||
fee è anomala (troppo alta/bassa).
|
||||
|
||||
### 6.5 Firma e modello PSBT
|
||||
Ogni transazione non banale passa per **PSBT**:
|
||||
- **Crea** PSBT (non firmata) dal wallet.
|
||||
- **Firma** con: keystore software, hardware wallet, o firma offline su altra macchina.
|
||||
- **Combina** PSBT parzialmente firmate (multisig: ogni cosigner firma e si uniscono).
|
||||
- **Finalizza** ed estrai la transazione grezza.
|
||||
- **Import/export PSBT** via: file, **QR code** (anche animato per PSBT grandi),
|
||||
**scambio su rete decentralizzata per cosigning** *(opzionale)*, **trasmissione audio**
|
||||
*(opzionale, "audio modem")*.
|
||||
- Flusso **watch-only / air-gapped**: la macchina online crea la PSBT, quella offline firma,
|
||||
la online trasmette.
|
||||
|
||||
### 6.6 Gestione post-invio
|
||||
- **Broadcast** della transazione (e broadcast di un *pacchetto* di transazioni correlate).
|
||||
- **RBF (Replace-By-Fee)**: bump della fee di una tx non confermata; **cancellazione**
|
||||
(invio a sé stessi con fee più alta).
|
||||
- **CPFP (Child-Pays-For-Parent)**: accelerare una tx in entrata spendendola con fee alta.
|
||||
- **Rimozione di tx locali** non confermate.
|
||||
- **Sweep**: spazzare tutti i fondi di una chiave privata esterna verso il wallet.
|
||||
- **Aggiunta manuale di una tx** (incolla raw/hex) e firma con chiave fornita.
|
||||
|
||||
### 6.7 Reportistica
|
||||
- **Plusvalenze/minusvalenze on-chain** (capital gains) per anno fiscale.
|
||||
- Esportazione storico (CSV/JSON) con etichette.
|
||||
- Timestamp di inizio/fine anno per i report.
|
||||
|
||||
---
|
||||
|
||||
## 7. Blockchain, sincronizzazione e validazione (SPV)
|
||||
|
||||
### 7.1 Modello SPV
|
||||
Il wallet **non scarica la catena completa**. Mantiene solo gli **header dei blocchi** e
|
||||
verifica le transazioni che lo riguardano con **prove di Merkle** contro tali header.
|
||||
|
||||
### 7.2 Validazione header
|
||||
- Header a lunghezza fissa (campi: versione, prev_hash, merkle_root, timestamp, bits, nonce).
|
||||
- Verifica del **collegamento** (prev_hash) e dell'**hash atteso** di ogni header.
|
||||
- **Proof-of-Work**: confronto `hash <= target` e coerenza dei `bits`.
|
||||
- **Difficoltà**: la catena di riferimento usa **LWMA** (retargeting per-blocco, tempo di
|
||||
blocco 2 minuti — confermato dal nodo). Un client SPV non ricalcola LWMA, quindi il flag
|
||||
`skip_pow_validation` del profilo (§3) **disattiva** il controllo bits/target; in tal caso
|
||||
la fiducia è ancorata ai **checkpoint** (sotto). Implementare entrambe le modalità (PoW
|
||||
classico stile Bitcoin e modalità "skip" per catene LWMA).
|
||||
- Header organizzati in **chunk** (es. 2016) salvati su file locale; gestione di **fork**
|
||||
(più rami concorrenti) con scelta del ramo a maggior lavoro.
|
||||
|
||||
### 7.3 Checkpoint
|
||||
Lista hardcoded di `[hash, target]` a intervalli regolari, usata per:
|
||||
- accelerare la validazione iniziale (non riverificare dall'origine),
|
||||
- ancorare la validità della catena quando la verifica PoW è disattivata.
|
||||
Fornire un meccanismo per aggiornare/spedire i checkpoint con le release.
|
||||
|
||||
### 7.4 Sincronizzazione wallet
|
||||
1. Per ogni indirizzo: calcola lo **scripthash**, sottoscrivi al server.
|
||||
2. Alla notifica di cambiamento: richiedi lo **storico** (lista txid + altezza).
|
||||
3. Scarica le transazioni mancanti.
|
||||
4. **Verifica** ciascuna con prova di Merkle contro l'header all'altezza indicata.
|
||||
5. Aggiorna saldo (confermato/non confermato), UTXO e storico.
|
||||
6. Estendi la scansione finché si raggiunge il gap limit di indirizzi vuoti.
|
||||
|
||||
---
|
||||
|
||||
## 8. Persistenza e configurazione
|
||||
|
||||
- **File wallet**: un file (JSON) contenente keystore, indirizzi, storico, etichette,
|
||||
contatti, richieste, fatture, (e stato canali Lightning se attivo). **Cifrato** con AES
|
||||
quando è impostata una password (la password protegge il file su disco; **non** sblocca
|
||||
il seed). Schema **versionato** con migrazioni automatiche all'apertura.
|
||||
- **Configurazione globale** separata dal wallet: rete selezionata, server, proxy, unità,
|
||||
lingua, politiche fee di default, block explorer.
|
||||
- **Percorsi dati** per piattaforma + **modalità portable** (dati accanto all'eseguibile,
|
||||
utile per chiavette USB).
|
||||
- **Multi-wallet**: aprire/chiudere più wallet, elencarli, passare dall'uno all'altro.
|
||||
|
||||
---
|
||||
|
||||
## 9. Rete e connettività
|
||||
|
||||
- **Pool di connessioni**: più server contemporaneamente per ridondanza; un server
|
||||
"primario" per gli header; fan-out delle query.
|
||||
- **Selezione server**: automatica o manuale; scoperta di nuovi peer dal protocollo.
|
||||
- **TLS con pinning (TOFU)**: al primo contatto il certificato del server viene salvato; ai
|
||||
successivi viene confrontato. Se cambia → connessione rifiutata. Fornire un comando
|
||||
**"reset certificati SSL"** per i server self-signed (caso tipico: il server rinnova il
|
||||
certificato e il client va sbloccato manualmente).
|
||||
- **Proxy / Tor**: supporto SOCKS5 per instradare tutto il traffico.
|
||||
- **Stima fee** dal server; **relay fee** minima.
|
||||
- **Stato connessione** visibile in UI; riconnessione automatica.
|
||||
|
||||
---
|
||||
|
||||
## 10. Protocollo client ↔ server (query di indicizzazione)
|
||||
|
||||
Il client comunica via **JSON-RPC** (su TCP, opzionalmente TLS). Implementare richieste e
|
||||
parsing per i seguenti metodi (gli indirizzi sono indicizzati per **scripthash**):
|
||||
|
||||
```
|
||||
# Negoziazione / server
|
||||
server.version server.banner server.features
|
||||
server.ping server.peers.subscribe server.donation_address
|
||||
|
||||
# Header / catena
|
||||
blockchain.headers.subscribe blockchain.block.header blockchain.block.headers
|
||||
blockchain.estimatefee blockchain.relayfee
|
||||
|
||||
# Indirizzi (per scripthash)
|
||||
blockchain.scripthash.subscribe blockchain.scripthash.get_balance
|
||||
blockchain.scripthash.get_history blockchain.scripthash.listunspent
|
||||
|
||||
# Transazioni
|
||||
blockchain.transaction.get blockchain.transaction.get_merkle
|
||||
blockchain.transaction.broadcast blockchain.transaction.broadcast_package
|
||||
blockchain.transaction.id_from_pos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Lightning Network — ⏳ DA FARE IN SEGUITO (fase successiva, fuori dal primo rilascio)
|
||||
|
||||
> **Non incluso nel primo rilascio.** Coerentemente con il modello Sparrow (on-chain only),
|
||||
> il wallet parte **senza Lightning**. Questo capitolo resta documentato come specifica per
|
||||
> una **fase successiva**: va affrontato solo dopo che tutte le funzioni on-chain (§4–§10)
|
||||
> sono complete e stabili. Va progettato come **sottosistema separato e disattivabile**, in
|
||||
> modo da non bloccare né complicare il primo rilascio.
|
||||
|
||||
Quando verrà affrontato, è un grande sottosistema a sé. Funzionalità da implementare:
|
||||
|
||||
- **Canali**: apertura, chiusura cooperativa e forzata, lista canali e peer.
|
||||
- **Pagamenti**: invio/ricezione su BOLT11, **MPP** (multi-part payments), **trampoline
|
||||
routing**, onion routing, gossip/routing della rete.
|
||||
- **Fatture**: creazione/decodifica BOLT11; **hold invoice** (trattenute fino a conferma).
|
||||
- **Backup canali**: export/import dei backup di stato (critici per non perdere fondi).
|
||||
- **Watchtower**: locale e remoto, per punire chiusure fraudolente quando offline.
|
||||
- **Submarine swap**: scambio on-chain ↔ Lightning, con provider/server di swap;
|
||||
rebalance dei canali.
|
||||
- **LNURL** e indirizzi Lightning (lightning-address).
|
||||
- **Nostr Wallet Connect (NWC)** *(opzionale)*: controllo remoto del wallet Lightning.
|
||||
- **Payserver** *(opzionale)*: endpoint per ricevere pagamenti.
|
||||
|
||||
> **Promemoria:** per il primo rilascio Lightning è escluso. Le funzioni base (§4–§10) non
|
||||
> dipendono in alcun modo da questo capitolo; affrontarlo solo come iterazione successiva.
|
||||
|
||||
---
|
||||
|
||||
## 12. Contatti, etichette e dati ausiliari
|
||||
|
||||
- **Rubrica contatti**: nome ↔ indirizzo, con ricerca.
|
||||
- **Etichette** su indirizzi, transazioni, UTXO, richieste.
|
||||
- **Sincronizzazione etichette** *(opzionale)*: cifrate, condivise tra istanze del wallet
|
||||
via un servizio.
|
||||
- **Lista fatture** (uscite) e **lista richieste** (entrate) con stato.
|
||||
|
||||
---
|
||||
|
||||
## 13. Interfacce non grafiche
|
||||
|
||||
- **CLI**: ogni caso d'uso del core esposto come comando da riga di comando (utile per
|
||||
scripting e per i test automatici).
|
||||
- **RPC locale / daemon** *(opzionale)*: un processo in background che espone l'API su
|
||||
socket locale autenticato, così la GUI e strumenti esterni parlano con lo stesso core.
|
||||
|
||||
Famiglie di comandi da prevedere (elenco rappresentativo della superficie API completa):
|
||||
creazione/restore/apertura/chiusura wallet, generazione indirizzi, saldo e storico,
|
||||
pay-to / pay-to-many, firma/broadcast, gestione PSBT (deserialize/combine/finalize),
|
||||
freeze/unfreeze UTXO, bumpfee/cancel, sweep, import/export chiavi e xkey, conversione xkey,
|
||||
firma/verifica messaggi, gestione richieste e fatture, contatti, conversione fiat,
|
||||
stato sincronizzazione, gestione server/config. *(Comandi Lightning — apertura/chiusura
|
||||
canali, pagamenti, swap, backup canali — solo nella fase successiva, vedi §11.)*
|
||||
|
||||
---
|
||||
|
||||
## 14. Funzioni di supporto e infrastruttura
|
||||
|
||||
- **Tassi di cambio / fiat**: integrazione con più provider di prezzo; conversione importi
|
||||
in valuta locale; **prezzi storici** per la reportistica; aggiornamento periodico.
|
||||
- **Internazionalizzazione (i18n)**: UI multilingua, con rilevamento lingua di sistema.
|
||||
- **Crash reporter**: raccolta e invio (con consenso) dei crash.
|
||||
- **Sistema di plugin** *(opzionale)*: caricamento di estensioni (hardware wallet, swap
|
||||
server, watchtower, label sync, ecc.) come moduli separati.
|
||||
- **Hardening memoria** *(opzionale)*: blocco in RAM (no swap su disco) dei segreti dove il
|
||||
SO lo consente; azzeramento dei buffer sensibili dopo l'uso.
|
||||
- **Block explorer**: apertura di tx/indirizzi nell'explorer configurato.
|
||||
|
||||
---
|
||||
|
||||
## 15. Wizard di creazione/restore (flusso UI)
|
||||
|
||||
1. Avvio: **crea nuovo wallet** / **apri esistente** / **importa**.
|
||||
2. Tipo wallet: **Standard** / **Multisig (M-di-N)** / **Importa indirizzi o chiavi** /
|
||||
**Hardware**.
|
||||
3. Per Standard: **nuovo seed** / **ho già un seed** / **usa master key** / **usa device hardware**.
|
||||
4. Nuovo seed → mostra le parole → **conferma** reinserendole.
|
||||
5. **Passphrase** opzionale (extension word) con avvisi (§4.1).
|
||||
6. Scelta **tipo di indirizzo** (default: native segwit).
|
||||
7. **Password** di cifratura del file wallet.
|
||||
8. Sincronizzazione e comparsa di saldo/storico.
|
||||
|
||||
Per multisig: raccolta di N cosigner (seed/xpub/hardware), scelta soglia M, derivation path
|
||||
e tipo di script; generazione del descrittore del wallet e verifica incrociata degli xpub
|
||||
tra i partecipanti.
|
||||
|
||||
---
|
||||
|
||||
## 16. Sequenza di costruzione consigliata (passo per passo)
|
||||
|
||||
Costruire e **testare** in quest'ordine; ad ogni passo verificare con vettori noti.
|
||||
|
||||
1. **Profilo di rete (§3)**: centralizzare tutte le costanti; selettore mainnet/testnet/regtest.
|
||||
2. **Crittografia e chiavi (§4)**: hash, secp256k1, base58/bech32, BIP32/39, generazione
|
||||
indirizzi. *Test:* dato un seed → produrre gli indirizzi attesi (golden vectors).
|
||||
3. **Persistenza (§8)**: definire e versionare lo schema del file wallet (JSON) + cifratura.
|
||||
4. **Rete + protocollo (§9–10)**: connessione, TLS+pinning, query base.
|
||||
5. **SPV + sincronizzazione (§7)**: header, checkpoint, Merkle, saldo/storico su un wallet
|
||||
**watch-only**. *Test:* stesso xpub → stesso saldo di un wallet di riferimento.
|
||||
6. **Transazioni (§6)**: costruzione, coin selection, fee, PSBT, firma, broadcast su testnet.
|
||||
7. **GUI desktop (§15 + viste)**: wizard, dashboard saldo/storico, invia (con coin control),
|
||||
ricevi, UTXO, contatti, impostazioni.
|
||||
8. **Hardware wallet (§4.6)** e **multisig (§4.5)**: firma collaborativa via PSBT.
|
||||
9. **Estensioni**: fiat/exchange rate, label sync, reportistica.
|
||||
10. **⏳ Fase successiva (post-rilascio)**: **Lightning (§11)** come sottosistema separato,
|
||||
da iniziare solo quando i passi 1–9 sono completi e stabili.
|
||||
|
||||
**Test cross-implementazione (obbligatorio per un wallet):** ad ogni passo confrontare gli
|
||||
output (indirizzi, txid, PSBT) con un wallet di riferimento usando gli stessi input. Un
|
||||
indirizzo o un txid diverso è un bug bloccante.
|
||||
|
||||
---
|
||||
|
||||
## 17. Requisiti di sicurezza (non negoziabili)
|
||||
|
||||
- Seed e chiavi private **mai** in chiaro su disco non cifrato, **mai** nei log, **mai**
|
||||
inviati in rete.
|
||||
- Cifratura del file wallet con derivazione robusta della chiave dalla password.
|
||||
- Validare **ogni** dato proveniente dalla rete: le risposte dei server **non sono fidate**;
|
||||
verificare sempre con prove di Merkle + checkpoint.
|
||||
- Watch-only realmente read-only: nessuna chiave privata derivabile dalle sole pubbliche.
|
||||
- TLS con pinning del certificato e reset esplicito controllato dall'utente.
|
||||
- Azzeramento dei segreti in memoria dopo l'uso; ove possibile blocco anti-swap.
|
||||
- **Firma del codice** dei binari Windows/Linux e **build riproducibili** per consentire la
|
||||
verifica indipendente.
|
||||
|
||||
---
|
||||
|
||||
## 18. Packaging e distribuzione desktop
|
||||
|
||||
- **Windows**: eseguibile installabile e versione **portable** (dati accanto all'exe).
|
||||
Firma Authenticode.
|
||||
- **Linux**: formato portabile autoconsistente (es. immagine eseguibile singola) e/o
|
||||
pacchetto nativo; firma GPG dei rilasci.
|
||||
- **Build riproducibili** (ambiente di build isolato/containerizzato) e pubblicazione degli
|
||||
hash + firme dei binari.
|
||||
- File di associazione per lo schema URI dei pagamenti (`uri_scheme` del profilo) così che i
|
||||
link di pagamento aprano il wallet.
|
||||
|
||||
---
|
||||
|
||||
## 19. Stack tecnologico raccomandato (.NET 8 + Avalonia + NBitcoin)
|
||||
|
||||
> I capitoli §1–§18 sono **indipendenti dal linguaggio** e restano il riferimento. Questa
|
||||
> sezione propone una **realizzazione concreta** scelta per tre vincoli: sviluppo assistito
|
||||
> da IA, **semplicità**, e **un solo sorgente** che produca sia `.exe` (Windows) sia
|
||||
> AppImage (Linux). Lo stack non è obbligatorio: è la via più liscia per *questo* prodotto.
|
||||
|
||||
### 19.1 Perché questo stack
|
||||
- **`NBitcoin` (C#)** modella reti altcoin custom via `NetworkBuilder` (prefissi
|
||||
P2PKH/P2SH, WIF, header BIP32 xpub/xprv, HRP bech32, genesi): mappatura **diretta** della
|
||||
§3. Fornisce già HD/BIP32/39, indirizzi (legacy/segwit/wrapped), costruzione e
|
||||
serializzazione transazioni, **PSBT**, firma, base58/bech32, hashing → è la libreria che
|
||||
fa scrivere **meno crittografia a mano** per un altcoin.
|
||||
- **Avalonia UI** è cross-platform nativo: un solo sorgente per Windows e Linux.
|
||||
- **C#/.NET** ha tooling maturo e un enorme corpus → l'IA produce codice idiomatico con
|
||||
poche frizioni; alto livello e GC = sviluppo rapido.
|
||||
- Posizione di **sicurezza accettabile** sulle chiavi senza la curva di apprendimento di
|
||||
Rust e senza i rischi di Electron (chiavi in JS, footprint, supply-chain npm).
|
||||
|
||||
*Alternative scartate:* **Rust + BDK + Tauri** (binari minimi e memory-safe, ma curva
|
||||
ripida e customizzazione altcoin più laboriosa → contro "semplicità"); **Electron + TS**
|
||||
(packaging e UI rapidissimi, ma gestione chiavi più fragile → sconsigliato per un wallet);
|
||||
**Java/JavaFX** (è lo stack di Sparrow e produce già `.exe`+AppImage, ma nessun vantaggio
|
||||
netto su .NET per questo caso).
|
||||
|
||||
### 19.2 Cosa è coperto dalla libreria e cosa va scritto a mano
|
||||
**Coperto da NBitcoin** (non reimplementare): rete custom, BIP32/39, indirizzi,
|
||||
transazioni, PSBT, firma, base58/bech32, hashing.
|
||||
|
||||
**Da implementare a mano** (NBitcoin non lo include — è il grosso del lavoro originale):
|
||||
1. **Client del protocollo del server di indicizzazione** (§10): JSON-RPC su TCP/TLS,
|
||||
pool di connessioni, TLS pinning/TOFU, reset certificati, proxy/Tor.
|
||||
2. **Sincronizzazione SPV** (§7.4): scripthash, storico, verifica prove di Merkle.
|
||||
3. **Validazione header + checkpoint con modalità "skip PoW"** per LWMA (§3/§7):
|
||||
NBitcoin assume il retargeting di Bitcoin, quindi questo strato è **custom**.
|
||||
4. **Coin selection** privacy-preserving e **fee policy** (fissa/rate/ETA/mempool),
|
||||
RBF/CPFP (§6): logica di dominio sopra le primitive NBitcoin.
|
||||
5. **File wallet cifrato** (schema JSON versionato + AES) e **config** (§8).
|
||||
|
||||
### 19.3 Mappatura strato del blueprint → componente concreto
|
||||
| Strato (§2) | Realizzazione |
|
||||
|---|---|
|
||||
| Crittografia (§4) | NBitcoin (`Network` custom, `ExtKey`, `Mnemonic`, `BitcoinAddress`, `PSBT`) |
|
||||
| Profilo rete (§3) | `NetworkBuilder` con i valori §3, centralizzato in `Core/Chain` |
|
||||
| SPV/Sync (§7) | codice custom in `Core/Spv` |
|
||||
| Rete/protocollo (§9–10) | client custom in `Core/Net` |
|
||||
| Dominio wallet (§4–§6) | `Core/Wallet` sopra NBitcoin |
|
||||
| Persistenza (§8) | `System.Text.Json` + AES in `Core/Storage` |
|
||||
| Application API (§13) | progetti `Cli` e API condivisa |
|
||||
| GUI desktop (§15) | Avalonia in `App` |
|
||||
|
||||
### 19.4 Struttura del progetto (una sola solution .NET)
|
||||
```
|
||||
PalladiumWallet.sln
|
||||
├─ src/Core/ (libreria, nessuna dipendenza UI)
|
||||
│ ├─ Chain/ profilo rete (NetworkBuilder), costanti §3, checkpoint
|
||||
│ ├─ Crypto/ wrapper NBitcoin: seed, BIP32/39, indirizzi, keystore
|
||||
│ ├─ Wallet/ wallet, UTXO, coin selection, fee policy, PSBT, firma
|
||||
│ ├─ Spv/ header store, verifier (Merkle), sync
|
||||
│ ├─ Net/ client protocollo, pool, TLS pinning, proxy
|
||||
│ └─ Storage/ file wallet JSON cifrato, config
|
||||
├─ src/App/ (Avalonia UI: wizard, dashboard, invia/ricevi, coin control)
|
||||
├─ src/Cli/ (riga di comando sullo stesso Core — utile ai test)
|
||||
└─ tests/ (xUnit: golden vectors indirizzi/txid, test SPV)
|
||||
```
|
||||
Regola di dipendenza (§2): `App` e `Cli` dipendono da `Core`; `Core` non conosce la UI.
|
||||
|
||||
### 19.5 Ambiente di sviluppo e build — tutto su Ubuntu, senza Wine
|
||||
- **Dev**: `.NET 8 SDK` (repo Microsoft / `apt`), VS Code + estensione C# o Rider.
|
||||
Avalonia gira ed è eseguibile nativamente su Ubuntu.
|
||||
- **Architetture host**: si sviluppa sia su **x86_64** sia su **arm64** (il .NET SDK è
|
||||
nativo su `linux-x64` e `linux-arm64`; NBitcoin è C# puro gestito, nessuna dipendenza
|
||||
nativa legata all'arch).
|
||||
- **Build cross dei binari da Linux, senza Wine** (.NET cross-targeta nativamente):
|
||||
- Windows: `dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained` → `.exe`.
|
||||
- Linux: `dotnet publish -r linux-x64 --self-contained`.
|
||||
- **Docker** consigliato: un solo `Dockerfile` su `mcr.microsoft.com/dotnet/sdk:8.0`
|
||||
produce i target in modo riproducibile.
|
||||
- Wine **non** serve per compilare. Servirebbe solo per costruire un *installer* Windows
|
||||
con Inno Setup su Linux (evitabile distribuendo l'`.exe` single-file **portable**) o per
|
||||
*testare* l'`.exe` su Linux (test, non build). Firma Authenticode fattibile da Linux con
|
||||
`osslsigncode`.
|
||||
|
||||
### 19.6 Packaging "un sorgente → .exe + AppImage" (multi-architettura)
|
||||
Da una macchina **Ubuntu x86_64** si producono tutti e quattro i target:
|
||||
|
||||
| Target | RID | Output | Da x86_64 |
|
||||
|---|---|---|---|
|
||||
| Windows x64 | `win-x64` | `.exe` | ✅ diretto (no Wine) |
|
||||
| Windows arm64 | `win-arm64` | `.exe` | ✅ diretto (no Wine) |
|
||||
| Linux x64 | `linux-x64` | AppImage | ✅ nativo |
|
||||
| Linux arm64 | `linux-arm64` | AppImage | ✅ via `docker buildx` + QEMU |
|
||||
|
||||
- **Binari**: `dotnet publish -r <rid> -p:PublishSingleFile=true --self-contained` per
|
||||
ciascun RID. Il `.exe` Windows (x64 e arm64) esce direttamente; niente Wine.
|
||||
- **AppImage Linux x64**: `dotnet publish -r linux-x64` + **PupNet Deploy** su Ubuntu.
|
||||
- **AppImage Linux arm64**: il `dotnet publish -r linux-arm64` cross-compila i binari da
|
||||
x86_64, ma l'assemblaggio dell'AppImage (appimagetool/runtime) è arch-specifico → si fa
|
||||
con **`docker buildx` multi-arch + emulazione QEMU** (`--platform linux/arm64`) nella
|
||||
stessa pipeline, così l'AppImage arm64 viene impacchettato in ambiente arm64 emulato.
|
||||
- Associazione dello schema URI `palladium:`; build riproducibili in Docker; firma codice.
|
||||
|
||||
### 19.7 Flusso di test (non serve compilare e lanciare l'app per testare)
|
||||
Tre livelli, tutti su Ubuntu, il grosso **headless**:
|
||||
1. **`dotnet test`** — logica del `Core` senza GUI né rete reale: golden vector
|
||||
seed→indirizzi del profilo (confronto 1:1 col wallet di riferimento), costruzione/firma
|
||||
tx, PSBT, coin selection, parsing protocollo e verifica Merkle con server mockato.
|
||||
2. **CLI** (`dotnet run --project src/Cli -- ...`) contro **regtest/testnet**: sync, saldo
|
||||
su xpub watch-only, costruzione tx — flussi reali senza aprire la UI.
|
||||
3. **GUI** solo per rifinire l'interfaccia: `dotnet run` compila+lancia in un comando, con
|
||||
**Hot Reload** Avalonia e previewer XAML (niente ciclo build-lancia manuale).
|
||||
|
||||
**Dev-loop (equivalente di `npm run dev`) — nativo anche su Debian arm64:**
|
||||
- `dotnet watch --project src/App` ≈ `npm run dev`: ricompila e applica **Hot Reload** a
|
||||
ogni salvataggio, con la finestra Avalonia aggiornata dal vivo; `dotnet run` per il lancio
|
||||
singolo; previewer XAML nell'IDE per le viste.
|
||||
- Su **arm64 si sviluppa e si vede la grafica nativamente** (.NET SDK `linux-arm64`,
|
||||
Avalonia rende con Skia, fallback software se manca l'accelerazione GPU). **QEMU non serve
|
||||
per sviluppare/testare** — l'emulazione riguarda solo l'impacchettamento di AppImage di
|
||||
un'altra architettura (§19.6).
|
||||
- Prerequisiti runtime su Debian/Ubuntu: ambiente grafico (X11/Wayland) e librerie native di
|
||||
Avalonia, es. `apt install libx11-6 libice6 libsm6 libfontconfig1 libglib2.0-0 libgl1`
|
||||
(più mesa). Per logica/crypto non serve GUI: `dotnet test` e la CLI girano headless.
|
||||
|
||||
---
|
||||
|
||||
*Questo blueprint è una specifica completa e indipendente dal linguaggio. I parametri del
|
||||
profilo di rete (§3) e la logica di validazione header/checkpoint (§7) sono gli elementi
|
||||
critici e specifici della catena; tutto il resto è l'ingegneria standard di un wallet SPV
|
||||
desktop. Implementando i capitoli nell'ordine del §16 si ottiene un wallet desktop completo
|
||||
in stile Sparrow.*
|
||||
+103
-184
@@ -1,247 +1,166 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace PalladiumWallet.App.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Localizzazione UI: dizionario chiave → traduzioni per lingua, con
|
||||
/// Localizzazione UI (blueprint §14): dizionario chiave → [it, en], con
|
||||
/// indicizzatore bindabile da XAML ({Binding Loc[chiave]}). Al cambio lingua
|
||||
/// il ViewModel sostituisce l'istanza così Avalonia rivaluta tutte le binding.
|
||||
/// notifica "Item[]" e tutte le binding si aggiornano.
|
||||
/// </summary>
|
||||
public sealed class Loc
|
||||
public sealed class Loc : INotifyPropertyChanged
|
||||
{
|
||||
public static Loc Instance { get; private set; } = new();
|
||||
public static Loc Instance { get; } = new();
|
||||
|
||||
public static readonly string[] Languages = ["it", "en", "es", "fr", "pt", "de"];
|
||||
public static readonly string[] LanguageNames = ["Italiano", "English", "Español", "Français", "Português", "Deutsch"];
|
||||
public static readonly string[] Languages = ["it", "en"];
|
||||
public static readonly string[] LanguageNames = ["Italiano", "English"];
|
||||
|
||||
public string Language { get; private set; } = "en";
|
||||
public string Language { get; private set; } = "it";
|
||||
|
||||
private Loc() { }
|
||||
private Loc(string language) { Language = language; }
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Crea una nuova istanza con la lingua specificata e aggiorna il singleton
|
||||
/// usato da <see cref="Tr"/>. Il ViewModel assegna questa istanza alla
|
||||
/// propria property Loc così Avalonia vede un riferimento diverso e
|
||||
/// rivaluta tutte le binding {Binding Loc[chiave]}.
|
||||
/// </summary>
|
||||
internal static Loc SwitchTo(string language)
|
||||
public void SetLanguage(string language)
|
||||
{
|
||||
if (System.Array.IndexOf(Languages, language) < 0) language = "it";
|
||||
var loc = new Loc(language);
|
||||
Instance = loc;
|
||||
return loc;
|
||||
if (Language == language || System.Array.IndexOf(Languages, language) < 0)
|
||||
return;
|
||||
Language = language;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Language)));
|
||||
}
|
||||
|
||||
public string this[string key] =>
|
||||
Strings.TryGetValue(key, out var values)
|
||||
? values[System.Math.Max(0, System.Array.IndexOf(Languages, Language))]
|
||||
? values[Language == "en" ? 1 : 0]
|
||||
: key;
|
||||
|
||||
public static string Tr(string key) => Instance[key];
|
||||
|
||||
private static readonly Dictionary<string, string[]> Strings = new()
|
||||
{
|
||||
// Menu it en es fr pt de
|
||||
["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.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.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.resetcerts"] = ["Reset certificati SSL", "Reset SSL certificates", "Restablecer certificados SSL", "Réinitialiser les certificats SSL", "Redefinir certificados SSL", "SSL-Zertifikate zurücksetzen"],
|
||||
["menu.settings"] = ["_Impostazioni", "_Settings", "_Configuración", "_Paramètres", "_Configurações", "_Einstellungen"],
|
||||
["settings.unit.short"] = ["Unità", "Unit", "Unidad", "Unité", "Unidade", "Einheit"],
|
||||
// Menu
|
||||
["menu.file"] = ["_File", "_File"],
|
||||
["menu.file.new"] = ["Nuovo / ripristina wallet…", "New / restore wallet…"],
|
||||
["menu.file.open"] = ["Apri wallet da file…", "Open wallet from file…"],
|
||||
["menu.file.close"] = ["Chiudi wallet", "Close wallet"],
|
||||
["menu.net"] = ["_Rete", "_Network"],
|
||||
["menu.net.discover"] = ["Cerca altri server (peer)", "Discover servers (peers)"],
|
||||
["menu.net.resetcerts"] = ["Reset certificati SSL", "Reset SSL certificates"],
|
||||
["menu.settings"] = ["_Impostazioni", "_Settings"],
|
||||
["settings.unit.short"] = ["Unità", "Unit"],
|
||||
|
||||
// Wizard
|
||||
["wiz.net"] = ["Rete:", "Network:", "Red:", "Réseau :", "Rede:", "Netzwerk:"],
|
||||
["wiz.open.btn"] = ["Apri il wallet esistente", "Open existing wallet", "Abrir wallet existente", "Ouvrir le wallet existant", "Abrir carteira existente", "Vorhandenes Wallet öffnen"],
|
||||
["wiz.new.btn"] = ["Crea un nuovo wallet", "Create a new wallet", "Crear nuevo wallet", "Créer un nouveau wallet", "Criar nova carteira", "Neues Wallet erstellen"],
|
||||
["wiz.restore.btn"] = ["Ripristina da seed", "Restore from seed", "Restaurar desde semilla", "Restaurer depuis la graine", "Restaurar da semente", "Aus Seed wiederherstellen"],
|
||||
["wiz.open.title"] = ["Apri il wallet", "Open the wallet", "Abrir el wallet", "Ouvrir le wallet", "Abrir a carteira", "Wallet öffnen"],
|
||||
["wiz.open.placeholder"] = ["Password del file (vuoto se non impostata)", "File password (empty if not set)", "Contraseña del archivo (vacío si no establecida)", "Mot de passe du fichier (vide si non défini)", "Senha do arquivo (vazio se não definida)", "Dateipasswort (leer lassen, wenn nicht gesetzt)"],
|
||||
["wiz.open.ok"] = ["Apri", "Open", "Abrir", "Ouvrir", "Abrir", "Öffnen"],
|
||||
["wiz.seed.title"] = ["Il tuo seed (12 parole)", "Your seed (12 words)", "Tu semilla (12 palabras)", "Votre graine (12 mots)", "Sua semente (12 palavras)", "Ihr Seed (12 Wörter)"],
|
||||
["wiz.net"] = ["Rete:", "Network:"],
|
||||
["wiz.open.btn"] = ["Apri il wallet esistente", "Open existing wallet"],
|
||||
["wiz.new.btn"] = ["Crea un nuovo wallet", "Create a new wallet"],
|
||||
["wiz.restore.btn"] = ["Ripristina da seed", "Restore from seed"],
|
||||
["wiz.open.title"] = ["Apri il wallet", "Open the wallet"],
|
||||
["wiz.open.placeholder"] = ["Password del file (vuoto se non impostata)", "File password (empty if not set)"],
|
||||
["wiz.open.ok"] = ["Apri", "Open"],
|
||||
["wiz.seed.title"] = ["Il tuo seed (12 parole)", "Your seed (12 words)"],
|
||||
["wiz.seed.warning"] = [
|
||||
"Scrivi le parole su carta, nell'ordine. Chi le possiede controlla i fondi; se le perdi, i fondi sono irrecuperabili.",
|
||||
"Write the words on paper, in order. Whoever holds them controls the funds; if you lose them, funds are unrecoverable.",
|
||||
"Escribe las palabras en papel, en orden. Quien las posea controla los fondos; si las pierdes, los fondos son irrecuperables.",
|
||||
"Écrivez les mots sur papier, dans l'ordre. Celui qui les possède contrôle les fonds ; si vous les perdez, les fonds sont irrécupérables.",
|
||||
"Escreva as palavras no papel, em ordem. Quem as possuir controla os fundos; se as perder, os fundos são irrecuperáveis.",
|
||||
"Schreiben Sie die Wörter auf Papier, in der richtigen Reihenfolge. Wer sie besitzt, kontrolliert die Gelder; wenn Sie sie verlieren, sind die Gelder unwiederbringlich verloren."],
|
||||
["wiz.seed.next"] = ["Le ho scritte — Avanti", "I wrote them down — Next", "Las anoté — Siguiente", "Je les ai notés — Suivant", "Eu as anotei — Próximo", "Ich habe sie notiert — Weiter"],
|
||||
["wiz.confirm.title"] = ["Conferma il seed", "Confirm the seed", "Confirmar la semilla", "Confirmer la graine", "Confirmar a semente", "Seed bestätigen"],
|
||||
["wiz.confirm.placeholder"] = ["Reinserisci le 12 parole separate da spazi", "Re-enter the 12 words separated by spaces", "Reingresa las 12 palabras separadas por espacios", "Ressaisissez les 12 mots séparés par des espaces", "Reinsira as 12 palavras separadas por espaços", "12 Wörter durch Leerzeichen getrennt erneut eingeben"],
|
||||
["wiz.words.title"] = ["Ripristina da seed", "Restore from seed", "Restaurar desde semilla", "Restaurer depuis la graine", "Restaurar da semente", "Aus Seed wiederherstellen"],
|
||||
["wiz.words.placeholder"] = ["Mnemonica BIP39 (12 o 24 parole separate da spazi)", "BIP39 mnemonic (12 or 24 words separated by spaces)", "Mnemónico BIP39 (12 o 24 palabras separadas por espacios)", "Mnémonique BIP39 (12 ou 24 mots séparés par des espaces)", "Mnemônico BIP39 (12 ou 24 palavras separadas por espaços)", "BIP39-Mnemonic (12 oder 24 durch Leerzeichen getrennte Wörter)"],
|
||||
["wiz.passphrase.title"] = ["Passphrase opzionale", "Optional passphrase", "Frase de contraseña opcional", "Phrase de passe optionnelle", "Frase-senha opcional", "Optionale Passphrase"],
|
||||
["wiz.passphrase.placeholder"] = ["Lascia vuoto per non usarla", "Leave empty to skip", "Deja vacío para omitir", "Laisser vide pour ignorer", "Deixe vazio para ignorar", "Leer lassen zum Überspringen"],
|
||||
["wiz.password.title"] = ["Password del file wallet", "Wallet file password", "Contraseña del archivo wallet", "Mot de passe du fichier wallet", "Senha do arquivo da carteira", "Wallet-Dateipasswort"],
|
||||
["wiz.password.placeholder"] = ["Consigliata (vuoto = file in chiaro su disco)", "Recommended (empty = plaintext file on disk)", "Recomendada (vacío = archivo en texto claro en disco)", "Recommandé (vide = fichier en texte clair sur disque)", "Recomendada (vazio = arquivo em texto simples no disco)", "Empfohlen (leer = Klartextdatei auf Disk)"],
|
||||
["wiz.password.create"] = ["Crea il wallet", "Create wallet", "Crear wallet", "Créer le wallet", "Criar carteira", "Wallet erstellen"],
|
||||
["wiz.back"] = ["Indietro", "Back", "Atrás", "Retour", "Voltar", "Zurück"],
|
||||
["wiz.next"] = ["Avanti", "Next", "Siguiente", "Suivant", "Próximo", "Weiter"],
|
||||
"Write the words on paper, in order. Whoever holds them controls the funds; if you lose them, funds are unrecoverable."],
|
||||
["wiz.seed.next"] = ["Le ho scritte — Avanti", "I wrote them down — Next"],
|
||||
["wiz.confirm.title"] = ["Conferma il seed", "Confirm the seed"],
|
||||
["wiz.confirm.placeholder"] = ["Reinserisci le 12 parole separate da spazi", "Re-enter the 12 words separated by spaces"],
|
||||
["wiz.words.title"] = ["Ripristina da seed", "Restore from seed"],
|
||||
["wiz.words.placeholder"] = ["Mnemonica BIP39 (12 o 24 parole separate da spazi)", "BIP39 mnemonic (12 or 24 words separated by spaces)"],
|
||||
["wiz.passphrase.title"] = ["Passphrase opzionale", "Optional passphrase"],
|
||||
["wiz.passphrase.placeholder"] = ["Lascia vuoto per non usarla", "Leave empty to skip"],
|
||||
["wiz.password.title"] = ["Password del file wallet", "Wallet file password"],
|
||||
["wiz.password.placeholder"] = ["Consigliata (vuoto = file in chiaro su disco)", "Recommended (empty = plaintext file on disk)"],
|
||||
["wiz.password.create"] = ["Crea il wallet", "Create wallet"],
|
||||
["wiz.back"] = ["Indietro", "Back"],
|
||||
["wiz.next"] = ["Avanti", "Next"],
|
||||
|
||||
// Pannello wallet
|
||||
["wallet.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
|
||||
["wallet.server"] = ["Server:", "Server:", "Servidor:", "Serveur :", "Servidor:", "Server:"],
|
||||
["wallet.connect"] = ["Connetti e sincronizza", "Connect and sync", "Conectar y sincronizar", "Connecter et synchroniser", "Conectar e sincronizar", "Verbinden und synchronisieren"],
|
||||
["wallet.manual"] = ["oppure host:porta manuale", "or manual host:port", "o host:puerto manual", "ou hôte:port manuel", "ou host:porta manual", "oder manuell host:port"],
|
||||
["wallet.discover"] = ["Cerca altri server", "Discover servers", "Buscar otros servidores", "Rechercher des serveurs", "Procurar servidores", "Server suchen"],
|
||||
["wallet.resetcert"] = ["Reset cert.", "Reset certs", "Restablecer cert.", "Réinit. cert.", "Redefinir cert.", "Zert. zurücksetzen"],
|
||||
["tab.receive"] = ["Ricevi", "Receive", "Recibir", "Recevoir", "Receber", "Empfangen"],
|
||||
["tab.history"] = ["Storico", "History", "Historial", "Historique", "Histórico", "Verlauf"],
|
||||
["tab.addresses"] = ["Indirizzi", "Addresses", "Direcciones", "Adresses", "Endereços", "Adressen"],
|
||||
["tab.send"] = ["Invia", "Send", "Enviar", "Envoyer", "Enviar", "Senden"],
|
||||
["tab.contacts"] = ["Contatti", "Contacts", "Contactos", "Contacts", "Contatos", "Kontakte"],
|
||||
["receive.next"] = ["Prossimo indirizzo non usato:", "Next unused address:", "Próxima dirección no usada:", "Prochaine adresse non utilisée :", "Próximo endereço não usado:", "Nächste ungenutzte Adresse:"],
|
||||
["wallet.close"] = ["Chiudi wallet", "Close wallet"],
|
||||
["wallet.server"] = ["Server:", "Server:"],
|
||||
["wallet.connect"] = ["Connetti e sincronizza", "Connect and sync"],
|
||||
["wallet.manual"] = ["oppure host:porta manuale", "or manual host:port"],
|
||||
["wallet.discover"] = ["Cerca altri server", "Discover servers"],
|
||||
["wallet.resetcert"] = ["Reset cert.", "Reset certs"],
|
||||
["tab.receive"] = ["Ricevi", "Receive"],
|
||||
["tab.history"] = ["Storico", "History"],
|
||||
["tab.addresses"] = ["Indirizzi", "Addresses"],
|
||||
["tab.send"] = ["Invia", "Send"],
|
||||
["receive.next"] = ["Prossimo indirizzo non usato:", "Next unused address:"],
|
||||
["receive.hint"] = [
|
||||
"Ogni pagamento ricevuto qui comparirà nello storico alla prossima sincronizzazione.",
|
||||
"Payments received here will appear in the history at the next synchronization.",
|
||||
"Los pagos recibidos aquí aparecerán en el historial en la próxima sincronización.",
|
||||
"Les paiements reçus ici apparaîtront dans l'historique à la prochaine synchronisation.",
|
||||
"Os pagamentos recebidos aqui aparecerão no histórico na próxima sincronização.",
|
||||
"Hier empfangene Zahlungen erscheinen beim nächsten Synchronisieren im Verlauf."],
|
||||
["addr.type"] = ["Tipo", "Type", "Tipo", "Type", "Tipo", "Typ"],
|
||||
["addr.index"] = ["Indice", "Index", "Índice", "Index", "Índice", "Index"],
|
||||
["addr.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"],
|
||||
["addr.balance"] = ["Saldo", "Balance", "Saldo", "Solde", "Saldo", "Saldo"],
|
||||
["addr.copied"] = ["Indirizzo copiato negli appunti", "Address copied to clipboard", "Dirección copiada al portapapeles", "Adresse copiée dans le presse-papiers", "Endereço copiado para a área de transferência", "Adresse in die Zwischenablage kopiert"],
|
||||
["addr.derivpath"] = ["Percorso di derivazione:", "Derivation path:", "Ruta de derivación:", "Chemin de dérivation :", "Caminho de derivação:", "Ableitungspfad:"],
|
||||
["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.show.privkey"] = ["Mostra", "Show", "Mostrar", "Afficher", "Mostrar", "Anzeigen"],
|
||||
["addr.hide.privkey"] = ["Nascondi", "Hide", "Ocultar", "Masquer", "Ocultar", "Ausblenden"],
|
||||
["addr.receive"] = ["ricezione", "receive", "recepción", "réception", "recebimento", "Empfang"],
|
||||
["addr.change"] = ["change", "change", "cambio", "monnaie", "troco", "Wechselgeld"],
|
||||
["addr.info.title"] = ["Informazioni indirizzo", "Address information", "Información de dirección", "Informations sur l'adresse", "Informações do endereço", "Adressinformationen"],
|
||||
["addr.close"] = ["Chiudi", "Close", "Cerrar", "Fermer", "Fechar", "Schließen"],
|
||||
["send.from.contact"] = ["Da contatti:", "From contacts:", "De contactos:", "Depuis les contacts :", "De contatos:", "Aus Kontakten:"],
|
||||
["send.contact.hint"] = ["seleziona per riempire l'indirizzo", "select to fill address", "selecciona para rellenar la dirección", "sélectionner pour remplir l'adresse", "selecione para preencher o endereço", "auswählen um Adresse einzufügen"],
|
||||
["send.to"] = ["Indirizzo destinatario", "Recipient address", "Dirección destinataria", "Adresse du destinataire", "Endereço do destinatário", "Empfängeradresse"],
|
||||
["send.amount"] = ["Importo", "Amount", "Importe", "Montant", "Valor", "Betrag"],
|
||||
["send.all"] = ["Invia tutto", "Send all", "Enviar todo", "Tout envoyer", "Enviar tudo", "Alles senden"],
|
||||
["send.feerate"] = ["fee sat/vB:", "fee sat/vB:", "tarifa sat/vB:", "frais sat/vB :", "taxa sat/vB:", "Gebühr sat/vB:"],
|
||||
["send.prepare"] = ["Prepara transazione", "Prepare transaction", "Preparar transacción", "Préparer la transaction", "Preparar transação", "Transaktion vorbereiten"],
|
||||
["send.confirm"] = ["CONFERMA E TRASMETTI", "CONFIRM AND BROADCAST", "CONFIRMAR Y TRANSMITIR", "CONFIRMER ET DIFFUSER", "CONFIRMAR E TRANSMITIR", "BESTÄTIGEN UND SENDEN"],
|
||||
"Payments received here will appear in the history at the next synchronization."],
|
||||
["addr.type"] = ["Tipo", "Type"],
|
||||
["addr.index"] = ["Indice", "Index"],
|
||||
["addr.address"] = ["Indirizzo", "Address"],
|
||||
["addr.balance"] = ["Saldo", "Balance"],
|
||||
["addr.receive"] = ["ricezione", "receive"],
|
||||
["addr.change"] = ["change", "change"],
|
||||
["send.to"] = ["Indirizzo destinatario", "Recipient address"],
|
||||
["send.amount"] = ["Importo", "Amount"],
|
||||
["send.all"] = ["Invia tutto", "Send all"],
|
||||
["send.feerate"] = ["fee sat/vB:", "fee sat/vB:"],
|
||||
["send.prepare"] = ["Prepara transazione", "Prepare transaction"],
|
||||
["send.confirm"] = ["CONFERMA E TRASMETTI", "CONFIRM AND BROADCAST"],
|
||||
|
||||
// Stato connessione
|
||||
["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.reconnecting"] = ["riconnessione…", "reconnecting…", "reconectando…", "reconnexion…", "reconectando…", "Verbindung wird wiederhergestellt…"],
|
||||
["conn.error"] = ["errore di connessione", "connection error", "error de conexión", "erreur de connexion", "erro de conexão", "Verbindungsfehler"],
|
||||
["conn.certchanged"] = ["certificato cambiato", "certificate changed", "certificado cambiado", "certificat modifié", "certificado alterado", "Zertifikat geändert"],
|
||||
["conn.connectedto"] = ["connesso a", "connected to", "conectado a", "connecté à", "conectado a", "verbunden mit"],
|
||||
["conn.connectingto"] = ["connessione a", "connecting to", "conectando a", "connexion à", "conectando a", "Verbindung zu"],
|
||||
["conn.none"] = ["non connesso", "not connected"],
|
||||
["conn.disconnected"] = ["disconnesso", "disconnected"],
|
||||
["conn.reconnecting"] = ["riconnessione…", "reconnecting…"],
|
||||
["conn.error"] = ["errore di connessione", "connection error"],
|
||||
["conn.certchanged"] = ["certificato cambiato", "certificate changed"],
|
||||
["conn.connectedto"] = ["connesso a", "connected to"],
|
||||
["conn.connectingto"] = ["connessione a", "connecting to"],
|
||||
|
||||
// Messaggi di stato principali
|
||||
["msg.welcome.existing"] = [
|
||||
"Trovato un wallet esistente su questa rete: aprilo, oppure creane un altro.",
|
||||
"Found an existing wallet on this network: open it, or create another one.",
|
||||
"Se encontró un wallet existente en esta red: ábrelo o crea otro.",
|
||||
"Un wallet existant a été trouvé sur ce réseau : ouvrez-le ou créez-en un autre.",
|
||||
"Uma carteira existente foi encontrada nesta rede: abra-a ou crie outra.",
|
||||
"Ein vorhandenes Wallet wurde in diesem Netzwerk gefunden: öffnen Sie es oder erstellen Sie ein neues."],
|
||||
"Found an existing wallet on this network: open it, or create another one."],
|
||||
["msg.welcome.new"] = [
|
||||
"Benvenuto: crea un nuovo wallet o ripristina da seed.",
|
||||
"Welcome: create a new wallet or restore from seed.",
|
||||
"Bienvenido: crea un nuevo wallet o restaura desde semilla.",
|
||||
"Bienvenue : créez un nouveau wallet ou restaurez depuis une graine.",
|
||||
"Bem-vindo: crie uma nova carteira ou restaure da semente.",
|
||||
"Willkommen: erstellen Sie ein neues Wallet oder stellen Sie es aus einem Seed wieder her."],
|
||||
"Welcome: create a new wallet or restore from seed."],
|
||||
["msg.open.password"] = [
|
||||
"Inserisci la password del file (lascia vuoto se non impostata).",
|
||||
"Enter the file password (leave empty if not set).",
|
||||
"Ingresa la contraseña del archivo (deja vacío si no establecida).",
|
||||
"Entrez le mot de passe du fichier (laisser vide si non défini).",
|
||||
"Digite a senha do arquivo (deixe vazio se não definida).",
|
||||
"Geben Sie das Dateipasswort ein (leer lassen, wenn nicht gesetzt)."],
|
||||
"Enter the file password (leave empty if not set)."],
|
||||
["msg.seed.write"] = [
|
||||
"Scrivi le 12 parole SU CARTA, nell'ordine. Sono l'unico backup del wallet.",
|
||||
"Write the 12 words ON PAPER, in order. They are the only backup of the wallet.",
|
||||
"Escribe las 12 palabras EN PAPEL, en orden. Son la única copia de seguridad del wallet.",
|
||||
"Écrivez les 12 mots SUR PAPIER, dans l'ordre. C'est la seule sauvegarde du wallet.",
|
||||
"Escreva as 12 palavras NO PAPEL, em ordem. São o único backup da carteira.",
|
||||
"Schreiben Sie die 12 Wörter AUF PAPIER, in der richtigen Reihenfolge. Sie sind die einzige Sicherung des Wallets."],
|
||||
"Write the 12 words ON PAPER, in order. They are the only backup of the wallet."],
|
||||
["msg.seed.retype"] = [
|
||||
"Reinserisci le 12 parole per confermare di averle scritte.",
|
||||
"Re-enter the 12 words to confirm you wrote them down.",
|
||||
"Reingresa las 12 palabras para confirmar que las has anotado.",
|
||||
"Ressaisissez les 12 mots pour confirmer que vous les avez notés.",
|
||||
"Reinsira as 12 palavras para confirmar que as anotou.",
|
||||
"Geben Sie die 12 Wörter erneut ein, um zu bestätigen, dass Sie sie notiert haben."],
|
||||
"Re-enter the 12 words to confirm you wrote them down."],
|
||||
["msg.seed.mismatch"] = [
|
||||
"Le parole non corrispondono: ricontrolla quello che hai scritto su carta.",
|
||||
"The words do not match: check what you wrote on paper.",
|
||||
"Las palabras no coinciden: revisa lo que escribiste en papel.",
|
||||
"Les mots ne correspondent pas : vérifiez ce que vous avez écrit sur papier.",
|
||||
"As palavras não correspondem: verifique o que escreveu no papel.",
|
||||
"Die Wörter stimmen nicht überein: überprüfen Sie, was Sie auf Papier geschrieben haben."],
|
||||
"The words do not match: check what you wrote on paper."],
|
||||
["msg.words.enter"] = [
|
||||
"Inserisci la mnemonica BIP39 (12 o 24 parole separate da spazi).",
|
||||
"Enter the BIP39 mnemonic (12 or 24 words separated by spaces).",
|
||||
"Ingresa el mnemónico BIP39 (12 o 24 palabras separadas por espacios).",
|
||||
"Entrez le mnémonique BIP39 (12 ou 24 mots séparés par des espaces).",
|
||||
"Insira o mnemônico BIP39 (12 ou 24 palavras separadas por espaços).",
|
||||
"Geben Sie die BIP39-Mnemonic ein (12 oder 24 durch Leerzeichen getrennte Wörter)."],
|
||||
"Enter the BIP39 mnemonic (12 or 24 words separated by spaces)."],
|
||||
["msg.words.invalid"] = [
|
||||
"Mnemonica non valida (parole o checksum errati): ricontrolla.",
|
||||
"Invalid mnemonic (wrong words or checksum): check again.",
|
||||
"Mnemónico no válido (palabras o checksum incorrectos): verifica de nuevo.",
|
||||
"Mnémonique invalide (mots ou checksum incorrects) : vérifiez à nouveau.",
|
||||
"Mnemônico inválido (palavras ou checksum incorretos): verifique novamente.",
|
||||
"Ungültige Mnemonic (falsche Wörter oder Prüfsumme): bitte erneut prüfen."],
|
||||
"Invalid mnemonic (wrong words or checksum): check again."],
|
||||
["msg.passphrase.info"] = [
|
||||
"Passphrase BIP39 opzionale: cambia completamente il wallet. Se la usi, annotala A PARTE dal seed; se la perdi i fondi sono irrecuperabili. Lascia vuoto per non usarla.",
|
||||
"Optional BIP39 passphrase: it derives a completely different wallet. If you use it, note it SEPARATELY from the seed; if lost, funds are unrecoverable. Leave empty to skip.",
|
||||
"Frase de contraseña BIP39 opcional: deriva un wallet completamente diferente. Si la usas, anótala SEPARADA de la semilla; si la pierdes, los fondos son irrecuperables. Deja vacío para omitir.",
|
||||
"Phrase de passe BIP39 optionnelle : elle dérive un wallet complètement différent. Si vous l'utilisez, notez-la SÉPARÉMENT de la graine ; si vous la perdez, les fonds sont irrécupérables. Laisser vide pour ignorer.",
|
||||
"Frase-senha BIP39 opcional: deriva uma carteira completamente diferente. Se a usar, anote-a SEPARADAMENTE da semente; se a perder, os fundos são irrecuperáveis. Deixe vazio para ignorar.",
|
||||
"Optionale BIP39-Passphrase: leitet ein völlig anderes Wallet ab. Falls verwendet, GETRENNT vom Seed notieren; falls verloren, sind die Gelder unwiederbringlich. Leer lassen zum Überspringen."],
|
||||
"Optional BIP39 passphrase: it derives a completely different wallet. If you use it, note it SEPARATELY from the seed; if lost, funds are unrecoverable. Leave empty to skip."],
|
||||
["msg.password.info"] = [
|
||||
"Password di cifratura del file wallet su disco (consigliata). Non sostituisce il seed: serve solo a proteggere il file.",
|
||||
"Encryption password for the wallet file on disk (recommended). It does not replace the seed: it only protects the file.",
|
||||
"Contraseña de cifrado para el archivo wallet en disco (recomendada). No reemplaza la semilla: solo protege el archivo.",
|
||||
"Mot de passe de chiffrement pour le fichier wallet sur disque (recommandé). Ne remplace pas la graine : protège uniquement le fichier.",
|
||||
"Senha de criptografia para o arquivo da carteira no disco (recomendada). Não substitui a semente: apenas protege o arquivo.",
|
||||
"Verschlüsselungspasswort für die Wallet-Datei auf der Festplatte (empfohlen). Ersetzt nicht den Seed: schützt nur die Datei."],
|
||||
["msg.wrongpassword"] = ["Password errata.", "Wrong password.", "Contraseña incorrecta.", "Mot de passe incorrect.", "Senha incorreta.", "Falsches Passwort."],
|
||||
["msg.opened"] = ["Wallet aperto: connessione al server…", "Wallet opened: connecting to server…", "Wallet abierto: conectando al servidor…", "Wallet ouvert : connexion au serveur…", "Carteira aberta: conectando ao servidor…", "Wallet geöffnet: Verbindung zum Server…"],
|
||||
["msg.synced"] = ["Sincronizzato", "Synchronized", "Sincronizado", "Synchronisé", "Sincronizado", "Synchronisiert"],
|
||||
"Encryption password for the wallet file on disk (recommended). It does not replace the seed: it only protects the file."],
|
||||
["msg.wrongpassword"] = ["Password errata.", "Wrong password."],
|
||||
["msg.opened"] = ["Wallet aperto: connessione al server…", "Wallet opened: connecting to server…"],
|
||||
["msg.synced"] = ["Sincronizzato", "Synchronized"],
|
||||
["msg.synced.detail"] = [
|
||||
"transazioni verificate SPV. Aggiornamento in tempo reale attivo.",
|
||||
"SPV-verified transactions. Real-time updates active.",
|
||||
"transacciones verificadas SPV. Actualizaciones en tiempo real activas.",
|
||||
"transactions vérifiées SPV. Mises à jour en temps réel actives.",
|
||||
"transações verificadas SPV. Atualizações em tempo real ativas.",
|
||||
"SPV-verifizierte Transaktionen. Echtzeit-Updates aktiv."],
|
||||
["msg.height"] = ["altezza", "height", "altura", "hauteur", "altura", "Höhe"],
|
||||
["msg.pending"] = ["in attesa di conferma", "pending confirmation", "pendiente de confirmación", "en attente de confirmation", "aguardando confirmação", "ausstehende Bestätigung"],
|
||||
["msg.notspendable"] = ["non ancora spendibile", "not yet spendable", "aún no gastable", "pas encore dépensable", "ainda não gastável", "noch nicht verwendbar"],
|
||||
["msg.settings.saved"] = ["Impostazioni salvate.", "Settings saved.", "Configuración guardada.", "Paramètres enregistrés.", "Configurações salvas.", "Einstellungen gespeichert."],
|
||||
"SPV-verified transactions. Real-time updates active."],
|
||||
["msg.height"] = ["altezza", "height"],
|
||||
["msg.pending"] = ["in attesa di conferma", "pending confirmation"],
|
||||
["msg.notspendable"] = ["non ancora spendibile", "not yet spendable"],
|
||||
["msg.settings.saved"] = ["Impostazioni salvate.", "Settings saved."],
|
||||
["msg.certreset"] = [
|
||||
"Certificati SSL azzerati: riprova la connessione.",
|
||||
"SSL certificates cleared: retry the connection.",
|
||||
"Certificados SSL restablecidos: reintenta la conexión.",
|
||||
"Certificats SSL réinitialisés : réessayez la connexion.",
|
||||
"Certificados SSL redefinidos: tente novamente a conexão.",
|
||||
"SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."],
|
||||
["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"],
|
||||
|
||||
// Contatti
|
||||
["contacts.name"] = ["Nome", "Name", "Nombre", "Nom", "Nome", "Name"],
|
||||
["contacts.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"],
|
||||
["contacts.name.ph"] = ["Nome contatto", "Contact name", "Nombre del contacto", "Nom du contact", "Nome do contato", "Kontaktname"],
|
||||
["contacts.address.ph"] = ["Indirizzo blockchain", "Blockchain address", "Dirección blockchain", "Adresse blockchain", "Endereço blockchain", "Blockchain-Adresse"],
|
||||
["contacts.add"] = ["Aggiungi", "Add", "Agregar", "Ajouter", "Adicionar", "Hinzufügen"],
|
||||
["contacts.remove"] = ["Rimuovi selezionato", "Remove selected", "Eliminar seleccionado", "Supprimer la sélection", "Remover selecionado", "Auswahl entfernen"],
|
||||
["contacts.empty"] = ["Nessun contatto salvato.", "No saved contacts.", "No hay contactos guardados.", "Aucun contact enregistré.", "Nenhum contato salvo.", "Keine gespeicherten Kontakte."],
|
||||
"SSL certificates cleared: retry the connection."],
|
||||
["msg.error"] = ["Errore", "Error"],
|
||||
|
||||
// Finestra impostazioni
|
||||
["settings.title"] = ["Impostazioni", "Settings", "Configuración", "Paramètres", "Configurações", "Einstellungen"],
|
||||
["settings.language"] = ["Lingua", "Language", "Idioma", "Langue", "Idioma", "Sprache"],
|
||||
["settings.unit"] = ["Unità degli importi", "Amount unit", "Unidad de importes", "Unité des montants", "Unidade dos valores", "Betrageinheit"],
|
||||
["settings.ok"] = ["Salva", "Save", "Guardar", "Enregistrer", "Salvar", "Speichern"],
|
||||
["settings.cancel"] = ["Annulla", "Cancel", "Cancelar", "Annuler", "Cancelar", "Abbrechen"],
|
||||
["settings.title"] = ["Impostazioni", "Settings"],
|
||||
["settings.language"] = ["Lingua", "Language"],
|
||||
["settings.unit"] = ["Unità degli importi", "Amount unit"],
|
||||
["settings.ok"] = ["Salva", "Save"],
|
||||
["settings.cancel"] = ["Annulla", "Cancel"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,24 +21,8 @@ namespace PalladiumWallet.App.ViewModels;
|
||||
/// <summary>Riga dello storico transazioni per la vista.</summary>
|
||||
public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata);
|
||||
|
||||
/// <summary>Riga della vista indirizzi con chiavi e derivation path pre-calcolati.</summary>
|
||||
public sealed record AddressRow(
|
||||
string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx,
|
||||
bool IsChange = false, string PubKey = "", string PrivKey = "", string DerivPath = "")
|
||||
{
|
||||
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
|
||||
}
|
||||
|
||||
/// <summary>Dati completi di un indirizzo passati alla finestra di dettaglio.</summary>
|
||||
public sealed record AddressInfo(
|
||||
Localization.Loc Loc,
|
||||
string Address, string DerivPath, string PubKey, string PrivKey)
|
||||
{
|
||||
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
|
||||
}
|
||||
|
||||
/// <summary>Contatto in rubrica: nome + indirizzo blockchain.</summary>
|
||||
public sealed record ContactEntry(string Name, string Address);
|
||||
/// <summary>Riga della vista indirizzi (stile Electrum): saldo e uso per indirizzo.</summary>
|
||||
public sealed record AddressRow(string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
|
||||
@@ -62,10 +46,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
/// <summary>Configurazione globale (§8): lingua e unità.</summary>
|
||||
private AppConfig _config = AppConfig.Load();
|
||||
|
||||
/// <summary>Istanza Loc corrente: viene rimpiazzata ad ogni cambio lingua
|
||||
/// così Avalonia vede un riferimento diverso e rivaluta le binding {Binding Loc[chiave]}.</summary>
|
||||
private Loc _loc = Loc.Instance;
|
||||
public Loc Loc => _loc;
|
||||
/// <summary>Stringhe localizzate, bindabili da XAML come Loc[chiave].</summary>
|
||||
public Loc Loc => Loc.Instance;
|
||||
|
||||
/// <summary>Unità corrente per il campo importo del pannello Invia.</summary>
|
||||
public string UnitLabel => _config.Unit;
|
||||
@@ -75,10 +57,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
// Spunte del menu Impostazioni (ToggleType Radio).
|
||||
public bool IsLangIt => _config.Language == "it";
|
||||
public bool IsLangEn => _config.Language == "en";
|
||||
public bool IsLangEs => _config.Language == "es";
|
||||
public bool IsLangFr => _config.Language == "fr";
|
||||
public bool IsLangPt => _config.Language == "pt";
|
||||
public bool IsLangDe => _config.Language == "de";
|
||||
public bool IsUnitPlm => _config.Unit == "PLM";
|
||||
public bool IsUnitMilli => _config.Unit == "mPLM";
|
||||
public bool IsUnitMicro => _config.Unit == "µPLM";
|
||||
@@ -103,15 +81,10 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
_config = config;
|
||||
_config.Save();
|
||||
_loc = Loc.SwitchTo(config.Language);
|
||||
OnPropertyChanged(nameof(Loc));
|
||||
Loc.SetLanguage(config.Language);
|
||||
OnPropertyChanged(nameof(UnitLabel));
|
||||
OnPropertyChanged(nameof(IsLangIt));
|
||||
OnPropertyChanged(nameof(IsLangEn));
|
||||
OnPropertyChanged(nameof(IsLangEs));
|
||||
OnPropertyChanged(nameof(IsLangFr));
|
||||
OnPropertyChanged(nameof(IsLangPt));
|
||||
OnPropertyChanged(nameof(IsLangDe));
|
||||
OnPropertyChanged(nameof(IsUnitPlm));
|
||||
OnPropertyChanged(nameof(IsUnitMilli));
|
||||
OnPropertyChanged(nameof(IsUnitMicro));
|
||||
@@ -124,18 +97,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
private string Fmt(long sats, bool withLabel = true) =>
|
||||
CoinAmount.FormatIn(sats, _config.Unit, withLabel);
|
||||
|
||||
/// <summary>Chiave privata WIF per un indirizzo; stringa vuota se watch-only.</summary>
|
||||
private string KeyWif(bool isChange, int index)
|
||||
{
|
||||
if (_account is null or { IsWatchOnly: true }) return "";
|
||||
try
|
||||
{
|
||||
return _account.GetExtPrivateKey(isChange, index)
|
||||
.PrivateKey.GetWif(PalladiumNetworks.For(Net)).ToString();
|
||||
}
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
/// <summary>File in attesa di password (apertura da menu File → Apri).</summary>
|
||||
private string? _pendingOpenPath;
|
||||
|
||||
@@ -244,73 +205,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public ObservableCollection<AddressRow> Addresses { get; } = [];
|
||||
|
||||
// ---- tab indirizzi ----
|
||||
|
||||
[ObservableProperty]
|
||||
private AddressRow? selectedAddressRow;
|
||||
|
||||
// ---- rubrica contatti ----
|
||||
|
||||
public ObservableCollection<ContactEntry> Contacts { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private ContactEntry? selectedContactInList;
|
||||
|
||||
/// <summary>Contatto selezionato nella ComboBox del pannello Invia: riempie SendTo.</summary>
|
||||
[ObservableProperty]
|
||||
private ContactEntry? sendToContact;
|
||||
|
||||
partial void OnSendToContactChanged(ContactEntry? value)
|
||||
{
|
||||
if (value is not null)
|
||||
SendTo = value.Address;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string newContactName = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string newContactAddress = "";
|
||||
|
||||
[RelayCommand]
|
||||
private void AddContact()
|
||||
{
|
||||
var name = NewContactName.Trim();
|
||||
var addr = NewContactAddress.Trim();
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(addr)) return;
|
||||
Contacts.Add(new ContactEntry(name, addr));
|
||||
NewContactName = NewContactAddress = "";
|
||||
PersistContacts();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveSelectedContact()
|
||||
{
|
||||
if (SelectedContactInList is { } c)
|
||||
{
|
||||
Contacts.Remove(c);
|
||||
SelectedContactInList = null;
|
||||
PersistContacts();
|
||||
}
|
||||
}
|
||||
|
||||
private void PersistContacts()
|
||||
{
|
||||
if (_doc is null || _walletPath is null) return;
|
||||
_doc.Contacts = Contacts
|
||||
.Select(c => new PalladiumWallet.Core.Storage.StoredContact { Name = c.Name, Address = c.Address })
|
||||
.ToList();
|
||||
WalletStore.Save(_doc, _walletPath, _password);
|
||||
}
|
||||
|
||||
private void LoadContacts()
|
||||
{
|
||||
Contacts.Clear();
|
||||
if (_doc is null) return;
|
||||
foreach (var c in _doc.Contacts)
|
||||
Contacts.Add(new ContactEntry(c.Name, c.Address));
|
||||
}
|
||||
|
||||
// ---- pannello invia ----
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -333,7 +227,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
_loc = Loc.SwitchTo(_config.Language);
|
||||
RefreshSetupState();
|
||||
// Aggiornamenti continui (§9): ping periodico per tenere viva la
|
||||
// connessione e accorgersi subito della caduta; se cade, riconnette
|
||||
@@ -590,7 +483,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
|
||||
+ (doc.IsWatchOnly ? " · watch-only" : "");
|
||||
LoadContacts();
|
||||
ApplyCache(doc.Cache);
|
||||
IsWalletOpen = true;
|
||||
StatusMessage = Loc.Tr("msg.opened");
|
||||
@@ -612,12 +504,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
// Prima della sincronizzazione si mostrano i primi indirizzi derivati.
|
||||
Addresses.Clear();
|
||||
for (var i = 0; i < 10; i++)
|
||||
Addresses.Add(new AddressRow(_loc["addr.receive"], i,
|
||||
_account.GetReceiveAddress(i).ToString(), "—", "—",
|
||||
false,
|
||||
_account.GetPublicKey(false, i).ToHex(),
|
||||
KeyWif(false, i),
|
||||
$"m/{_doc!.AccountPath}/0/{i}"));
|
||||
Addresses.Add(new AddressRow("ricezione", i,
|
||||
_account.GetReceiveAddress(i).ToString(), "—", "—"));
|
||||
return;
|
||||
}
|
||||
BalanceText = Fmt(cache.ConfirmedSats);
|
||||
@@ -640,15 +528,11 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
Addresses.Clear();
|
||||
foreach (var a in cache.Addresses)
|
||||
Addresses.Add(new AddressRow(
|
||||
a.IsChange ? _loc["addr.change"] : _loc["addr.receive"],
|
||||
a.IsChange ? "change" : "ricezione",
|
||||
a.Index,
|
||||
a.Address,
|
||||
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(),
|
||||
KeyWif(a.IsChange, a.Index),
|
||||
$"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}"));
|
||||
a.TxCount > 0 ? a.TxCount.ToString() : "—"));
|
||||
}
|
||||
|
||||
// ---------- comandi wallet ----------
|
||||
@@ -861,10 +745,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
_pendingSend = null;
|
||||
HasPendingSend = false;
|
||||
History.Clear();
|
||||
Contacts.Clear();
|
||||
SelectedContactInList = null;
|
||||
SendToContact = null;
|
||||
SelectedAddressRow = null;
|
||||
IsWalletOpen = false;
|
||||
IsConnected = false;
|
||||
ConnectionStatus = Loc.Tr("conn.none");
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PalladiumWallet.App.ViewModels"
|
||||
x:Class="PalladiumWallet.App.Views.AddressInfoWindow"
|
||||
x:DataType="vm:AddressInfo"
|
||||
Title="{Binding Loc[addr.info.title]}"
|
||||
Width="560" SizeToContent="Height"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<StackPanel Margin="24" Spacing="16">
|
||||
|
||||
<!-- Indirizzo -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[addr.address]}" FontSize="11" Foreground="Gray"/>
|
||||
<SelectableTextBlock Text="{Binding Address}"
|
||||
FontFamily="monospace" FontSize="13"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Derivation path -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[addr.derivpath]}" FontSize="11" Foreground="Gray"/>
|
||||
<SelectableTextBlock Text="{Binding DerivPath}"
|
||||
FontFamily="monospace" FontSize="13"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Chiave pubblica -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[addr.pubkey]}" FontSize="11" Foreground="Gray"/>
|
||||
<SelectableTextBlock Text="{Binding PubKey}"
|
||||
FontFamily="monospace" FontSize="12"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Chiave privata (solo se disponibile) -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding HasPrivKey}">
|
||||
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11" Foreground="OrangeRed"/>
|
||||
<SelectableTextBlock Text="{Binding PrivKey}"
|
||||
FontFamily="monospace" FontSize="12"
|
||||
Foreground="OrangeRed"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Content="{Binding Loc[addr.close]}"
|
||||
HorizontalAlignment="Right"
|
||||
Click="OnCloseClick"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -1,20 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using PalladiumWallet.App.ViewModels;
|
||||
|
||||
namespace PalladiumWallet.App.Views;
|
||||
|
||||
public partial class AddressInfoWindow : Window
|
||||
{
|
||||
public AddressInfoWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AddressInfoWindow(AddressInfo info) : this()
|
||||
{
|
||||
DataContext = info;
|
||||
}
|
||||
|
||||
private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
+36
-113
@@ -37,18 +37,6 @@
|
||||
<MenuItem Header="English" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangEn, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="en"/>
|
||||
<MenuItem Header="Español" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangEs, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="es"/>
|
||||
<MenuItem Header="Français" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangFr, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="fr"/>
|
||||
<MenuItem Header="Português" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangPt, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="pt"/>
|
||||
<MenuItem Header="Deutsch" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangDe, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="de"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{Binding Loc[settings.unit.short]}">
|
||||
<MenuItem Header="PLM" ToggleType="Radio"
|
||||
@@ -222,10 +210,18 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Tab: Storico / Invia / Ricevi / Indirizzi / Contatti -->
|
||||
<!-- Tab: Ricevi / Storico / Indirizzi / Invia -->
|
||||
<TabControl Grid.Row="2">
|
||||
<TabItem Header="{Binding Loc[tab.receive]}">
|
||||
<StackPanel Spacing="10" Margin="8">
|
||||
<TextBlock Text="{Binding Loc[receive.next]}"/>
|
||||
<SelectableTextBlock Text="{Binding ReceiveAddress}"
|
||||
FontFamily="monospace" FontSize="16"/>
|
||||
<TextBlock Text="{Binding Loc[receive.hint]}"
|
||||
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 1. Storico -->
|
||||
<TabItem Header="{Binding Loc[tab.history]}">
|
||||
<ListBox ItemsSource="{Binding History}" Margin="4">
|
||||
<ListBox.ItemTemplate>
|
||||
@@ -242,23 +238,34 @@
|
||||
</ListBox>
|
||||
</TabItem>
|
||||
|
||||
<!-- 2. Invia -->
|
||||
<TabItem Header="{Binding Loc[tab.addresses]}">
|
||||
<Grid RowDefinitions="Auto,*" Margin="4">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="90,60,*,140,60" Margin="12,4">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Loc[addr.type]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Loc[addr.index]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Loc[addr.address]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Loc[addr.balance]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="4" Text="Tx" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding Addresses}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AddressRow">
|
||||
<Grid ColumnDefinitions="90,60,*,140,60">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Tipo}" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Indice}" Foreground="Gray"/>
|
||||
<SelectableTextBlock Grid.Column="2" Text="{Binding Indirizzo}"
|
||||
FontFamily="monospace" FontSize="13"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Saldo}" FontFamily="monospace"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding NumTx}" Foreground="Gray"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{Binding Loc[tab.send]}">
|
||||
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left">
|
||||
<!-- Contatto rapido -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Loc[send.from.contact]}" VerticalAlignment="Center"/>
|
||||
<ComboBox ItemsSource="{Binding Contacts}"
|
||||
SelectedItem="{Binding SendToContact}"
|
||||
PlaceholderText="{Binding Loc[send.contact.hint]}"
|
||||
MinWidth="220">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ContactEntry">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<TextBox PlaceholderText="{Binding Loc[send.to]}" Text="{Binding SendTo}"
|
||||
FontFamily="monospace"/>
|
||||
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
|
||||
@@ -283,90 +290,6 @@
|
||||
FontSize="13"/>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 3. Ricevi -->
|
||||
<TabItem Header="{Binding Loc[tab.receive]}">
|
||||
<StackPanel Spacing="10" Margin="8">
|
||||
<TextBlock Text="{Binding Loc[receive.next]}"/>
|
||||
<SelectableTextBlock Text="{Binding ReceiveAddress}"
|
||||
FontFamily="monospace" FontSize="16"/>
|
||||
<TextBlock Text="{Binding Loc[receive.hint]}"
|
||||
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 4. Indirizzi -->
|
||||
<TabItem Header="{Binding Loc[tab.addresses]}">
|
||||
<Grid RowDefinitions="Auto,*" Margin="4">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="90,60,*,140,60" Margin="12,4">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Loc[addr.type]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Loc[addr.index]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Loc[addr.address]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Loc[addr.balance]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="4" Text="Tx" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding Addresses}"
|
||||
SelectedItem="{Binding SelectedAddressRow}"
|
||||
x:Name="AddressesListBox"
|
||||
Tapped="OnAddressListTapped"
|
||||
PointerPressed="OnAddressListPointerPressed">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AddressRow">
|
||||
<Grid ColumnDefinitions="90,60,*,140,60">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Tipo}" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Indice}" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Indirizzo}"
|
||||
FontFamily="monospace" FontSize="13"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Saldo}" FontFamily="monospace"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding NumTx}" Foreground="Gray"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 5. Contatti -->
|
||||
<TabItem Header="{Binding Loc[tab.contacts]}">
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto" Margin="4">
|
||||
<!-- Intestazioni colonne -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="180,*" Margin="12,4">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Loc[contacts.name]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Loc[contacts.address]}" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
<!-- Lista contatti -->
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding Contacts}"
|
||||
SelectedItem="{Binding SelectedContactInList}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ContactEntry">
|
||||
<Grid ColumnDefinitions="180,*">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}"
|
||||
VerticalAlignment="Center"/>
|
||||
<SelectableTextBlock Grid.Column="1" Text="{Binding Address}"
|
||||
FontFamily="monospace" FontSize="12"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<!-- Pulsante rimuovi selezionato -->
|
||||
<Button Grid.Row="2" Content="{Binding Loc[contacts.remove]}"
|
||||
Command="{Binding RemoveSelectedContactCommand}"
|
||||
IsEnabled="{Binding SelectedContactInList, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Margin="0,6,0,0"/>
|
||||
<!-- Form aggiungi contatto -->
|
||||
<Grid Grid.Row="3" ColumnDefinitions="180,*,Auto" Margin="0,8,0,0">
|
||||
<TextBox Grid.Column="0" PlaceholderText="{Binding Loc[contacts.name.ph]}"
|
||||
Text="{Binding NewContactName}" Margin="0,0,6,0"/>
|
||||
<TextBox Grid.Column="1" PlaceholderText="{Binding Loc[contacts.address.ph]}"
|
||||
Text="{Binding NewContactAddress}" FontFamily="monospace"
|
||||
Margin="0,0,6,0"/>
|
||||
<Button Grid.Column="2" Content="{Binding Loc[contacts.add]}"
|
||||
Command="{Binding AddContactCommand}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using PalladiumWallet.App.ViewModels;
|
||||
|
||||
namespace PalladiumWallet.App.Views;
|
||||
@@ -18,6 +13,7 @@ public partial class MainWindow : Window
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>File → Apri wallet da file (il picker richiede il TopLevel, da qui).</summary>
|
||||
private async void OnOpenWalletFileClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm)
|
||||
@@ -36,26 +32,4 @@ public partial class MainWindow : Window
|
||||
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
|
||||
vm.OpenFromPath(path);
|
||||
}
|
||||
|
||||
private async void OnAddressListTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm || vm.SelectedAddressRow is not { } row)
|
||||
return;
|
||||
var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
||||
await new AddressInfoWindow(info).ShowDialog(this);
|
||||
}
|
||||
|
||||
private async void OnAddressListPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!e.GetCurrentPoint(null).Properties.IsRightButtonPressed) return;
|
||||
if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
|
||||
|
||||
var item = (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>();
|
||||
if (item is not { DataContext: AddressRow row }) return;
|
||||
|
||||
lb.SelectedItem = row;
|
||||
|
||||
var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
||||
await new AddressInfoWindow(info).ShowDialog(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace PalladiumWallet.Core.Storage;
|
||||
/// </summary>
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>Codice lingua UI.</summary>
|
||||
public string Language { get; set; } = "en";
|
||||
/// <summary>Codice lingua UI ("it", "en").</summary>
|
||||
public string Language { get; set; } = "it";
|
||||
|
||||
/// <summary>Unità di visualizzazione degli importi (vedi <see cref="Wallet.CoinAmount.Units"/>).</summary>
|
||||
public string Unit { get; set; } = "PLM";
|
||||
|
||||
@@ -18,12 +18,9 @@ public static class AppPaths
|
||||
if (Directory.Exists(portable))
|
||||
return portable;
|
||||
|
||||
// Windows → %APPDATA%\PalladiumWallet
|
||||
// Linux → $XDG_CONFIG_HOME/PalladiumWallet (default ~/.config/PalladiumWallet)
|
||||
// macOS → ~/Library/Application Support/PalladiumWallet
|
||||
return Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"PalladiumWallet");
|
||||
return OperatingSystem.IsWindows()
|
||||
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PalladiumWallet")
|
||||
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".palladium-wallet");
|
||||
}
|
||||
|
||||
/// <summary>Cartella dati della rete (config, wallet, header, certificati).</summary>
|
||||
|
||||
@@ -40,9 +40,6 @@ public sealed class WalletDocument
|
||||
/// <summary>Etichette per indirizzo/txid (§12).</summary>
|
||||
public Dictionary<string, string> Labels { get; set; } = [];
|
||||
|
||||
/// <summary>Rubrica contatti (nome + indirizzo blockchain).</summary>
|
||||
public List<StoredContact> Contacts { get; set; } = [];
|
||||
|
||||
/// <summary>Cache dell'ultimo stato sincronizzato (saldo/storico mostrabili offline).</summary>
|
||||
public SyncCache? Cache { get; set; }
|
||||
|
||||
@@ -72,13 +69,6 @@ public sealed class WalletDocument
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Contatto in rubrica: nome leggibile + indirizzo blockchain.</summary>
|
||||
public sealed class StoredContact
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Address { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Stato sincronizzato persistito: permette di mostrare saldo/storico offline.</summary>
|
||||
public sealed class SyncCache
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user