CI ran tool/generate_new_secrets.dart unpinned, so every release binary
shipped different AES keys/salts for the stored PIN and wallet passwords.
Updating the app then failed to decrypt what the previous build stored:
'Invalid or corrupted pad block' at PIN entry and the 'Corrupted seeds'
dialog, with wallet files fully intact.
- entities/encrypt.dart: legacy-key fallback decode for PIN and wallet
password blobs (candidate sets recovered from the shipped 1.0.1 binary;
output validated to reject PKCS7 false positives)
- key_service/auth_service: on successful legacy decode, re-encrypt under
the pinned key — installs self-heal at first unlock
- all build workflows: inject the eight pinned CRYPTO_* repo secrets after
secret generation and hard-fail if any is missing, so crypto salts can
never silently rotate again (values pinned via
~keys/set-gitea-crypto-secrets.sh)
- bump 1.0.2+12
App Store Connect warning 90068 on the last two uploads: from Spring
2027 all iOS apps must set MinimumOSVersion 15.0 or later. Raising from
13.0 costs almost no devices (iOS 15 still covers iPhone 6s and SE 1).
Rides the next iOS build.
generated/i18n.dart and several .g.dart parts are untracked generation
outputs too; without them a fresh checkout analyzes with thousands of
undefined_identifier errors. Run model_generator.sh and
generate_localization exactly as the build lanes do.
pubspec.yaml, the cw_* dependency wiring, wallet_types.g.dart, and the
.secrets.g.dart files are generated and untracked, so the workflow's
bare pub get failed on every push since the workflow landed. Mirror the
build lanes: description -> generate_pubspec -> coin configure ->
generate_new_secrets (empty defaults), then analyze and test. This also
makes the CI resolve identical to release builds, so descriptor drift
like the cake_backup ref conflict fails here in minutes instead of at
release-build time.
Pub requires identical git descriptors across the graph: the root's
pinned SHAs (pubspec_base) conflicted with cw_core's ref:main /
unpinned declarations, failing CI's pub resolve after the configure
step regenerates pubspec.yaml. Local resolution had masked it because
the untracked working-tree pubspec.yaml predated the pins. Pins now
identical everywhere (audit script confirms no remaining multi-ref git
deps); sibling lockfiles re-locked.
User report 05ebf86e: monero?.getCurrentAccount on a Wownero wallet. The
null-aware operator guards plugin presence, not wallet type, so the
Monero bridge always ran and CWMonero's cast threw before the ?? chain
could reach the wownero branch. Dispatch by wallet.type instead. Swept
lib for the same anti-pattern: account_customizer already dispatches
(fixed 2026-08-07) and the settings_store call sites take no wallet
argument; this was the last live instance.
Server-side forex (ECB daily reference rates) shipped in
hash-wallet-prices; the wallet needed no code change. Comment updated so
the file stops claiming USD-only.
- Wownero default becomes node.wownero.org:443 (SSL, ours) instead of a
third-party plaintext host
- Arbitrum's default was arbitrum.nownodes.io, which requires an API key
we don't ship (nowNodesApiKey is empty) — every fresh install had a dead
default RPC; now arbitrum-one-rpc.publicnode.com
- remove the other NOWNodes entries from node lists and the EVM chain
registry for the same reason; historical migration maps keep their
entries so old users still get migrated off
Deposit side: natives of shipped chains (WOW, XMR, BTC, BCH, DOGE, XNO,
ETH, ETH-Base, ETH-Arb, BNB, POL) plus major stables (USDT/USDC variants,
DAI). Receive side: same minus BNB (inherited upstream restriction) plus
LTC as a receive-only target. ZEC, DASH, SOL, AVAX, Zano, Haven, ADA,
EOS, XLM, XRP, TRX, TON and the rest disappear from the swap UI; their
CryptoCurrency entries stay defined because the raw ids back Hive-stored
trade history.
The 10 language wordlists were byte-identical between cw_monero and
cw_wownero (16,291 duplicated lines). A single copy now lives in
cw_core/lib/mnemonics/, and the configure.dart code-gen templates import
from there.
- Cake Pay: 36 files (~5.3k LOC) plus DI registrations, routes, and UI
branches (feature was hard-disabled and unreachable)
- unreachable buy providers: robinhood, dfx, onramper, meld, kryptonim
(~1.7k LOC); MoonPay untouched
- exchange providers that only existed as commented-out wiring:
changenow, sideshift, chainflip, exolix, swaptrade, letsexchange,
stealthex, xoswap, swapsxyz, thorchain, simpleswap (~5.2k LOC);
Trocador untouched; ExchangeProviderDescription values and Hive
adapters kept so stored trades and orders still deserialize
- di.dart setup() split from one 1,452-line function into 10 sequential
chunk functions; registration execution order verified identical
before and after (251 register calls)
Address-field input no longer leaves the device except toward
infrastructure implied by the typed name itself:
- remove Twitter, Mastodon, Nostr, Yat, Unstoppable Domains, FIO,
ThorChain-name, zcash.me, and Zano alias lookups (Nostr and FIO ran
with no setting gate at all; Zano is not even a supported chain)
- keep OpenAlias, BIP-353, .well-known, LNURL, and ENS, each toggleable
in Settings -> Domain lookups
- BTC fee/block/restore-height queries move from mempool.cakewallet.com
to public mempool.space (also fixes two /api/v1 paths that 404 on
standard mempool deployments)
- PRIVACY.md updated to disclose the remaining flows (mempool.space,
name lookups, opt-in silent payments and payjoin); issue templates and
the translation prompt de-Caked
- delete the stock Flutter counter widget_test (always failed) and the
lightning invoice tests (network-dependent, and the feature is off)
- fix rotted assertions: removed-chain wallet types (solana, tron, zano,
decred) and mainnet-only BTC/LTC pattern expectations
- new test.yml workflow: pub get + analyze + test on push/PR to dev/main
- analysis_options: drop excludes for deleted paths, stop excluding the
five live coin facades, enable empty_catches and 7 other correctness
lints (still 0 analyzer errors)
- integration_test_runner: clean hash_wallet data dirs, not cake_wallet
- resetToDefault omitted Wownero: the reset button null-crashed and left
WOW wallets with an empty node list
- accountLabel was missing a return and rendered blank for WOW wallets
- sec_random now throws instead of returning empty bytes when the native
entropy channel fails or under-delivers
- iOS builds no longer register the leftover cakewallet:// URL scheme
- WOW fee estimate now fetched from the connected daemon's
get_fee_estimate RPC and cached; the inherited XMR constant table
remains only as a pre-fetch fallback
- pin cake_backup, flutter_local_authentication, and bech32 git deps to
their locked SHAs for reproducible builds
flutter build windows materialises plugins under
windows/flutter/ephemeral/.plugin_symlinks/ as NTFS symlinks. Flutter gates
this on the AppModelUnlock registry key rather than the actual privilege, so
the build failed with "Building with plugins requires symlink support".
Sets AllowDevelopmentWithoutDevLicense=1 in HKLM (what the Settings toggle
does); LocalSystem can write it, so no interactive login. Idempotent.
storeshots.py (website-dark theme), storeshots_ipad.py (2048x2732), and
storeshots_play.py (1290x2580, Play's 2:1 cap). Compose the funded demo captures
onto the Hash Bags brand background for the store listings.
Replace the static success illustration with HashBagSuccessAnimation: the money
sack sits at the bottom and #/$/₿ glyphs erupt from its mouth, rising and fading
on a loop. Glyphs are PNG assets (the app bundles no font with a ₿ glyph).
InfoPage gains an overridable buildIllustration() hook (default unchanged:
static HashImageWidget); only TransactionSuccessPage overrides it, so every
other InfoPage is untouched.
NOTE: written without a local Flutter build (Dart pub can't resolve here) —
verify on the next CI/device build. Self-contained and revertible; the static
hash_bag.png remains the fallback via imageLightPath/imageDarkPath.
Send-success art: replace the Cake birthday cake with a Hash Bags money sack
that has a # on it and erupts #/$/₿ (assets/images/hash_bag.png). Used by the
success page, send-confirm sheet, exchange-trade, and dEURO savings.
Asset renames (files + refs):
- birthday_cake.png -> hash_bag.png
- restore_cupcake(.png/_dark) -> restore_hashbag.png
- qr-cake.png -> qr-hashbag.png
- arrow_bottom_cake_green.png -> arrow_bottom_green.png (+ 2.0x/3.0x)
Identifier renames:
- CakeImageWidget -> HashImageWidget (widget + file + 93 refs)
- typography _cakeRegular/_cakeBold/_cakeSemiBold -> _hash*
Transaction filter: drop the Cake Pay "Orders" row and every non-Trocador
trade provider; the swap gate is already Trocador-only, so this just aligns the
filter UI with reality.
Android launcher: add breathing room to the adaptive-foreground # (all
densities, monochrome included); iOS icon unchanged.
About page: swap the Cake easter eggs for Hash Bags ones.
Deferred (needs a build to verify): new-ui .svg.vec filename renames
(cake_squircle_icon, cakewallet-wordmark, cake-setting) — already Hash Bags in
content, only the filenames are cake, and they route through the .vec precompile.
Composes App-Store-ready 1290x2796 (6.9" iPhone) screenshots from the raw
1080x1920 harness captures: crops the mobile status bar (per-screen — deeper
for bottom-sheet modals), frames each capture with rounded corners + soft
shadow on the Hash Bags brand gradient, and burns a headline + subhead.
Copy is factual and benefit-driven (no price/return/"investment" claims) to
stay inside App Store Review 3.1.5 and the no-misleading-claims rules. Reads
brand colors/fonts from packs/hash/pack.json; needs Pillow.
Output (funded demo wallet, real 13.37 WOW): dashboard / receive / send / swap
under ~/Build/hash-wallet/demo/appstore/. Same source works for the Play
listing; frame + resize for other required device sizes as needed.
getWalletPassword force-unwrapped the secure-storage read, so a null iOS
Keychain result (item written by an older build under different Keychain
params, e.g. after a TestFlight update) hard-crashed the app and the wallet
showed "Corrupted seeds" — reported by a tester whose older Wownero and BTC
wallets broke on update.
Now it:
- falls back to the legacy readNoIOptions path (queries the OTHER iOS Keychain
accessibility), which can AUTO-RECOVER such wallets without a seed restore;
- falls back to the #__<name>_bak__# backup password the Monero migration
writes before re-encrypting;
- throws a catchable error instead of a null-check crash, so the loader can
offer seed/restore recovery rather than the app dying.
Only changes behavior on the previously-crashing null path — strictly safer.
Not the file cipher (iOS is always Salsa20) nor the Monero migration
(Wownero/BTC don't run it): the wallet files are intact, only the Keychain
password was unreadable. flutter_secure_storage stays pinned (ca897a08) and the
accessibility stays first_unlock; those must not change without a migration
(see docs/internal-notes.md).
Adds DEMO_WALLET_SEED/DEMO_WALLET_HEIGHT support so a demo build restores a
fixed (throwaway, funded) Wownero wallet instead of creating a fresh one, via
WalletRestoreViewModel.create. Balance lives on-chain against the seed, so it
survives any emulator wipe and is reproducible.
Three real bugs fixed on the way:
- build-apk.sh built the --dart-define args as a word-split string; the 16-word
seed's internal spaces split into separate argv tokens and flutter treated a
seed word as a build target ("Target file '<word>' not found"). Now a bash
array keeps each --dart-define one token.
- WalletRestoreViewModel.getCredentials reads name/passphrase from the options
MAP (not the vm field) and casts name as non-null String, so the restore
options must include 'name' and 'passphrase'.
- forward DEMO_WALLET_SEED/HEIGHT into the build container; source demo.env.
Verified end to end: demo build restores the wallet, its address matches the
funded one exactly (QR-decoded), it syncs at height 861800, and the dashboard
shows the real 13.37 WOW balance + tx history. Send shows Max 13.37.
The seed lives only in gitignored tools/demo/demo.env (never committed).
docs/internal-notes.md (gitignored) documents the FailedDecryption gotcha.
The iOS fastlane snapshot UITest already passes --dart-define=SCREENSHOT_MODE=
true; accept it as an alias for DEMO_MODE so the Mac screenshot path drives the
same demo-wallet boot without changing the UITest. Trivial const OR; validated
on next build.
Adds a debug-only capture mode so screenshots/video never depend on the
onboarding/PIN flow, whose synthetic taps are unreliable under the headless
emulator (both flutter_test and adb input drop taps on the PIN confirm screen).
lib/entities/demo_mode.dart: on launch, when kDebugMode AND
--dart-define=DEMO_MODE=true, create a fresh throwaway Wownero wallet through
the real WalletNewVM.create() (so walletInfo/derivation/paths/persistence and
changeCurrentWallet all happen exactly as in the UI), then set the PIN and
allow auth — landing on the dashboard. Reuses the wallet on later launches.
Both halves are compile-time const/kDebugMode, so this is tree-shaken out of
release: DEMO_MODE cannot be enabled in a store binary.
lib/main.dart: call maybeSetupDemoWallet() during bootstrap and force the
initial route to the dashboard in demo mode.
tools/demo/{build-apk,capture}.sh: DEMO=1 bakes the define into a local build;
flutter drive passes it so scenarios start at the dashboard.
Verified end to end: demo build boots to a live Wownero wallet and captured a
clean 1080x1920 store set (dashboard/receive/send/swap/settings) with no PIN.
This also implements the SCREENSHOT_MODE the iOS UITest scaffold already
references (store/SCREENSHOTS.md Path B).
Note: the API-35 emulator needs -gpu host (real GPU) for deep-page rendering;
-gpu software hard-crashes qemu on the Receive page. Stills use screencap
(screenrecord's encoder is unrelated).
Hardening from the first real end-to-end runs against the app:
- build-apk.sh: guard bitbox on the api.aar artifact (not the dir); unshallow
the reown_flutter tarball so prepare_reown.sh can checkout its pinned commit
(the tarball ships a shallow repo -> "unable to read tree"); generate reown
uniffi bindings when absent.
- capture.sh boot: API 35 + `-gpu software -feature -Vulkan`. API 36.1's guest
encoder is broken (screenrecord err=-38, screencap DMA assert) under both
swiftshader_indirect and -gpu host; API 35 works.
- capture.sh: defer screenrecord until the app is foregrounded (flutter drive
builds for minutes; the 180s cap must cover the scenario, not the build);
match the target device by AVD name so a co-running emulator isn't hijacked;
record to /data/local/tmp (headless /sdcard FUSE is flaky).
- README: document the API-35 requirement and reasoning.
integration_test/ has not compiled since 784a082f1 removed SOL/TRX/ZEC/DCR/
ZANO/Haven: common_test_flows.dart and common_test_constants.dart still
referenced WalletType.solana/.tron/.zano/.decred/.zcash/.haven and secrets that
were never generated, and wallet_keys_robot branched on .solana/.tron. Any
`flutter drive` over these components failed at kernel compile.
- common_test_flows.dart: drop removed chains from both wallet-type switches.
- common_test_constants.dart: default test wallet SOL -> WOW (Wownero-first;
SOL no longer exists), with a real WOW address.
- wallet_keys_robot.dart: EVM-only branch (solana/tron gone).
- common_test_cases.dart tapItemByKey: wait for the widget (up to 20s) instead
of pumping a flat 500ms then asserting. Screens behind an async transition
(PIN-setup success button) were a coin flip; this makes the failure honest
("not there after 20s") and the pass reliable. Pump loop, not pumpAndSettle,
since the dashboard sync animation never settles.
The Appfile and Runner.xcodeproj carried 32J6BB6VUS (a stale leftover). The
real Such Software LLC team is D8PL9F7X33. This did not affect CI signing:
build-ios-testflight.yml rewrites DEVELOPMENT_TEAM to the APPLE_TEAM_ID secret
before archiving, so TestFlight builds already signed with the correct team.
This aligns local Xcode and the fastlane snapshot path (which are NOT rewritten
by CI) with what CI actually uses.
Team IDs are public identifiers (embedded in every distributed app's
provisioning profile), not secrets.
Drives the real app through real flows on a device while recording the screen,
then cuts ONE recording into both deliverables — store screenshots and
marketing video — so a listing can't drift out of sync with its own trailer.
integration_test/demo/
DemoSession adds what a passing test lacks: pacing (beat/hold/typeSlowly, so
footage is watchable rather than a strobe), timing marks reported through
binding.reportData, and seed guards. Scenarios reuse the existing 25 page
robots rather than reimplementing navigation:
core_store_set dashboard, receive, send (composed, NEVER broadcast),
wallets, security — the store screenshot set
onboarding_create welcome -> PIN -> seed -> dashboard
restore_from_seed restore -> syncing
swap_and_buy Trocador quote, stopping BEFORE creating a trade; Buy
skipped when no provider key, matching what reviewers see
tools/demo/
capture.sh orchestrates boot/record/cut/shots/reel
marks.py maps timing marks onto the recording; cuts, extracts frames,
builds manifests
reel.py branded master; reuses such_graphics video_transitions/video_qa
when ~/src/such-graphics is present, degrades cleanly when not
(this repo is public and must build without it)
build-apk.sh local APK via the CI container image
Seed safety — the demo wallet is a real wallet with real funds:
- seed comes from Vaultwarden at capture time, never the repo
- guardNoSeedOnScreen() fails the run on a visible mnemonic, catching both
the known seed and the generic shape of one (so freshly generated seeds
are caught too)
- unavoidable seed screens are wrapped in blindfold(); those spans are
excised from the video with 0.6s padding, and screenshots honour the same
padding
- marks.py refuses to build a publishable master if any clip was cut without
marks, since blindfold spans could not have been excised
Apple App Preview gets no touch indicators, captions, cards or music, and
reel.py enforces the 15-30s window rather than emitting an out-of-spec file.
Everything else keeps taps on, drawn by SurfaceFlinger via show_touches rather
than composited, so they look real.
Play sizing: the harness builds its own 1080x1920 AVD. The stock Medium Phone
AVD is 1080x2400 = 2.22:1 and Play rejects phone screenshots above 2:1.
Also gitignores android/app/upload-keystore.jks (and *.jks/*.keystore): the
Android workflow decodes ANDROID_KEYSTORE_BASE64 to exactly that path, so the
Play upload signing key was one `git add -A` away from being committed.
TestFlight run #118 succeeded on 2026-07-09, and that workflow ends with
`xcrun altool --upload-app` — so 1.0.0+8 is already uploaded and App Store
Connect will reject a redundant binary at the same build number. Re-running at
+8 would have burned a full signed archive before failing at the last step.
Both toolchains read this single source (scripts/{ios,android}/app_env.sh parse
pubspec_description.yaml), verified reporting build=9.
Note there is still no auto-bump: build-ios-testflight.yml is workflow_dispatch
only and its own comment flags this as a TODO, so every TestFlight run needs a
manual bump until that is wired up.
Wownero left Codeberg in July 2026 (Codeberg announced it would ban crypto
projects) and remigrated to github.com/wownero/wownero. monero_c's .gitmodules
still points at the dead Codeberg URLs, which now fail with "remote: Bye" /
HTTP 403. That broke the iOS TestFlight build on 2026-07-31 during "Fetch
prebuilt monero_c bundle".
The clone never needed to happen. Every workflow (android/ios-sim/
ios-testflight/linux/windows) consumes PREBUILT .so/.a files from
release-bundle.zip; nothing outside release/ references the monero/wownero
source trees. The only thing needed from the checkout is `git describe --tags`
to resolve the release/<tag>/ path. prepare_moneroc.sh was cloning ~1GB of
source plus deep transitive submodules purely to discard it.
So skip the submodules by default. Source checkout is now opt-in behind
MONERO_C_BUILD_FROM_SOURCE=1, and when requested it redirects the dead
Codeberg URLs:
wownero -> github.com/wownero/wownero (canonical after the migration)
randomwow -> github.com/mrcyjanek/randomwow (what monero_c's apply_patches.sh
already switches the remote to; NOT wownero/RandomWOW, which is
a tevador/RandomX fork on a different lineage without the
pinned commit)
Both submodules stay pinned to exact SHAs, so git verifies the object hash on
fetch and a different host cannot substitute content.
Verified clean-room with Codeberg still 403ing: prebuilt mode exits 0 with
`git describe --tags` = v0.18.4.6-RC1-2-gbc8d1a0 and zero Codeberg access;
source mode lands randomwow at the pinned cd137b1ea.
The dependency_override pinned ledger_usb_plus to
konstantinullrich/ledger-usb-plus@dad482c847..., a commit that was force-pushed
away and no longer exists — so 'flutter pub get' fails ('bad object') on any
cold runner (the Android/Linux containers). It passed on iOS only because the
Mac-mini runner had a warm pub cache. cake-tech's ledger-flutter-plus fork
depends on the published ledger_usb_plus ^1.0.4, so pin the override to that.
tool/generate_new_secrets.dart imports app packages (encrypt/convert/intl).
pubspec_gen.sh runs `flutter pub get` without set -e, so a silent hiccup there
surfaced downstream as 'Couldn't resolve the package encrypt' at secrets-gen.
Run `flutter pub get` explicitly (set -e) right before the tool on Android +
Linux, matching what the iOS flow effectively guarantees. The build container
already pins Flutter 3.32.0, so this resolves cleanly.
Build 7 is already on TestFlight. Both iOS (CFBundleVersion) and Android
(versionCode) source the build number from pubspec_description.yaml at build
time (scripts/{ios,android}/app_env.sh), so this single bump moves both to 8.
getAvailableBuyProviderTypes() now returns [moonpay] only when moonPayApiKey is
non-empty, so MoonPay's Buy button appears only in builds where the CI secret is
present (and the exchange-helper signer is live). Both build workflows now inject
MOONPAY_API_KEY -> moonPayApiKey and EXCHANGE_HELPER_API_KEY -> exchangeHelperApiKey
alongside Trocador + WalletConnect. moonPaySecretKey is server-side and never injected.
Both build workflows now set walletConnectProjectId in lib/.secrets.g.dart from
the WALLETCONNECT_PROJECT_ID secret (alongside the Trocador keys), so
WalletConnect pairing works in signed builds. Add WALLETCONNECT_PROJECT_ID
(your reown Wallet projectId) to the CI secrets before building.
The previous Linux artifact was a zip of the Flutter bundle/ directory.
That's awkward to distribute — users have to unzip, find the binary,
chmod +x, and figure out the lib/ layout themselves. Replace with a
proper AppImage: single file, double-click to run, no install, no
Flutter knowledge required.
Build steps:
- Stage AppDir with the Flutter bundle under usr/bin/ (the binary's
RPATH=\$ORIGIN/lib already finds plugin libs there).
- Add .desktop file (Hash Bags / Office;Finance;) and 256x256 hash
icon scaled from the iOS marketing PNG.
- Download appimagetool and invoke with --appimage-extract-and-run
so it works inside the build container without FUSE.
Artifact name: hash-wallet-linux-appimage-<sha>.
Android had the same hardcoded CAKEWALLET_BUILD_NUMBER=1 bug we just
fixed for iOS — bumping pubspec_description.yaml would not have moved
Play Store versionCode at all, and the second AAB upload would have
been rejected for duplicate versionCode. Mirror the iOS fix: parse
the build number from pubspec_description.yaml in scripts/android/app_env.sh.
Regenerate the 16 Tinted iOS icon slices as black bg + white hash mark.
The previous slices reused the green-bg light variant, which iOS 18+
cannot meaningfully tint (green-on-green collapses when the system
applies the home-screen accent color). Black background + white shape
gives iOS a clean luminance map to tint against.
Wownero monero! audit: cleared. Audit flagged two callsites in
fees_view_model.dart, but both wownero!.getWowneroTransactionPriority*
and monero!.getMoneroTransactionPriority* return the same
MoneroTransactionPriority singleton (cw_wownero.dart:158 +
cw_monero.dart:169), so the cross-call is inelegant but not buggy.
Bump 1.0.0+6 -> 1.0.0+7.