Fills in the CHANGELOG.md entry for the watch-only address-only import
mode, progressive Merkle verification, transaction size-limit fix, and
Android sync-reconnect fixes since 1.0.0, and bumps <Version>/versionCode
across the App and Android head csproj files ahead of the tag.
The tab requires an open wallet to send from, so showing it before
one is loaded led to a dead end. Gate it behind IsWalletOpen, same as
the other wallet-only UI.
Width-capped containers (wizard steps, private-key prompt, wallet info
overlay) used HorizontalAlignment="Center" with MaxWidth, which sizes
the panel from its content's DesiredSize. Since PlaceholderText only
contributes to that measurement while a TextBox is empty and
unfocused, clicking into an empty field shrank the whole panel.
Switch to HorizontalAlignment="Stretch": Avalonia's ArrangeCore sizes
Stretch from the available arrange rect (clamped by MaxWidth) instead
of DesiredSize, and centers the result exactly like Center alignment
does when MaxWidth caps it — so the box stays a fixed width regardless
of focus/placeholder state, on both desktop and Android.
Three issues surfaced by testing the previous lock/unlock reconnect fix
on Android with a large wallet:
- The keep-alive ping had no timeout, so a "half-open" TCP connection
(remote end gone with no FIN/RST ever delivered, the common outcome
of Doze/mobile-radio suspend after a longer lock) left it awaiting a
response that never arrives — the failure path that tears the dead
client down was never reached. Bound the ping to 8s and added a
re-entrancy guard so overlapping 20s ticks can't pile up concurrent
reconnect attempts while one is stuck.
- CheckConnectionOnResumeAsync bailed out whenever a sync was already
in progress, so a lock/unlock during an active sync (which has no
per-request timeout of its own) left it hung indefinitely instead of
recovering. It now cancels the stuck sync and tears down the dead
client, marking the interruption as self-inflicted (_resumeRecovering)
so the UI shows "reconnecting" and restarts immediately instead of a
transient error message.
- WalletSynchronizer.ExportCaches persisted raw transaction bytes only
for already-verified transactions, discarding anything downloaded but
not yet through Merkle-proof verification when a sync was interrupted
mid-way — forcing a full re-download of a large wallet's transactions
on every resume instead of resuming straight into proof verification.
Track confirmed txids at download time (_confirmedTxids), independent
of verification status, and export/preload against that instead.
Also unified the three sync progress messages onto one template
reporting against the sync's total transaction/proof counts rather than
this session's download count, so a resumed sync immediately shows
"transactions n/n" instead of a misleading "0/0" before jumping into
proof verification.
TcpClient.Connected only reflects the last known socket state, so a
connection killed silently while the phone was locked (Doze, mobile
radio suspend, NAT timeout) still reported IsConnected == true. The
keep-alive ping's failure was swallowed by an empty catch, so the
stale "connected" state never cleared and sync kept retrying on a
dead socket instead of reconnecting.
Treat a failed keep-alive ping as a disconnect (tear down the client
and reconnect), and add OnPause/OnResume to the Android activity to
force an immediate health check on resume instead of waiting for the
20s timer, which may itself be suspended during Doze.
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.
Send already refused to enable Confirm for watch-only accounts (Signed
false), but the App had no way to get the resulting PSBT out to sign
elsewhere — only the CLI printed it. Adds a base64 PSBT box + copy button
in the Send summary card, and a visible warning banner explaining the
wallet cannot sign, on both desktop and mobile layouts.
New wizard flow mirroring the existing xpub/WIF import steps: paste one
or more plain addresses (no key material at all) and go straight to the
password step, since there is no derivation and no script-type ambiguity
to resolve for a fixed address list.
Parallels restore-xpub but for one or more plain addresses with no
extended key and no private key — output is explicitly labelled
"cannot sign" since the resulting wallet can never produce a signed tx.
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.
MaxInFlight was a hardcoded constant (32); different indexing servers
tolerate different levels of request concurrency before throttling, so
expose it as an optional ConnectAsync parameter instead of requiring a
recompile to tune initial-sync throughput against a given server.
_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).
Translate every key in Loc.Strings and register "zh" in Loc.Languages
/LanguageNames. Wire it into the Settings language picker, which is a
hand-written RadioButton list (not generated from Loc.Languages), so
add IsLangZh to MainWindowViewModel and the matching button in
MainView.axaml. Update CLAUDE.md/AGENTS.md language count and note the
non-dynamic picker to avoid the same gap next time.
Sending a large amount from a wallet with many small UTXOs could
produce a transaction over the standard relay size limit, surfaced
only as a cryptic "Invalid transaction: Transaction's size is too
high" from builder.Verify() after everything else had already
succeeded — with no way for the user to recover other than manually
picking fewer coins.
Build() now orders spendable UTXOs largest-first and binary-searches
the smallest prefix of them that produces a valid transaction, so it
naturally prefers big coins over dust and avoids the limit whenever a
smaller input set can cover the amount. NBitcoin's own coin selector
already refuses to assemble a combination over the size cap and
reports it via NotEnoughFundsException with a distinctive message
rather than ever handing back an oversized transaction, so that case
is now recognized and translated into a clear, actionable error
instead of being read as "insufficient funds".
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.
Fills in the CHANGELOG.md entry for the checkpoint-anchoring security fix,
fuzzing infrastructure and its findings, OP_RETURN/coinbase-tag decoding,
and localization fixes since 0.9.1 (see previous commits), and bumps
<Version>/versionCode across the App and Android head csproj files ahead
of the tag.
Transaction detail view silently dropped OP_RETURN messages and coinbase
scriptSig text (e.g. pool tags), since TransactionInspector discarded
scriptPubKey/scriptSig bytes once no destination address could be derived.
Each output/input is decoded independently, so multiple OP_RETURN outputs
in one tx are all shown.
Adds a "User guide" button next to "Report a bug" in the Help > Info
tab, linking to USERGUIDE.md on GitHub via the same Launcher pattern
used for bug reports and release pages.
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).
server.peers.subscribe is attacker-controlled data: ParsePeers assumed
the standard [ip, hostname, [features...]] shape and threw on anything
else (non-array response, wrong element types), and GetString() on a
JSON string containing invalid UTF-8 bytes threw InvalidOperationException
instead of failing to parse — both found by fuzzing. Any unexpected
shape or unparseable string now degrades to "no usable data" instead of
propagating an exception into the sync/discovery path.
Wordlist.AutoDetect lands on NBitcoin's internal "Unknown" language for
text resembling no wordlist and throws NotSupportedException instead
of failing gracefully — found by fuzzing the restore-mnemonic input.
TryParse now treats that (and the same failure mode from the Mnemonic
constructor) as "not a valid mnemonic", consistent with every other
rejection path.
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.
CheckAsync built its own HttpClient inline, so nothing beyond tag
parsing was testable without hitting the real GitHub API. Added an
internal CheckAsync(currentVersion, HttpMessageHandler, ct) overload
(the public method now delegates to it) as a seam for a stub handler,
and tests for the full best-effort matrix: newer/equal/older/unparseable
tag, missing html_url, HTTP error status, malformed JSON, a thrown
HttpRequestException, and an unparseable local version — all of which
must resolve to null or the correct LatestRelease, never throw.
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).
The CLI printed all wallet/sync/send output, error messages and usage
text in Italian by design; per the project's language policy (code,
comments and docs are English-only, Italian is reserved for user
conversation) this was a design mismatch, not a documented feature.
Translated every user-facing string and the file's Italian comments,
and updated USERGUIDE.md §17 to match.
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.
Fills in the CHANGELOG.md entry for the test-suite expansion and bug fixes
since 0.9.0 (see previous commits), and bumps <Version>/versionCode across
the App and Android head csproj files ahead of the tag.
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.
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.
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.
Add a feature request template and a config.yml (blank issues disabled,
security reports routed to SECURITY.md) alongside the existing bug report
template, plus a PR template with a checklist matching the project's
security rules. Point the app's "Report a bug" button at the new GitHub
template now that the repo is hosted there instead of the old self-hosted
tracker.
Previously the server settings overlay had only "Connect and sync" and
"Reset certs" — peer discovery had no button and only ran implicitly
when reopening the dialog while connected. Add a dedicated "Sincronizza"
button wired to DiscoverServersCommand, and make it work even without
an active connection by opening a short-lived connection to a candidate
server just to query its peer list, then closing it without touching
the wallet's connection state.
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".
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)
When IsSyncing and the user requests a different server, cancel the
running CancellationTokenSource instead of silently ignoring the click.
OperationCanceledException is caught separately (not an error), and
ConnectAndSync restarts automatically with the new server after the
cancelled task unwinds.
The "Connect" button no longer has IsEnabled="{Binding !IsSyncing}":
it is always clickable, and clicking it with the same server sets
_resyncRequested as before. CancellationToken is now passed to both
ElectrumClient.ConnectAsync and WalletSynchronizer.SyncOnceAsync.
All six languages updated: the one-liner was replaced with a two-sentence
description that names the network (PLM), the lightweight SPV design,
the non-custodial model, and the local encryption guarantee.
Adds a "Report a bug" / "Segnala un bug" button (6 languages) in
Help → Info that opens the issue tracker in the system browser via
TopLevel.Launcher (cross-platform: desktop + Android).
URL points to the current self-hosted repo; a TODO comment marks
the constant for easy update once the project moves to GitHub.
Adds .github/ISSUE_TEMPLATE/bug_report.yml — GitHub issue form with
structured fields for version, platform, network, repro steps,
expected behavior, and logs.
Input rows switch to a two-line layout (full prev-txid:index on the first
line, address + amount on the second) so nothing is cut by a fixed 160px
column. Output address column gets TextWrapping instead of TextTrimming.
Overlay grows to MaxWidth=660/MaxHeight=800 for better desktop use; the
same XAML wraps naturally on narrow mobile screens.
Removes the Shorten() helper that was masking input references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SPV is the only verification method the wallet uses, so showing it as a
badge adds no information — if a transaction appears confirmed, it has
already passed Merkle verification.
- Remove the SPV chip from the transaction detail overlay
- Remove the Verificata column from the history list (desktop and mobile)
- Drop VerifiedText property from TransactionDetailsViewModel
- Drop the Verificata field from HistoryRow
- Remove the orphaned tx.verified loc key
The wizard already covers opening existing wallets; a separate file-picker
entry in the menu duplicates that flow without adding value. Remove the menu
item, its click handler, the OpenFromPath VM method, and the loc key.
- File → Esci (separator + Quit at the bottom) calls IClassicDesktopStyleApplicationLifetime.Shutdown()
- Wallet menu item and mobile button now use IsVisible instead of IsEnabled:
they disappear entirely on the wizard/setup screen and reappear once a
wallet is open, avoiding a confusing greyed-out entry
Replace the direct private-key display in the address detail overlay with a
two-step reveal flow:
- Unencrypted wallet: clicking Show reveals the WIF key immediately.
- Encrypted wallet: clicking Show opens a modal password-prompt overlay; the
key is displayed only if the entered password matches the wallet file
password; a wrong password shows an inline error without closing the prompt.
Hiding the key (Hide button) clears the revealed state. Closing the address
overlay or opening a different one always resets both the reveal state and
the password prompt, so no sensitive material lingers between sessions.
Add a Wallet menu item (between File and Settings) that opens an in-app
overlay showing wallet metadata: file name, network, type (HD seed / xprv /
WIF / watch-only), script kind, derivation path, master fingerprint, and
account xpub.
Includes a seed section: if the wallet has no seed, a note is shown; if it
has one and is unencrypted the seed is revealed directly; if encrypted the
user must enter the wallet file password before the mnemonic is displayed.
The revealed seed is shown in a danger-coloured bordered box with a warning.
Closing the overlay (backdrop, Close button, or Esc) always clears the
password input and hides the seed.
Also adds the private-key password-prompt overlay and its localization keys
(shared UI scaffolding used by the following commit).
- 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
- Removed TLS checkbox (SSL still used internally, just not exposed in UI)
- Removed "Discover servers" button (discovery now runs automatically on panel open)
- ConnectionStatus now includes host:port when connected ("Connesso a ip:porta")
- Replaced status ellipse+text with a single TextBlock
Help button now shows a two-tab overlay:
- Info: existing about/version content
- Donate: donate flow to the dev address (plm1qdq3…) with free amount
input, prepare preview, and confirm-and-broadcast button
New MainWindowViewModel.Donate.cs partial: DonateAmount / DonatePreview
/ HasPendingDonate observable properties, PrepareDonateCommand (builds
the tx with hardcoded dev address via the existing TransactionFactory),
ConfirmDonateCommand (broadcasts and syncs). Donate state is reset on
overlay close. Six-language Loc keys added (help.tab.info/donate,
donate.desc/dev.address/amount/prepare/confirm).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UniformGrid Columns="5" on TabStrip so all 5 tabs always stay on one
row on Android (Rows="1" alone did not auto-calculate columns at
measure time on the Android Avalonia renderer)
- Contacts tab header was using hardcoded FontSize=13/Width=24/Spacing=4
instead of the TabFontSize/TabIconSize/TabSpacing ViewModel bindings,
making it visually larger than the other four tabs on mobile
- ContentPresenter Margin="0,0,0,4" in Controls.axaml pushes the tab
header content above the 2 px selection indicator, which is drawn at
the absolute bottom of the template Panel and was overlapping the label
- TabItem horizontal padding 4→8 px and TabSpacing 2→4 for both
desktop and mobile to give more breathing room between tab items
Send tab: Recipient and Amount cards side-by-side on desktop using a
two-column Grid, making full use of the available window width.
Summary card and action buttons remain full-width below both columns.
Mobile retains the original vertical stack.
Receive tab: QR card on the left (auto-width), address card expands
to fill the right column on desktop — standard fintech wallet pattern.
Mobile retains QR above, address below.
Window enlarged to 1020×680 (from 900×620) and fixed with CanResize=False;
Min/Max dimensions match so the resize handle never appears.