6 Commits

Author SHA1 Message Date
davide f0fb3fe05d docs: link CHANGELOG.md and require updating it on new version tags
CLAUDE.md: add a working rule to update CHANGELOG.md whenever <Version> is
bumped for a new release tag. README.md: add a Changelog section pointing
to it, and fix a stale claim that release signing "is not set up yet" for
Android — it now is, via docker/build.sh android and the persistent
keystore (docker/keystore/).
2026-07-02 22:01:35 +02:00
davide 3f26f52f8b docs: add CHANGELOG.md for 0.9.0
Technical changelog covering the full history from the initial commit,
grouped by subsystem (Core/Chain, Crypto, Net, Spv, Storage, Wallet, App UI,
Android, CLI, Build, Testing, Docs) rather than raw commit order, since this
is the first release.
2026-07-02 22:01:17 +02:00
davide cb3ff714d9 feat(android): sign release builds with the persistent keystore
build_android now requires the keystore generated by the previous commit,
prompts for its passwords at build time, and signs the APK with it instead
of an ephemeral debug key auto-generated per container run — that was the
actual cause of every release needing a manual uninstall to update. Also
derives versionCode from <Version> instead of leaving it fixed at 1, so
version ordering stays monotonic across releases.

Docs (docker/README.md, CLAUDE.md) updated to match the new signing flow.
2026-07-02 21:54:23 +02:00
davide d0037747b5 chore(android): add persistent release-signing keystore generator
An Android APK's signature identifies the app to the OS; a new build must
carry the same one as the last install or Android refuses it as an update.
generate-keystore.sh creates the keystore once, interactively, and refuses
to run again once one exists to avoid ever changing it by accident. The
resulting file is git-ignored — it's a secret that must live outside the
repo, backed up separately.
2026-07-02 21:53:56 +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 2d017231eb chore(github): complete issue/PR templates and wire in-app bug report to them
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.
2026-07-02 21:22:42 +02:00
18 changed files with 572 additions and 28 deletions
-11
View File
@@ -28,17 +28,6 @@ body:
validations:
required: true
- type: dropdown
id: network
attributes:
label: Network
options:
- Mainnet
- Testnet
- Regtest
validations:
required: true
- type: textarea
id: description
attributes:
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
url: https://github.com/davide3011/PalladiumWallet/security/policy
about: Please report security vulnerabilities privately, not as a public issue. See SECURITY.md.
@@ -0,0 +1,55 @@
name: Feature Request
description: Suggest an idea or improvement for Palladium Wallet
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest an improvement.
- type: textarea
id: problem
attributes:
label: Problem
description: What problem are you trying to solve? Is it related to something you find missing or frustrating?
placeholder: "I'm always frustrated when …"
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: Describe what you'd like to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Any alternative solutions or workarounds you've considered.
validations:
required: false
- type: dropdown
id: platform
attributes:
label: Affected platform(s)
multiple: true
options:
- Windows
- Linux
- Android
- CLI
- All
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Before submitting
options:
- label: I searched existing issues and this is not a duplicate
required: true
+26
View File
@@ -0,0 +1,26 @@
## Summary
<!-- What does this PR change and why? -->
## Related issue
<!-- Closes #... (if applicable) -->
## Type of change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor / cleanup
- [ ] Documentation
- [ ] Build / CI
## Testing
<!-- How was this verified? e.g. `dotnet test`, manual run on Windows/Linux/Android, golden-vector comparison -->
## Checklist
- [ ] `dotnet test` passes
- [ ] No seed/private key material logged or persisted in plaintext (if touching Crypto/Wallet/Storage)
- [ ] Server responses still validated with Merkle + checkpoints (if touching Spv/Net)
- [ ] Code/comments/commit messages are in English
+2
View File
@@ -74,6 +74,8 @@ temp/
*.key
*.pfx
*.p12
*.keystore
*.jks
secrets/
# Wallet data and local runtime state
+156
View File
@@ -0,0 +1,156 @@
# Changelog
Technical changelog for PalladiumWallet. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); entries are grouped
by subsystem rather than strictly by date, since `0.9.0` is the first
release and covers the full history from the initial commit.
## [0.9.0] — 2026-07-02
First release. SPV wallet (Sparrow-style) for the Palladium (PLM) network —
Bitcoin-derived UTXO chain — targeting desktop (Windows/Linux) and Android
from a single Avalonia UI codebase on .NET 10 + NBitcoin.
### Core — Chain
- Network profiles and consensus constants centralized in `Core/Chain`
(`ChainProfiles`/`PalladiumNetworks`): address prefixes, BIP32 headers,
bech32 HRP, genesis, ports, `coin_type` 746 — selectable per network
(mainnet/testnet/regtest), no scattered magic numbers.
- LWMA difficulty / 2-minute blocks: SPV cannot recompute LWMA retargeting,
so PoW validation is skipped and trust is anchored to hardcoded
checkpoints instead.
### Core — Crypto
- HD key derivation: BIP32/39/84, HD accounts, SLIP-132 extended key
serialization.
- Address types: P2PKH, P2SH-P2WPKH, and P2TR (Taproot/BIP86).
- `IWalletAccount` abstraction with WIF/xpub/xprv keystore import (including
watch-only accounts from public material only).
### Core — Net
- Custom `ElectrumClient`: JSON-RPC 2.0 client for the ElectrumX-like
indexing server, with TLS support and TOFU certificate pinning.
- `ServerRegistry`: bootstrap server list, peer discovery, persisted last-used
server, fallback resolution for `--server`.
- Batched writes, zero-allocation reads, bounded in-flight requests for
network throughput.
- Fix: allow changing the indexing server at any time, including during an
active sync.
### Core — Spv
- Header sync with Merkle proof verification and scripthash subscriptions
against hardcoded checkpoints (no full PoW recomputation, per the chain
profile above).
- Per-address balance and transaction-count aggregation in sync results.
- Electrum-style continuous updates: incremental, parallelized sync across
address chains, with a persistent header cache.
- Fix: resilient sync — history discovery via `GetHistory`, transaction
caching, automatic reconnect fallback.
### Core — Storage
- Versioned, encrypted JSON wallet file (`WalletDocument`) with dedicated
persistence and loader layer.
- `WalletLock`: prevents concurrent access to the same wallet file; acquired
before load and before close (fixed a race where it wasn't).
- XDG-compliant data paths (`AppPaths`) with per-platform override seam used
by the Android head; English as default UI language.
- Contacts list (name + address) persisted in `WalletDocument`.
- Documented caveat: `WalletStore.Save` writes plaintext JSON when the
wallet is unencrypted — seed/keys are only ever encrypted-at-rest when the
user opts into a password.
### Core — Wallet
- Coin selection, PSBT-centric transaction factory, and wallet loader.
- Confirmation-threshold enforcement before spending UTXOs; immature
(coinbase) balance surfaced separately from confirmed balance; pending
mempool balance shown, with unconfirmed UTXOs excluded from spending by
default.
- Fix: reject amounts with sub-satoshi precision.
### App — Avalonia UI (shared, `src/App`)
- Single shared `MainView` (UserControl) hosted by `MainWindow` on desktop
and as the single-view root on Android; single `MainWindowViewModel`
(CommunityToolkit.Mvvm), later split into partial files by area
(Wizard/Settings/Sync/Send/Contacts/Receive/...).
- Step-by-step setup wizard (replacing a single-form panel), including a
first-run data-location step, multi-wallet chooser, confirm-password and
encrypt toggle, and dedicated flows for creating a wallet vs. importing
from xpub/xprv/WIF.
- In-app overlay pattern for details/settings/help (`IsXxxOpen` flags, no OS
windows) replacing earlier nested submenus and a separate
`AddressInfoWindow`: server settings, app settings (language/unit),
wallet info (xpub, password-gated seed reveal), address detail
(password-gated private key reveal), transaction detail with full
on-chain data (inputs/outputs, no truncation), and a Help overlay
(Info/Donate tabs).
- Connection-status indicator in the bottom bar; connect-before-wallet-open
flow; persisted last-used server; server discovery split into its own
always-usable button; mainnet hardcoded (network selector removed for the
first release).
- Localization: `Loc` key → 6-language dictionary (it/en/es/fr/pt/de) with
live language switching.
- Receive: QR code generation and copy-to-clipboard for the receive address.
- Android: QR code scanner for the Send address field.
- Centralized design system (color tokens, gradient hero, SVG tab icons);
responsive layout for portrait mobile, unified tab bar (desktop +
mobile), two-column desktop layout for Send/Receive; various mobile-only
fixes (tab bar sizing/indicator overlap, text overflow, full-screen server
overlay on mobile).
- App version shown in window title and Help overlay, read from the single
`<Version>` source in the App csproj.
- In-app update check: compares the running version against the latest
GitHub release tag on startup and shows an overlay with the new tag when
one is available (best-effort — silent on network/parse failure).
- In-app bug-report button (Help overlay) opening a pre-filled GitHub issue
template; issue/PR templates completed.
### Android head (`src/App.Android`)
- Architecture split: shared UI library (`src/App`) + `src/App.Desktop` +
`src/App.Android` heads from the same source (`refactor(arch)`), each
carrying only the per-platform entry point and packages.
- App logo as launcher icon.
- Persistent release-signing keystore workflow
(`docker/keystore/generate-keystore.sh`, git-ignored output): every
release APK is signed with the same key so installing a newer build
updates a previous install in place instead of requiring an uninstall.
`versionCode` derived from `<Version>` instead of a fixed constant.
### CLI (`src/Cli`)
- Commands: `create`/`restore`/`restore-xpub`/`info`,
`sync`/`send`/`servers`/`reset-certs`, `newseed`/`addresses`.
- `servers` command, `info --addresses`, and registry-based fallback
resolution for `--server`.
### Build & Distribution
- Docker-based reproducible build system (`docker/build.sh` +
`docker/Dockerfile.*`): pinned toolchain (.NET 10 SDK, JDK, Android SDK),
builds Windows/Linux single-file executables and a signed Android APK
without any SDK installed on the host.
- Android release signing wired into `build_android` (see Android head
above): requires the persistent keystore, prompts for its passwords at
build time, mounts it read-only into the build container.
### Testing
- Unit test coverage across all Core modules, later expanded to 209 tests.
- Property-based tests via CsCheck (`PropertyTests.cs`), bringing total
coverage to 218 tests; dedicated `WalletLock` concurrency tests.
### Documentation
- `README.md` (project overview, quickstart, reproducible builds), `CLAUDE.md`
(codebase guidance for AI tooling, kept in sync with the multi-head
architecture and .NET 10 migration), `SECURITY.md` (threat model and SPV
trust assumptions), coding-agent guide.
- Code comments translated to English project-wide, per the language policy
(Italian conversation, English code/docs).
+2 -1
View File
@@ -47,7 +47,7 @@ by `MainWindow` on desktop and as the single-view root on Android.
- Tests (headless, the primary verification layer): `dotnet test` — single: `dotnet test --filter "FullyQualifiedName~TestName"`; property-based tests (CsCheck, `PropertyTests.cs`) run in the same command and take ~30 s
- GUI hot reload: `dotnet watch --project src/App.Desktop` (on WSL2/WSLg the window shows on the Windows desktop, no graphics dependencies to install)
- CLI: `dotnet run --project src/Cli -- <command>` (no args → usage)
- **Release binaries (all 3 targets): `./docker/build.sh [windows|linux|android|all]`** — reproducible builds in Docker (toolchain pinned in `docker/Dockerfile.*`, no SDK needed on host), artifacts in `dist/`, version taken from the App csproj. See `docker/README.md`. Gotchas already encoded there: single-file desktop publishes need `-p:IncludeNativeLibrariesForSelfExtract=true` (without it Avalonia's native libs — Skia/HarfBuzz/ANGLE — stay outside the exe, which then silently fails to start); the android workload dictates the SDK API level (error XA5207 → bump `ANDROID_SDK_PLATFORM` in `Dockerfile.android`).
- **Release binaries (all 3 targets): `./docker/build.sh [windows|linux|android|all]`** — reproducible builds in Docker (toolchain pinned in `docker/Dockerfile.*`, no SDK needed on host), artifacts in `dist/`, version taken from the App csproj. See `docker/README.md`. Gotchas already encoded there: single-file desktop publishes need `-p:IncludeNativeLibrariesForSelfExtract=true` (without it Avalonia's native libs — Skia/HarfBuzz/ANGLE — stay outside the exe, which then silently fails to start); the android workload dictates the SDK API level (error XA5207 → bump `ANDROID_SDK_PLATFORM` in `Dockerfile.android`). Android release builds need a persistent signing keystore, generated once with `docker/keystore/generate-keystore.sh` (never committed — see `docker/keystore/README.md`): without it every build gets a different random signature and users must uninstall the old app to receive an update instead of updating in place.
- Manual Windows publish: `dotnet publish src/App.Desktop -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true --self-contained`
- Manual Linux publish: same with `-r linux-x64` (single-file binary; AppImage via PupNet Deploy is a future step, no pupnet.conf yet)
@@ -84,3 +84,4 @@ Note: a plain `dotnet build` at the solution level needs the Android SDK path fo
- **Cross-implementation tests:** compare addresses, txids, and PSBTs against a reference wallet (golden vectors). A different address or txid is a blocking bug.
- **Security:** seed and private keys never in plaintext on disk/logs/network; every server response validated with Merkle + checkpoints; watch-only truly read-only.
- **Releases:** whenever a new version tag is created (bumping `<Version>` in `src/App/PalladiumWallet.App.csproj`), update `CHANGELOG.md` with an entry for that version before/with the tag — it's the technical record of what shipped, not optional bookkeeping.
+9 -2
View File
@@ -218,8 +218,11 @@ The ABI restriction uses the `AbiArm64Only` flag, which is scoped to the Android
command line, it leaks to the `net10.0` projects (`Core`/`App`) and breaks the build. (The legacy
`AndroidSupportedAbis` property is deprecated and ignored.)
(Set `ANDROID_HOME` to skip the `-p:AndroidSdkDirectory` flag.) Release signing with your own
keystore is not set up yet; the debug apk is fine for personal sideloading.
(Set `ANDROID_HOME` to skip the `-p:AndroidSdkDirectory` flag.) This debug apk is fine for personal
sideloading, but debug builds are signed with a key regenerated per build machine/container, so a
newer debug apk won't install over an older one without an uninstall first. For a stable signature
across releases (needed to update an existing install in place), build via
`./docker/build.sh android` instead — see [docker/keystore/README.md](docker/keystore/README.md).
> **Verification status.** The default multi-ABI apk is verified running on the x86_64 emulator
> (UI renders, connects to a server over TLS). The arm64-only apk builds correctly (41 MB,
@@ -332,6 +335,10 @@ The default wallet file is `~/.palladium-wallet/<network>/wallets/default.wallet
---
## Changelog
Technical, per-version changes are tracked in [CHANGELOG.md](CHANGELOG.md).
## License
Released under the MIT License. See the [LICENSE](LICENSE) file.
+25 -9
View File
@@ -46,6 +46,16 @@ automatically on first use — a few minutes for desktop, 1020 minutes for
Android (large downloads). Subsequent builds reuse the cached images and take
well under a minute (desktop) / a few minutes (Android).
**Android release signing:** before the first `android` build, generate the
persistent signing keystore once — see [`keystore/README.md`](keystore/README.md):
```bash
./docker/keystore/generate-keystore.sh
```
Without it, `build_android` refuses to run — every APK must be signed with
the same key so future releases can update a previous install in place.
---
## Quick start
@@ -64,7 +74,7 @@ Running without arguments shows an interactive menu — pick a single target or
Targets:
windows Win x64 single-file executable (native libs embedded)
linux Linux x64 single-file binary (runs as-is, nothing to install)
android Android APK (debug-signed)
android Android APK (release-signed, prompts for keystore passwords)
all All three targets
Options:
@@ -109,16 +119,16 @@ effectively all of them); no .NET or other packages to install. If you
transfer it through a channel that strips permissions (e.g. a web download),
restore the execute bit with `chmod +x`.
**Android** — a debug-signed APK for sideloading: transfer it to the phone
**Android** — a release-signed APK for sideloading: transfer it to the phone
and open it (enable "install from unknown sources" if prompted), or install
via `adb install dist/android/PalladiumWallet-*.apk`. Supports Android 6.0+
(API 23), arm64 phones and x86_64 emulators.
> **Signature caveat:** debug-signed APKs built on different machines carry
> different keys. If a previous build is already installed, Android refuses
> the update (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`) — uninstall the old app
> first. **Uninstalling deletes the app's data: back up the wallet seed
> before doing this.** Release signing with a stable key is not set up yet.
> **Signature:** every APK is signed with the persistent keystore in
> `docker/keystore/` (see [Prerequisites](#prerequisites)), so installing a
> newer build over an existing one updates it in place — no uninstall, no
> data loss. This only holds as long as every release keeps using that same
> keystore file; see `docker/keystore/README.md` for the backup story.
---
@@ -166,8 +176,14 @@ docker volume rm plm-nuget-cache
android workload moved to a newer API level. Bump
`ANDROID_SDK_PLATFORM` (and `ANDROID_SDK_BUILD_TOOLS`) in
`Dockerfile.android` to the level the error names, then `--rebuild`.
- **APK won't install over an existing app** — signature mismatch between
debug keys; see the signature caveat above.
- **APK won't install over an existing app
(`INSTALL_FAILED_UPDATE_INCOMPATIBLE`)** — the new build wasn't signed with
the same keystore as the installed one. Make sure `docker/keystore/release.keystore`
hasn't changed since the installed build; if it's genuinely a different key,
the user must uninstall the old app first (this deletes app data — back up
the wallet seed before doing this).
- **`build_android` refuses to run, asks to generate a keystore** — run
`./docker/keystore/generate-keystore.sh` once (see `docker/keystore/README.md`).
- **Everything is broken / start from scratch** —
`docker system prune -a && docker volume rm plm-nuget-cache`, then rerun
the script (images and packages are re-downloaded).
+36 -2
View File
@@ -29,7 +29,7 @@ $(bold "Usage:") $(basename "$0") [TARGET] [OPTIONS]
$(bold "Targets:")
windows Win x64 single-file executable → dist/windows/
linux Linux x64 single-file binary → dist/linux/
android Android APK (debug-signed) → dist/android/
android Android APK (release-signed) → dist/android/
all All three targets
$(bold "Options:")
@@ -112,6 +112,10 @@ ensure_android_image() {
# $1 = image name, $2 = inline bash commands to execute inside the container.
# Source is copied to /tmp/build inside the container so bin/obj never pollute
# the repo. Output is written to /output (→ host DIST_DIR sub-folder).
# Optional extra `docker run` args (e.g. keystore mount, signing passwords via
# -e) can be set by the caller in the EXTRA_DOCKER_ARGS array beforehand.
EXTRA_DOCKER_ARGS=()
run_build() {
local image="$1"
local commands="$2"
@@ -123,6 +127,7 @@ run_build() {
--volume "${PROJECT_ROOT}:/src:ro" \
--volume "${out_dir}:/output" \
--volume "${NUGET_VOLUME}:/root/.nuget/packages" \
"${EXTRA_DOCKER_ARGS[@]}" \
"$image" \
bash -euo pipefail -c "
cp -r /src/. /tmp/build
@@ -169,16 +174,45 @@ build_linux() {
build_android() {
ensure_android_image
local keystore="${SCRIPT_DIR}/keystore/release.keystore"
[[ -f "$keystore" ]] || die "No release keystore found at docker/keystore/release.keystore. Run ./docker/keystore/generate-keystore.sh once first (see docker/keystore/README.md)."
info "Building Android APK …"
read -r -s -p "Keystore password: " ANDROID_KS_PASS
echo
read -r -s -p "Key password (press Enter to reuse the keystore password): " ANDROID_KEY_PASS
echo
ANDROID_KEY_PASS="${ANDROID_KEY_PASS:-$ANDROID_KS_PASS}"
# versionCode derived from <Version> (MAJOR.MINOR.PATCH, pre-release suffix
# stripped) so it always increases with releases — required for some
# installers to accept an in-place update even when the signature matches.
local semver="${VERSION%%-*}"
IFS='.' read -r VMAJOR VMINOR VPATCH <<< "$semver"
local VERSION_CODE=$(( ${VMAJOR:-0} * 10000 + ${VMINOR:-0} * 100 + ${VPATCH:-0} ))
info "versionCode: ${VERSION_CODE}"
EXTRA_DOCKER_ARGS=(
--volume "${keystore}:/keystore/release.keystore:ro"
--env "ANDROID_KS_PASS=${ANDROID_KS_PASS}"
--env "ANDROID_KEY_PASS=${ANDROID_KEY_PASS}"
)
run_build "$IMAGE_ANDROID" \
"JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
dotnet build src/App.Android \
-c Release \
-t:SignAndroidPackage \
-p:AndroidSdkDirectory=\${ANDROID_HOME}
-p:AndroidSdkDirectory=\${ANDROID_HOME} \
-p:ApplicationVersion=${VERSION_CODE} \
-p:AndroidSigningKeyStore=/keystore/release.keystore \
-p:AndroidSigningKeyAlias=palladiumwallet \
-p:AndroidSigningStorePass=\${ANDROID_KS_PASS} \
-p:AndroidSigningKeyPass=\${ANDROID_KEY_PASS}
cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \
\"/output/PalladiumWallet-${VERSION}.apk\"" \
"${DIST_DIR}/android"
EXTRA_DOCKER_ARGS=()
ok "Android → dist/android/PalladiumWallet-${VERSION}.apk"
}
+37
View File
@@ -0,0 +1,37 @@
# Android release-signing keystore
This folder holds the persistent keystore used to sign release APKs built by
`docker/build.sh android`. Every release must be signed with the **same**
key, or Android refuses to install a new build over a previously installed
one (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`) and forces the user to uninstall
first — which deletes the app's data.
## First-time setup
```bash
./docker/keystore/generate-keystore.sh
```
Run this **once**, on whichever machine will keep producing release builds.
It creates `release.keystore` in this folder and asks for a keystore
password (and, optionally, a separate key password). Nothing is written to
git — `release.keystore` is ignored by `.gitignore`, and the script never
saves the passwords anywhere.
`docker/build.sh android` will refuse to run without this file, and will
prompt you for the same passwords at every build.
## Back it up
The keystore file and its passwords cannot be regenerated: if lost, no
future release can ever update existing installs in place again — every
user would need to uninstall and reinstall from scratch. Copy
`release.keystore` and the passwords to a password manager or encrypted
backup outside this repository as soon as you generate them.
## What NOT to do
- Do not commit `release.keystore` (or any `*.jks`/`*.keystore` file) to git.
- Do not re-run `generate-keystore.sh` once a keystore already exists — it
refuses on purpose; delete the file yourself only if you fully accept the
consequences above.
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Generates the persistent Android release-signing keystore used by
# docker/build.sh. Every APK built with this keystore carries the same
# signature, which is what lets Android treat a new release as an update to
# a previously installed one instead of a conflicting, different app.
#
# Run this ONCE. Re-running it after the keystore already exists is refused
# below on purpose: regenerating it changes the signature, and every device
# with a prior release installed would then need a full uninstall (and lose
# any app data that isn't in the wallet file itself) to receive the next
# update.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KEYSTORE_PATH="${SCRIPT_DIR}/release.keystore"
ALIAS="palladiumwallet"
if [[ -f "$KEYSTORE_PATH" ]]; then
echo "A keystore already exists at $KEYSTORE_PATH — refusing to overwrite it." >&2
echo "Delete it manually first only if you understand the consequences above." >&2
exit 1
fi
command -v keytool &>/dev/null || {
echo "keytool not found on PATH — install a JDK (e.g. openjdk-17-jdk) and retry." >&2
exit 1
}
echo "Creating the Palladium Wallet Android release-signing keystore."
echo "This file and the passwords you enter now must be kept safe OUTSIDE this"
echo "repository (password manager, encrypted backup, ...). They are never"
echo "committed to git. Losing them means no future release can ever update"
echo "existing installs in place — only a fresh keystore + full user reinstall."
echo
read -r -s -p "Keystore password (min 6 characters): " KS_PASS
echo
read -r -s -p "Confirm keystore password: " KS_PASS_CONFIRM
echo
if [[ "$KS_PASS" != "$KS_PASS_CONFIRM" ]]; then
echo "Passwords do not match." >&2
exit 1
fi
if [[ "${#KS_PASS}" -lt 6 ]]; then
echo "Password too short (keytool requires at least 6 characters)." >&2
exit 1
fi
read -r -s -p "Key password (press Enter to reuse the keystore password): " KEY_PASS
echo
KEY_PASS="${KEY_PASS:-$KS_PASS}"
export KS_PASS KEY_PASS
keytool -genkeypair -v \
-keystore "$KEYSTORE_PATH" \
-alias "$ALIAS" \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass:env KS_PASS -keypass:env KEY_PASS \
-dname "CN=PalladiumWallet, OU=PalladiumWallet, O=PalladiumWallet, C=IT"
unset KS_PASS KEY_PASS
echo
echo "Keystore created at $KEYSTORE_PATH (git-ignored, alias: $ALIAS)."
echo "Back it up now, together with the passwords — it cannot be regenerated."
echo "./docker/build.sh android will prompt for these same passwords at build time."
+4
View File
@@ -63,6 +63,10 @@ public sealed class Loc
["help.tab.info"] = ["Info", "Info", "Info", "Info", "Info", "Info"],
["help.tab.donate"] = ["Dona", "Donate", "Donar", "Faire un don", "Doar", "Spenden"],
["help.bug.report"] = ["Segnala un bug", "Report a bug", "Informar un error", "Signaler un bug", "Reportar um bug", "Fehler melden"],
["update.title"] = ["Aggiornamento disponibile", "Update available", "Actualización disponible", "Mise à jour disponible", "Atualização disponível", "Update verfügbar"],
["update.message"] = ["È disponibile una nuova versione:", "A new version is available:", "Hay una nueva versión disponible:", "Une nouvelle version est disponible :", "Uma nova versão está disponível:", "Eine neue Version ist verfügbar:"],
["update.download"] = ["Scarica", "Download", "Descargar", "Télécharger", "Baixar", "Herunterladen"],
["update.dismiss"] = ["Ignora", "Dismiss", "Ignorar", "Ignorer", "Ignorar", "Verwerfen"],
["donate.desc"] = [
"Se questo wallet ti è utile, considera una piccola donazione allo sviluppatore.",
"If you find this wallet useful, consider a small donation to the developer.",
@@ -0,0 +1,36 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PalladiumWallet.Core.Net;
namespace PalladiumWallet.App.ViewModels;
public partial class MainWindowViewModel
{
// ---- update-available overlay ----
[ObservableProperty]
private bool isUpdateAvailableOpen;
[ObservableProperty]
private string updateAvailableTag = "";
public string UpdateReleaseUrl { get; private set; } = "";
[RelayCommand]
private void CloseUpdateAvailable() => IsUpdateAvailableOpen = false;
/// <summary>
/// Fire-and-forget check run once at startup. Best-effort: any failure or an
/// up-to-date app silently does nothing (see <see cref="UpdateChecker"/>).
/// </summary>
private async Task CheckForUpdatesAsync()
{
var latest = await UpdateChecker.CheckAsync(AppVersion);
if (latest is null) return;
UpdateReleaseUrl = latest.HtmlUrl;
UpdateAvailableTag = latest.Tag;
IsUpdateAvailableOpen = true;
}
}
@@ -158,6 +158,7 @@ public partial class MainWindowViewModel : ViewModelBase
_keepAliveTimer = new DispatcherTimer { Interval = System.TimeSpan.FromSeconds(20) };
_keepAliveTimer.Tick += async (_, _) => await KeepAliveTickAsync();
_keepAliveTimer.Start();
_ = CheckForUpdatesAsync();
}
private async System.Threading.Tasks.Task KeepAliveTickAsync()
+29
View File
@@ -1638,5 +1638,34 @@
</StackPanel>
</Border>
</Border>
<!-- ============ UPDATE AVAILABLE OVERLAY ============ -->
<!-- Same pattern as the help overlay: instant open/close. -->
<Border Grid.Row="0" Grid.RowSpan="3"
Background="{DynamicResource ScrimBrush}"
Tapped="OnUpdateAvailableOverlayBackdropTapped"
IsVisible="{Binding IsUpdateAvailableOpen}">
<Border Background="{DynamicResource OverlayCardBrush}"
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="8"
MaxWidth="440" Margin="16"
HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Margin="24" Spacing="16">
<TextBlock Text="{Binding Loc[update.title]}"
FontSize="18" FontWeight="Bold"/>
<TextBlock TextWrapping="Wrap">
<Run Text="{Binding Loc[update.message]}"/>
<Run Text=" "/>
<Run Text="{Binding UpdateAvailableTag}" FontWeight="Bold"/>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Content="{Binding Loc[update.download]}"
Classes="accent"
Click="OnOpenReleasePageClick"/>
<Button Content="{Binding Loc[update.dismiss]}"
Command="{Binding CloseUpdateAvailableCommand}"/>
</StackPanel>
</StackPanel>
</Border>
</Border>
</Grid>
</UserControl>
+18 -3
View File
@@ -129,16 +129,30 @@ public partial class MainView : UserControl
vm.IsHelpOpen = false;
}
private void OnUpdateAvailableOverlayBackdropTapped(object? sender, TappedEventArgs e)
{
if (!ReferenceEquals(e.Source, sender)) return;
if (DataContext is MainWindowViewModel vm)
vm.IsUpdateAvailableOpen = false;
}
private async void OnOpenBugReportClick(object? sender, RoutedEventArgs e)
{
// TODO: replace with the final GitHub URL once the repo is on GitHub
// e.g. https://github.com/davide3011/PalladiumWallet/issues/new?assignees=&labels=bug&template=bug_report.yml
const string issueUrl = "https://santantonio.sytes.net/davide/PalladiumWallet/issues/new";
const string issueUrl = "https://github.com/davide3011/PalladiumWallet/issues/new?template=bug_report.yml";
var launcher = TopLevel.GetTopLevel(this)?.Launcher;
if (launcher is not null)
await launcher.LaunchUriAsync(new Uri(issueUrl));
}
private async void OnOpenReleasePageClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm || string.IsNullOrEmpty(vm.UpdateReleaseUrl)) return;
var launcher = TopLevel.GetTopLevel(this)?.Launcher;
if (launcher is not null)
await launcher.LaunchUriAsync(new Uri(vm.UpdateReleaseUrl));
vm.IsUpdateAvailableOpen = false;
}
// Esc (desktop) or Back (Android) closes the topmost open overlay.
protected override void OnKeyDown(KeyEventArgs e)
{
@@ -151,6 +165,7 @@ public partial class MainView : UserControl
if (vm.IsWalletInfoOpen) { vm.CloseWalletInfoCommand.Execute(null); e.Handled = true; return; }
if (vm.IsSettingsOpen) { vm.IsSettingsOpen = false; e.Handled = true; return; }
if (vm.IsHelpOpen) { vm.IsHelpOpen = false; e.Handled = true; return; }
if (vm.IsUpdateAvailableOpen) { vm.IsUpdateAvailableOpen = false; e.Handled = true; return; }
}
base.OnKeyDown(e);
}
+66
View File
@@ -0,0 +1,66 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace PalladiumWallet.Core.Net;
/// <summary>A GitHub release newer than the running app.</summary>
public sealed record LatestRelease(string Tag, string HtmlUrl);
/// <summary>
/// Checks the latest GitHub release for the project against the running app version.
/// Best-effort only: any network/parse failure or an up-to-date app both resolve to null,
/// so callers never need to distinguish "no update" from "couldn't check".
/// </summary>
public static class UpdateChecker
{
private const string ReleasesApiUrl = "https://api.github.com/repos/davide3011/PalladiumWallet/releases/latest";
private sealed class GitHubReleaseResponse
{
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
}
public static async Task<LatestRelease?> CheckAsync(string currentVersion, CancellationToken ct = default)
{
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.UserAgent.ParseAdd("PalladiumWallet");
using var response = await http.GetAsync(ReleasesApiUrl, ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode) return null;
var release = await response.Content
.ReadFromJsonAsync<GitHubReleaseResponse>(ct)
.ConfigureAwait(false);
if (release?.TagName is not { Length: > 0 } tag) return null;
if (!TryParse(tag, out var remote) || !TryParse(currentVersion, out var local))
return null;
if (remote <= local) return null;
return new LatestRelease(tag, release.HtmlUrl ?? $"https://github.com/davide3011/PalladiumWallet/releases/tag/{tag}");
}
catch
{
return null;
}
}
/// <summary>Parses "v1.2.3" / "1.2.3-beta" style tags into a comparable <see cref="Version"/>.</summary>
private static bool TryParse(string raw, out Version version)
{
var s = raw.Trim();
if (s.StartsWith('v') || s.StartsWith('V')) s = s[1..];
var dash = s.IndexOf('-');
if (dash >= 0) s = s[..dash];
return Version.TryParse(s, out version!);
}
}