Commit Graph

36 Commits

Author SHA1 Message Date
davide e349a80ddc feat(spv): anchor header trust to hardcoded mainnet checkpoints
ChainProfiles.Mainnet.Checkpoints was an empty array with a "populate
before release" TODO, and even BlockHeaderInfo.MatchesCheckpoint()/
IsValidChild() — the methods meant to enforce it — were never called
anywhere in WalletSynchronizer. So on this LWMA chain, where PoW can't
be recomputed client-side (SkipPowValidation), a malicious or
eclipsing server could hand back any internally-consistent 80-byte
header for a Merkle proof: nothing tied it to the real Palladium
chain.

Populated Mainnet.Checkpoints with 24 real [height, hash, bits]
checkpoints (every 20,000 blocks + one near tip), pulled via RPC from
a fully-synced palladiumd. Testnet/Regtest are left empty (no node
available to verify against, and WalletSynchronizer already treats a
missing checkpoint as a no-op, so this is safe, just unanchored).

Added WalletSynchronizer.AnchorToCheckpointAsync: for every header
used in a Merkle proof, downloads the intervening headers back to the
nearest checkpoint at or below that height and verifies an unbroken
prev-hash chain terminating in the checkpoint's exact hash, finally
wiring up the previously-dead MatchesCheckpoint/IsValidChild. Verified
ranges are memoized in-memory per sync session to avoid re-walking.

Updated SECURITY.md and USERGUIDE.md to describe what is actually
enforced now (previously the guide deliberately avoided promising
checkpoint anchoring, since the array was empty).
2026-07-07 15:12:17 +02:00
davide af2fdcc894 fix(gui): route runtime status messages through Loc instead of hardcoded Italian
Send/Donate/Sync/Wizard view models wrote status/error strings
directly in Italian ("Importo non valido.", "Errore: …", the TLS
pin-mismatch message, …), so a user running the app in English/German/
etc. still saw them in Italian despite the 6-language Loc system
covering the rest of the UI.

Added the missing Loc keys and routed every occurrence through
Loc.Tr(...). CertificatePinMismatchException (Core) no longer bakes an
Italian message into .Message; it now exposes Host/Port so the UI can
translate it, with an English fallback for any exception type the UI
doesn't specifically handle. Also drops the now-stale USERGUIDE.md
caveat about a few messages staying in Italian regardless of language.
2026-07-07 14:45:43 +02:00
davide e8ff99a768 test: add in-process fake ElectrumX server for network-layer testing
Real loopback TCP socket speaking newline-delimited JSON-RPC 2.0, optionally
TLS with a self-signed certificate, with per-method handlers and call
counters. Lets ElectrumClient, WalletSynchronizer, TransactionInspector, and
CertificatePinStore be exercised against the same code paths used in
production instead of being mocked.

Also exposes UpdateChecker's tag-parsing logic internally so its version
comparison can be tested without a real GitHub API call.
2026-07-02 23:20:32 +02:00
davide 27231c8eec fix(core): stop corrupted state from permanently breaking SSL and wallet loading
CertificatePinStore.Load threw on a corrupted pin file, blocking every SSL
connection until the user manually deleted it; EncryptedFile.IsEncrypted
threw on valid JSON with a non-object root or invalid UTF-16 instead of
returning false. Both now degrade gracefully. Found by property-based tests
while extending the test suite.
2026-07-02 23:19:46 +02:00
davide 34b4313a36 feat(app): check GitHub releases on startup and prompt for updates
Compares the running assembly version against the latest GitHub release
tag on every app launch (desktop and Android) and shows an in-app
overlay with the new version tag when one is available. Best-effort:
network/parse failures are silent, matching the app's existing
non-blocking startup checks.
2026-07-02 21:33:18 +02:00
davide 6c24a8bb46 feat(wallet): surface immature balance separately from confirmed
Extract confirmation-threshold logic into UtxoSpendability (shared by
TransactionFactory and WalletSynchronizer) and use it to compute
ImmatureSats, so coinbase/under-confirmed amounts are shown separately
from spendable balance in the GUI and CLI instead of being lumped into
"confirmed".
2026-07-02 18:24:33 +02:00
davide 9aecdb1aaa feat(wallet): enforce confirmation thresholds before spending UTXOs
Coinbase outputs require COINBASE_MATURITY + 1 = 121 confirmations before
they can be spent. The consensus rule (palladiumcore tx_verify.cpp) is
nSpendHeight - nHeight >= 120; the +1 adds one block of safety margin,
matching the Qt wallet (wallet.cpp: COINBASE_MATURITY+1). Regular UTXOs
require MinConfirmations (6 on mainnet, 1 on testnet/regtest — wallet
policy, no consensus rule).

Changes:
- ChainProfile: add CoinbaseMaturity and MinConfirmations fields
- ChainProfiles: mainnet CoinbaseMaturity=120 / MinConfirmations=6;
  testnet/regtest inherit 120, override MinConfirmations=1
- PalladiumNetworks: align NBitcoin Consensus.CoinbaseMaturity to 120
- CachedUtxo: add IsCoinbase flag (ElectrumX listunspent does not expose
  this; derived from Transaction.IsCoinBase on the raw tx)
- WalletSynchronizer: set IsCoinbase during local UTXO reconstruction
- TransactionFactory.Build: add tipHeight param; filter by threshold;
  error reports reason (immature coinbase with X/121 counter, under-
  confirmed regulars with best-seen count, mempool amounts)
- All Build() call sites updated (App, Donate, CLI)
- Two new tests: coinbase maturity boundary (119→220 tip) and mainnet
  min-conf boundary (5→6 confirmations)
2026-07-02 16:25:42 +02:00
davide c5290a1796 feat(ui): persist last server and split connection status text
- AppConfig stores LastServerHost/Port/UseSsl; restored on next launch
  so the wallet reconnects to the last-used server instead of defaulting
  to the first bootstrap entry
- Added ConnectionStatusShort for the status bar (short label only);
  ConnectionStatus retains the full "Connesso a ip:porta" shown in the
  server settings overlay
2026-06-18 19:34:41 +02:00
davide 3d5a226a5a docs: translate all code comments to English (language policy)
Translate every Italian /// XML doc comment, <!-- --> XAML comment, and
// inline comment to English across all source files (Core, App, tests).
Add the language policy to CLAUDE.md (conversation Italian; all code
and docs English). Update one test assertion that checked an Italian
exception message that was also translated.
2026-06-16 14:40:11 +02:00
davide 5ff2075a45 perf(spv): incremental discovery, parallel chains, persistent header cache
Speed up wallet synchronization, especially re-syncs of large wallets:

- Incremental address discovery: known indices from the previous sync
  (NextReceiveIndex/NextChangeIndex) are fetched in a single parallel
  burst instead of sequential gap-limit rounds, cutting round-trips from
  O(used/gapLimit) to ~O(1). Receive and change chains scan in parallel.
- Merge tx download and Merkle verification into one parallel phase
  (no barrier between them); the per-call semaphore is dropped in favor
  of the transport-level in-flight gate.
- Persist verified block headers (SyncCache.BlockHeaders, height -> hex):
  immutable once stored, so blockchain.block.header round-trips for
  already-verified proofs are eliminated on later syncs.
- RetryOnBusyAsync with exponential backoff around server calls, treating
  -101/-102 and "server busy"/"excessive resource usage" as transient.

Wire the new PreloadCaches/ExportCaches signatures through the desktop
view model and the CLI sync command.
2026-06-16 09:27:48 +02:00
davide 38e0f0a52e perf(net): batched writes, zero-alloc reads, bounded in-flight requests
Rework the ElectrumClient transport to cut latency and allocations
during sync:

- Channel-based single-reader write loop: drains the whole queue into a
  single WriteAsync+FlushAsync, replacing the _writeLock that forced one
  flush per message. N queued requests now travel in one TCP segment.
- PipeReader read loop parsing via Utf8JsonReader over pooled spans: no
  StreamReader/ReadLineAsync and no intermediate string per response.
- TcpClient.NoDelay = true: no Nagle wait on small packets.
- Concurrency gate (SemaphoreSlim, MaxInFlight=32) in RequestAsync:
  caps requests in flight to the server without serializing writes,
  avoiding floods (-101/-102) and connection drops on wallets with large
  history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:27:40 +02:00
davide 3054a9baaa fix(spv): resilient sync — GetHistory discovery, tx cache, auto-connect fallback
Three interrelated fixes for sync reliability on large wallets:

1. Prevent -101 "excessive resource usage":
   ScanChainAsync uses GetHistoryAsync for discovery instead of
   SubscribeScripthashAsync. Subscriptions are limited to the gap window
   only (≤ 2×gapLimit addresses), so the session subscription count stays
   constant regardless of wallet history size — eliminating -101 on
   wallets with 1000+ historical addresses.

2. Persist and resume tx cache across sessions and disconnects:
   WalletSynchronizer gains ExportCaches() and PreloadCaches(). On every
   successful sync (and on partial failure) the downloaded raw tx hex and
   verified Merkle heights are written to SyncCache.RawTxHex/VerifiedAt
   in the wallet file. On reconnect or app restart the new synchronizer
   is pre-populated from disk so only new transactions are downloaded.
   _synchronizer is always nulled on reconnect (new client = new instance)
   while caches survive via the wallet file.

3. Auto-connect with multi-server fallback and retry:
   ConnectAndSync iterates BuildServerCandidates() (current server first,
   then all KnownServers in order) and stops at the first that responds.
   _autoReconnect and _syncFailed are set in OpenLoaded() so the keepalive
   timer (20 s) retries automatically on any failure — connection error,
   -101 mid-download, or timeout. PersistPartialTxCache() saves whatever
   was downloaded so each retry resumes from its checkpoint.
2026-06-15 14:23:31 +02:00
davide 47b6064964 feat(wallet): IWalletAccount abstraction and WIF/xpub/xprv keystore import types
Introduce IWalletAccount interface to abstract HD, imported-xprv, WIF,
and watch-only account types. HdAccount implements the interface; the
new ImportedKeyAccount handles lists of WIF keys with fixed addresses
(no gap scanning, change returns to first address).

WalletLoader gains three factory methods:
- NewFromXpub: watch-only from SLIP-132 xpub/ypub/zpub
- NewFromXprv: spendable from SLIP-132 xprv/yprv/zprv
- NewFromWif: spendable from one or more WIF private keys

WalletDocument gains optional AccountXprv and WifKeys fields; AccountPath
and AccountXpub become nullable (absent for WIF wallets). IsWatchOnly
updated to cover all non-signing wallet kinds.

TransactionFactory and CLI OpenWallet() updated to IWalletAccount.
12 new unit tests in ImportedKeyAccountTests cover factory round-trips,
address derivation, and watch-only detection.
2026-06-15 14:20:13 +02:00
davide 002c854497 feat(wallet): add P2PKH, P2SH-P2WPKH, and P2TR (Taproot/BIP86) address types
- ScriptKind.Taproot (BIP86, purpose 86', P2WPKH → witness v1 bech32m plm1p)
  derivation wired in DerivationPaths and ChainProfiles (xprv/xpub headers,
  no SLIP-132 standard for P2TR); NBitcoin ScriptPubKeyType.TaprootBIP86
- Wizard step StepScriptType inserted between passphrase and password: 4 radio
  buttons (Legacy / Wrapped SegWit / Native SegWit★ / Taproot), localized in
  6 languages; CreateOrRestore now uses SelectedScriptKind instead of hardcoded
  NativeSegwit; back navigation updated accordingly
- BIP86 golden-vector tests (Bip86TaprootTests): output key verified against
  official Bitcoin BIP86 addresses, plm1p prefix asserted
2026-06-15 12:03:52 +02:00
davide 5d061c6f21 docs(storage): document plaintext save caveat on WalletStore.Save
The password parameter accepts null, which saves the wallet in plaintext.
The XML doc comment now makes explicit that this is only acceptable when
the user has explicitly opted out of encryption with a UI warning shown.
2026-06-13 21:15:28 +02:00
davide 58645bd7a7 fix(storage): acquire wallet lock before load and before CloseWallet
Three improvements to WalletLock integration:

1. OpenExisting acquires the lock before decrypting the file — no point
   attempting a potentially expensive PBKDF2 derivation for a wallet
   already held by another instance.

2. OpenFromPath acquires the lock before CloseWallet — if the new wallet
   is unavailable, the current session stays intact instead of being
   silently destroyed.

3. OpenLoaded now receives an already-acquired WalletLock from the caller
   instead of acquiring it internally; TryAcquireWalletLock is removed.
   Callers that fail to open (wrong password, I/O error) dispose the lock
   in their catch blocks.

Also fix CoinAmount.Of() to throw ArgumentException for unknown units
instead of silently falling back to PLM.
2026-06-13 20:50:22 +02:00
davide 008e9c395a fix(wallet): reject amounts with sub-satoshi precision
Replace the silent (long) cast with an explicit fractional check:
if value * factor has a non-zero remainder, TryParseIn/TryParseCoins
return false. The caller already shows "Importo non valido." to the
user, so no UI change is needed.

Covers TryParseIn (all units) and TryParseCoins. Adds CoinAmountTests
with valid and invalid cases including the triggering example (1.9 sat).
2026-06-13 20:39:08 +02:00
davide 0f8a764a44 feat(storage): add WalletLock to prevent concurrent wallet access
Introduces an exclusive file lock (FileShare.None on wallet.json.lock)
held for the duration of a wallet session. A second instance trying to
open the same file receives null from TryAcquire and sees a localized
error message; UnauthorizedAccessException (OS permission issues)
propagates separately so the UI can show a distinct message.
The .lock file is deleted on Dispose (best-effort); the real guard is
the open FileStream, which the OS releases automatically on crash.
2026-06-13 20:34:06 +02:00
davide e94eaf7700 refactor(arch): split App into shared library + Desktop + Android heads
The Avalonia UI code (App, Views, ViewModels, Localization, Assets) now
lives in src/App as a plain library (no OutputType). Two thin heads
reference it:

- src/App.Desktop/ — WinExe, Avalonia.Desktop, hosts MainView in a
  MainWindow; carries Program.cs and app.manifest (moved from src/App)
- src/App.Android/ — net10.0-android, Avalonia.Android, MainActivity/
  MainApplication; targets API 23+, EmbedAssembliesIntoApk=true so the
  apk is self-contained for sideloading

All event handlers and TopLevel-dependent calls (file picker, clipboard,
folder picker) moved from MainWindow.axaml.cs into the new shared
MainView.axaml.cs (UserControl), using TopLevel.GetTopLevel(this) so
they work on both platforms. Esc/Back key handling is also in MainView.
MainWindow becomes a thin shell that hosts MainView.

Framework bump: all projects move to net10.0; Cli and Tests follow.
2026-06-12 16:06:46 +02:00
davide 4735490759 feat(app): transaction detail overlay with full on-chain data
Double-clicking a history row opens an in-app overlay (same pattern as
address/server/settings overlays) that shows all available data for a
transaction. The overlay appears immediately with a spinner; data is
fetched from the server in the background and rendered once ready.
Backdrop tap and Esc close it; a pending fetch is cancelled.

New Core type — TransactionInspector (Core/Wallet/):
- FetchAsync() downloads the raw tx, all parent txs (parallel requests,
  one round-trip instead of N sequential), and the block header
- Reconstructs inputs (amounts, addresses, is-mine), outputs, fee,
  virtual size, RBF flag, block timestamp, confirmations

New App types:
- TransactionDetails record: full parsed data (fee, net, I/O lists,
  counterparty addresses, coinbase detection…)
- TransactionDetailsViewModel: pre-formats all strings in the current
  locale and unit (status, date, signed amounts, sat/vB rate…)
- TxIoRow record: one row in the input/output tables
- MineColorConverter: bool → brush (MediumSeaGreen for own addresses)

ViewModel changes:
- ShowTransactionDetailsAsync / CloseTransactionDetailsCommand
- BuildTransactionDetailsAsync (also public for future RBF/CPFP use)
- CancellationTokenSource so opening a new tx cancels the previous fetch
- All server work runs on Task.Run to keep the UI thread free

XAML: history rows get DoubleTapped handler, Hand cursor, hint label,
txid now uses TextTrimming instead of SelectableTextBlock.

i18n: 24 new tx.* / history.hint keys in all 6 languages.
2026-06-12 11:48:33 +02:00
davide 7f2759b2fc feat(app): multi-wallet chooser, confirm-password, encrypt toggle
Multi-wallet (§8):
- AppPaths.WalletFiles(net) enumerates all *.wallet.json in the wallets
  dir; WizardStartOpen shows a chooser step when more than one is found
- New StepChooseWallet + ChooseWalletCommand; Back from StepOpen returns
  to chooser (or to StepStart when only one wallet)
- WalletFileEntry record for the chooser ItemsControl

Password step (Electrum style):
- ConfirmPasswordInput: password must be typed twice before creating
- EncryptWallet checkbox (default true); when unchecked, password is
  null (plaintext file) with an explicit warning
- Validation: empty password with encryption on → msg.password.required;
  mismatch → msg.password.mismatch
- ConfirmPasswordInput cleared on wallet open and wizard back

AppPaths:
- DefaultDataRoot() is now platform-aware: %APPDATA%\PalladiumWallet on
  Windows, ~/.palladium-wallet on Linux/macOS (matches §8 convention)
- Legacy root fallback removed (no prior releases to migrate from)

i18n: wiz.choose.title, wiz.password.confirm, wiz.password.encrypt,
wiz.password.encrypt.hint, msg.choose.wallet, msg.password.required,
msg.password.mismatch — all 6 languages
2026-06-12 10:13:17 +02:00
davide f3bf4cf94a feat(app): first-run data location wizard step
On first launch (no data yet, no portable dir, no pointer file),
the wizard now shows a new step-0 screen asking where to store
wallets, config and certificates.

AppPaths changes:
- DefaultDataRoot() → ~/.PalladiumWallet (home, always writable)
- IsDataLocationConfigured() → true when portable / override / pointer
  already written / legacy or default already has data
- ConfigureDataLocation(root) writes a bootstrap pointer file and
  creates the directory
- DataRoot() resolution order: override → portable → pointer → legacy
  (has data) → default

ViewModel: new StepDataLocation step, UseDefaultDataLocationCommand,
ApplyDataLocation(root) (also called from View's folder picker).

View: new wizard panel with description, default-path display, two
buttons (use default / choose folder); folder picker via
StorageProvider.OpenFolderPickerAsync.

Loc: add wiz.data.* keys (6 languages); fix fallback language "it"→"en";
update test assertion accordingly.
2026-06-12 09:25:04 +02:00
davide 6fe31964e1 feat(storage): add contacts list to WalletDocument
Adds StoredContact class and a Contacts list to WalletDocument so the
address book survives app restarts inside the encrypted wallet file.
2026-06-11 21:39:18 +02:00
davide 8ab8bbd8b3 feat(storage): XDG-compliant paths and default language English
AppPaths: use Environment.SpecialFolder.ApplicationData for all platforms
  - Windows  → %APPDATA%\PalladiumWallet  (unchanged)
  - Linux    → ~/.config/PalladiumWallet  (XDG standard, was ~/.palladium-wallet)
  - macOS    → ~/Library/Application Support/PalladiumWallet

AppConfig: change default language from "it" to "en" for new installs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 18:41:47 +02:00
davide 75583f7a69 feat(app): settings menu with language and display unit
Add global AppConfig (config.json, blueprint §8) persisting language
and amount unit. CoinAmount gains unit-aware formatting and parsing
(PLM, mPLM, µPLM, sat), applied everywhere amounts are shown or
entered, including the send form. New Loc i18n layer (it/en) with
live-updating XAML bindings covering menu, wizard, wallet panel and
status messages.

The Impostazioni menu opens a dropdown with separate Lingua and Unità
entries (radio-checked submenus, applied and saved on click), ready to
host future settings.
2026-06-11 16:23:06 +02:00
davide 73a2d5356d feat(spv): Electrum-style continuous updates with incremental parallel sync
WalletSynchronizer now pipelines JSON-RPC requests (address subscribes,
histories, tx downloads and Merkle proofs run in parallel batches) and
keeps per-connection caches of downloaded transactions and verified
proofs, so notification-triggered resyncs only cost the delta.

The app connects automatically when a wallet opens, keeps one
synchronizer per connection, and queues notifications that arrive
during a sync instead of dropping them: incoming transactions now show
up within seconds.
2026-06-11 13:37:34 +02:00
davide ece634f42e feat(wallet): show pending mempool balance and exclude unconfirmed UTXOs from spending
TransactionFactory now selects only confirmed UTXOs (height > 0), so
mempool funds are visible as pending balance but never spendable, with
a clear error when only unconfirmed funds are available. GUI and CLI
labels updated to "in attesa di conferma (non spendibile)".
2026-06-11 11:34:21 +02:00
davide 08217e5306 feat(spv): per-address balance and tx count in sync result 2026-06-11 11:27:31 +02:00
davide 1c2dacc27c feat(net): server registry, bootstrap servers, peer discovery 2026-06-11 11:27:16 +02:00
davide 3d6597a3b2 feat(wallet): coin selection, tx factory, wallet loader 2026-06-11 10:47:43 +02:00
davide 4d2ae1fd13 feat(storage): encrypted wallet file and persistence 2026-06-11 10:47:31 +02:00
davide bd5e740599 feat(spv): header sync, Merkle proofs, scripthash 2026-06-11 10:47:16 +02:00
davide df60763ddd feat(net): ElectrumX client with TLS pinning 2026-06-11 10:46:57 +02:00
davide 8a0dc920f6 feat(crypto): BIP-32/39/84, HD accounts, SLIP-132 2026-06-11 10:46:38 +02:00
davide cdd19024ad feat(chain): network profiles and consensus constants 2026-06-11 10:46:26 +02:00
davide ca70570242 chore: solution and project files 2026-06-11 10:46:10 +02:00