Commit Graph

23 Commits

Author SHA1 Message Date
davide 5bb94c071f Merge branch 'watch-only'
Adds a pure address-only watch-only wallet mode (Core + CLI + App wizard
+ Send UI), on top of the existing xpub/xprv/WIF import flows which are
unchanged. TransactionFactory already refuses to sign for any IsWatchOnly
account, so the new address-only accounts inherit that guarantee for free.
2026-07-19 12:44:51 +02:00
davide 214abd2892 feat(core): support pure watch-only address import
Adds a WalletDocument.WatchAddresses field and WalletLoader.NewFromAddresses
to build an ImportedKeyAccount with no private key at all (unlike xpub/WIF
imports, which can still derive/hold key material). ScriptKind is inferred
from the address itself via a new DerivationPaths.KindFor helper. Signing
already refuses to run for any IsWatchOnly account (TransactionFactory), so
this only had to wire up construction/reload of the new account shape.
2026-07-19 12:37:31 +02:00
davide d9dd05aa52 perf(spv): persist checkpoint-anchoring state across sync sessions
_anchoredUpTo tracked which heights were already proven to hash-chain
back to a checkpoint, but only in memory: every app restart re-walked
and re-verified the whole header chain from the checkpoint even when
the header bytes themselves were already cached on disk, dominating
reconnect time on large wallets. Round-trip it through SyncCache like
the other sync caches (RawTxHex/VerifiedAt/BlockHeaders).
2026-07-19 12:16:00 +02:00
davide 3460e53b4f feat(spv): verify Merkle proofs progressively, gate spendability on it
Balance/history now render as soon as tx downloads finish instead of
blocking on every historical Merkle proof, critical for mobile where
proof-checking can take much longer than the download itself. Proofs
continue to be checked in the background and each tx's Verified flag
catches up progressively; header ranges are now fetched in batches
(blockchain.block.headers) instead of one call per header to keep this
fast over high-latency links.

Coin selection (UtxoSpendability.IsSpendable) refuses to spend a UTXO
until its Merkle proof is actually checked, regardless of confirmation
count, so a server that fabricates a confirmed balance can get it
displayed early but never spent before the forgery is caught. The disk
cache only ever persists the fully-verified end state of a sync.

UI surfaces the new PendingVerificationSats/SpendableSats split with a
"verifying..." badge, and the sync save now runs off the UI thread to
avoid freezing on slower hardware.
2026-07-17 19:43:18 +02:00
davide a264669151 fix(storage): EncryptedFile.Decrypt must reject malformed containers cleanly
A tampered or corrupted wallet file could reach Decrypt with broken
JSON, missing fields, invalid base64, or a wrong nonce/tag size, all of
which previously escaped as raw JsonException/FormatException/
ArgumentNullException instead of the documented WrongPasswordException/
InvalidDataException contract. Worse, the iteration count is read
straight from the (attacker-controlled) container with no upper bound:
a tampered file demanding e.g. 2^31 PBKDF2 iterations would hang the
wallet at open - a DoS via a file the user merely tries to open. Now
every malformed shape maps to InvalidDataException and the iteration
count is clamped to a sane maximum (10,000,000, well above the current
600,000 default).
2026-07-07 22:05:08 +02:00
davide 413392f608 test(storage): exercise AppPaths' full data-root precedence chain
DefaultDataRoot, the pointer-file location and the portable-mode root
all read machine-global locations (APPDATA, AppContext.BaseDirectory),
so the override→portable→pointer→default precedence in DataRoot/
IsDataLocationConfigured was previously untestable beyond the override
case. Added three internal overrides (BootstrapDirOverride,
PortableBaseOverride, DefaultRootOverride) as sandboxing seams, and a
new AppPathsResolutionTests class covering every precedence branch:
default-only, portable-beats-pointer, pointer-beats-default, a blank
pointer falling back to default, "already has data" counting as
configured, and override beating everything. Both AppPaths test
classes share the "AppPaths" xUnit collection since this state is
static.
2026-07-07 16:26:20 +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 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 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 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 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 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 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 4d2ae1fd13 feat(storage): encrypted wallet file and persistence 2026-06-11 10:47:31 +02:00