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.
The "Wallet Accounts" reorder modal threw on open for Wownero wallets:
account_customizer.dart unconditionally called monero!.getCurrentAccount,
which casts the wallet to MoneroWallet internally. WowneroWallet is a
sibling type, not a subtype, so the cast blew up before the active-card
swap logic ran.
Dispatch on wallet.type and use the wownero! proxy for Wownero, matching
the pattern already in card_customizer_bloc.dart, addresses_page.dart,
and dashboard_view_model.dart. Skip the swap entirely for any other
wallet type rather than guessing.
Bump 1.0.0+5 -> 1.0.0+6.
Bumping version in pubspec_description.yaml has had zero effect on
the iOS CFBundleVersion: app_env.sh hardcoded CAKEWALLET_BUILD_NUMBER=1,
app_config.sh substituted that literal into Info.plist's
\$(CURRENT_PROJECT_VERSION) placeholder via PlistBuddy before flutter
even saw it, and every TestFlight upload shipped as build 1 regardless
of what we bumped in pubspec. Apple rejected the +4 and +5 IPAs as
duplicates of the +3 that got through as build 1.
Make pubspec_description.yaml the single source of truth by parsing
its `version: X.Y.Z+N` line in app_env.sh.
Hash Bags' crypto is either OS-provided (TLS / CryptoKit / CommonCrypto)
or standard published algorithms shipped as MIT-licensed open source
(secp256k1, ed25519, SHA-2/3, AES, Argon2, ChaCha20). All of that
qualifies as "exempt" under BIS export controls (§740.17(b)(3)).
Declaring the answer in Info.plist skips the per-build App Store Connect
encryption questionnaire and removes the annual BIS self-classification
filing requirement.
Bump 1.0.0+4 -> 1.0.0+5 in case +4 already claimed an Apple build slot.
The iOS build was still pulling Cake Wallet's purple/orange wallet
icon from assets/images/ios_icons/cakewallet_ios_icons/ — visible
in TestFlight as the wrong brand. Replace all 69 light/dark/tinted
slices with the Hash Bags green hash mark from hash.boats/icon-512.png,
flattened against #1a5c38 so the 1024 marketing icon is opaque (no
alpha, as App Store requires) and iOS applies its own corner mask.
Rename the directory cakewallet_ios_icons -> hashwallet_ios_icons so
the repo doesn't carry Cake-branded paths. The build-time flavor is
still named "cakewallet" internally (huge blast radius to rename),
so app_icon.sh just maps that flavor to the new directory name.
Bump build 1.0.0+3 -> 1.0.0+4 for re-upload.
LibTorch.xcframework ships with each slice's Info.plist claiming
MinimumOSVersion=12.0, but the Mach-O LC_BUILD_VERSION inside the
binaries actually requires 13.0 (device) and 14.0 (simulator). Apple's
validator catches the lie and rejects TestFlight uploads with ITMS-90208
("LibTorch.framework does not support the minimum OS Version specified
in the Info.plist") regardless of the app's deployment target.
Revert the previous 15.0 deployment-target bump (which was based on the
wrong hypothesis) back to 13.0 — that's actually what LibTorch supports.
Add a workflow step that plutil-rewrites both slice Info.plists to match
the value their binary actually requires. Bump build number 1.0.0+2 ->
1.0.0+3 for the re-upload.
First TestFlight upload was accepted and processed all the way through,
then Apple's binary validation kicked back ITMS-90208 Invalid Bundle:
The bundle Runner.app/Frameworks/LibTorch.framework does not support
the minimum OS Version specified in the Info.plist.
torch_dart-v1.0.17 ships a LibTorch.xcframework whose Info.plist
declares a higher MinimumOSVersion than our app's 13.0. Apple's
notarization-style check rejects an app that claims to run on iOS
versions where a bundled framework refuses to load.
Bump the four spots iOS deployment target lives:
- ios/Podfile (was 13.0)
- ios/Flutter/AppFrameworkInfo.plist MinimumOSVersion (was 12.0)
- ios/Runner.xcodeproj/project.pbxproj — 6 IPHONEOS_DEPLOYMENT_TARGET
entries (mixed 12.0 / 13.0)
all → 15.0. iPhone 6s (2015) and newer; very wide 2026 coverage.
Also bump build number 1.0.0+1 → 1.0.0+2 in pubspec_description.yaml.
Apple rejects re-uploads with the same build number (ITMS-90478), so
the next TestFlight upload after this fix needs a fresh +N.
Workflow diagnostic step that dumps LibTorch.xcframework's Info.plist
MinimumOSVersion now lives in the "Fetch prebuilt torch_dart" step. If
15.0 still isn't enough, the log tells us exactly what to bump to next.
altool bailed with "Failed to load AuthKey file. (-39) The file ...
does not contain a valid authentication key." — same shape as the
.p12 truncation we hit earlier. The .p8 stored secret is probably
truncated or has stray characters from the original web-UI paste.
Add fail-fast validation right after decoding the .p8 to disk:
prints the first + last lines (just the PEM markers, no key body)
and runs `openssl pkey -in file -noout` to actually parse it. If
either check fails the step bombs immediately with a clear "re-paste
via Gitea API" instruction, saving 30+ seconds before altool fails
the same way with a less useful error.
One of the Flutter plugins in our dep chain (sp_scanner / payjoin /
something in the cargokit family) compiles its rust component with
+nightly. The archive step ran for 27s then bombed with:
Error (Xcode): Missing manifest in toolchain 'nightly-aarch64-apple-darwin'
We were installing only stable + iOS targets. Add nightly toolchain
(minimal profile) + iOS targets for it alongside the stable ones.
~250 MB extra download once per runner workspace lifetime.
build-ios-sim.yml has the same stable-only setup but never tripped on
this — likely because the failing plugin's release-only build path
isn't exercised in the unsigned sim build. Leaving sim workflow
alone for now; copy the nightly steps over if it ever surfaces there.
Xcode build-setting precedence puts TARGET-LEVEL pbxproj settings
ABOVE the target's xcconfig. So the earlier append to Release.xcconfig
(CODE_SIGN_STYLE = Manual etc.) was being silently overridden by the
pbxproj's CODE_SIGN_STYLE = Automatic. The build did Automatic
signing anyway and bombed with:
Error (Xcode): No Accounts: Add a new account in Accounts settings.
Error (Xcode): No profiles for 'com.suchsoftware.hashwallet' were
found: Xcode couldn't find any iOS App Development provisioning
profiles matching 'com.suchsoftware.hashwallet'.
(Note "App Development" not "App Distribution" — confirming the
project was still asking for a dev profile via Automatic signing.)
sed-patch the Runner target's Release config block in
project.pbxproj directly, scoped by the stable Release config UUID
(97C147071CF9000F007C117D) so we don't touch Debug/Profile configs.
Sets:
CODE_SIGN_IDENTITY = "Apple Distribution"
CODE_SIGN_STYLE = Manual
DEVELOPMENT_TEAM = ${APPLE_TEAM_ID}
PROVISIONING_PROFILE_SPECIFIER = ${PROFILE_NAME}
Patch runs only at CI time; the committed pbxproj stays on Automatic
signing for local Xcode dev. The dumped block at the end of the
step confirms which lines actually changed, so the next failure
mode (if any) is debuggable.
The openssl pkcs12 round-trip step was speculative defense against
"modern macOS Keychain exports incompatible with security import" —
a problem we made up. What was actually happening:
1. The original failure ("SecKeychainItemImport: Unknown format")
was caused by a TRUNCATED base64 secret value. security wasn't
rejecting the format; it was rejecting bytes it couldn't parse
because the file was 29 bytes short.
2. The follow-on failure ("RC2-40-CBC unsupported") was the openssl
conversion step bombing because openssl 3.x has RC2-40-CBC
disabled by default. macOS Keychain Access actually exports
PKCS12 with RC2-40-CBC + PBE-SHA1 — exactly what `security
import` reads natively.
With the truncated-secret fixed (re-pasted via Gitea API), the .p12
on disk is now correct. Import it directly without round-tripping
through openssl. Less code, fewer failure modes, faster.
macOS Keychain Access on Sonoma+ exports .p12 with modern crypto
(AES-256-GCM + PBKDF2 + SHA-256). The `security import` binary on
some macOS releases — including ours apparently — rejects it with:
SecKeychainItemImport: Unknown format in import.
The openssl decode itself was fine (now using openssl base64 -d -A),
but security can't import the resulting PKCS12 envelope.
Pipe the .p12 through openssl: unpack with the user's password, then
re-export using legacy PBE-SHA1-3DES for both certs and keys, plus
SHA1 MAC. The resulting .p12 uses 90s-era crypto for the OUTER
envelope, which is what security import wants — the actual signing
keys + cert inside are unchanged. No security implication; the legacy
format encryption only matters for the few seconds the .p12 lives in
$RUNNER_TEMP before being imported and deleted.
Also added diagnostic output (file size, magic bytes, file(1)) so the
next failure mode (if any) is easier to characterize.
`base64 --decode` also failed on the Mac runner with the same
"(null): error decoding base64 input stream" — so either this macOS
version's BSD base64 doesn't honor --decode (only -D) or it chokes
on the input format. Either way, swap the three secret decodes to
`openssl base64 -d -A`:
- openssl ships with macOS by default; no extra install
- -d = decode (same on BSD and Linux, no platform skew)
- -A treats the input as one continuous line (matches our
`base64 -i x | tr -d '\n'` encoding flow)
If openssl base64 ever bites us we'll fall back to python3's
base64.b64decode, but openssl has been bulletproof for ~25 years.
macOS uses BSD base64 where -d historically means *debug*, not
*decode* like GNU base64. On the Mac runner that bit us — the
first base64 -d invocation ran in debug mode, consumed no stdin,
and produced an empty .p12 file, then the keychain import bombed
with the now-classic "(null): error decoding base64 input stream"
once a downstream tool tried to read the empty file.
Switch all three secret decodes (.p12 cert, .mobileprovision,
.p8 API key) to base64 --decode. Long form is identical between
GNU coreutils and BSD base64.
The Apple secrets themselves were fine — sanity-check step
confirmed all non-zero lengths (TEAM_ID/KEY_ID showed as ***0
in the log, which is the actual literal 10 with the 1 redacted
because some other secret value contains the substring "1").
First TestFlight trigger failed at the .p12 import step with
"base64: stdin: (null): error decoding base64 input stream" — meaning
$APPLE_DIST_CERT_P12_BASE64 was empty when reaching the keychain step.
Add a length-only debug step at the top of the Apple-signing section
that prints the length of every required Apple secret and bails
fast with a clear "set this secret on Builds/hash-wallet" message if
any is zero-length. Same pattern build-android.yml uses for the
Android keystore + Trocador secrets.
The likely cause is the Apple secrets being set on github-such-
software/hash-wallet but not on the Builds fork the workflow runs
in — Gitea secrets are repo-scoped, not org-scoped.
Two unrelated wallet-UX fixes in one commit:
1) lib/utils/exception_handler.dart — switch the "Report Error" flow
from "open prefilled GitHub issue URL" to "copy error trace to
clipboard + open https://hash.boats/report/". The hash.boats backend
(Node service + nginx proxy + Telegram relay) was already deployed
to the suchwow server; the in-app handler just never got wired to
use it. No GitHub account required, works on every platform.
2) lib/wownero/cw_wownero.dart — add 'primaryAddress' to the keys map
returned by getKeys(). The lib/monero/cw_monero.dart equivalent
already had this; without it, WalletKeysViewModel renders zero
primary-address entry for Wownero wallets and users can't easily
read off the main address for sharing (e.g. to set up a view-only
wallet on another device). The underlying WowneroWalletBase.keys
already populated primaryAddress — only the getKeys map projection
was missing it.
The committed project.pbxproj Release config uses CODE_SIGN_STYLE =
Automatic + CODE_SIGN_IDENTITY = "Apple Development" — both set up for
local Xcode-signed-into-an-Apple-ID development workflow. CI has only
the Distribution cert + provisioning profile we install into a temp
keychain, no Apple ID session, so Automatic signing fails at archive
time.
Append manual-signing overrides to ios/Flutter/Release.xcconfig in CI
(xcconfig values take precedence over pbxproj defaults, so this
overrides without modifying the committed project file):
CODE_SIGN_IDENTITY = Apple Distribution
CODE_SIGN_STYLE = Manual
DEVELOPMENT_TEAM = ${APPLE_TEAM_ID}
PROVISIONING_PROFILE_SPECIFIER = ${PROFILE_NAME}
PROFILE_NAME is parsed earlier from the .mobileprovision Name field.
APPLE_TEAM_ID comes from the existing Gitea secret. Local Debug/Profile
builds remain on Automatic signing — unchanged.
7-Zip SFX wrapped HashBags.exe extracted to a fresh %TEMP%\7zS<random>\
subdir on every launch. path_provider_windows derives the wallet
storage dir from the .exe location — so each launch saw a brand new
"app dir" with no prior wallets, failing with "keys corrupted" before
hanging at the PIN prompt. The multi-file zip distribution doesn't
have this problem because the .exe lives at a stable user-chosen path.
Multi-file zip is also what Firefox Portable, VS Code Portable, OBS
Portable, and ~every other Flutter Windows desktop app ship as. The
fashion in Windows is "portable = no install / no registry," not
"portable = one file."
Single-file via Inno Setup installer or Enigma Virtual Box remains
possible as a v1.1 follow-up. For v1, ship the zip.
The 23.01 "7-Zip Extra" archive (7z2301-extra.7z) was reorganized and
no longer contains the installer-mode SFX modules — only the
standalone 7za.exe/dll + Far plugin. The SFX modules were moved into
the LZMA SDK archive (lzma2301.7z), under bin/:
bin/7zSD.sfx — classic installer-mode SFX (config.txt + RunProgram)
bin/7zS2.sfx — newer variant
bin/7zS2con.sfx — console variant of the new module
Fetch the LZMA SDK instead. Use a filtered extract ('bin/*.sfx') so we
only pull ~150KB of binaries from the 1.3MB SDK archive (skipping the
700 source-code files we don't need).
7-Zip Extra 23.01 puts 7zSD.sfx / 7zS.sfx under x64/ (64-bit variants)
and x86/ subdirs, not at the archive root. The previous lookup only
checked the root and bailed even though the modules were one level
deeper. Add x64/ as the primary candidate path, fall back to root for
older Extra archive layouts.
The 24.x extra archives have been pruned from the live 7-zip.org CDN —
verified by curl HEAD over 2401/2403/2405/2406/2407/2408/2409, all
return 404. Only 7z2301-extra.7z (Jan 2023) is still served with 200.
That version's installer-mode SFX modules (7zS.sfx, 7zSD.sfx) are
backwards-compatible with any modern Windows so pinning here is fine.
choco's 7-Zip install ships 7z.sfx (GUI extract-here prompt) and
7zCon.sfx (console extractor) — but NOT 7zS.sfx / 7zSD.sfx, the
installer-mode SFX modules that support `extract to temp + auto-run
RunProgram`. Those live in the "7-Zip Extra" supplementary archive
(https://www.7-zip.org/a/7z2409-extra.7z) which has to be downloaded
separately.
Fetch + extract the Extra archive on demand in the SFX-wrap step.
Cache it under $RUNNER_TEMP so subsequent runs reuse the same module
without re-downloading. ~250 KB download, one-shot per runner working
dir lifetime.
The previous commit deleted assets/zano_node_list.yml (Zano isn't
shipped in Hash Bags) but pubspec_base.yaml still listed it under
flutter.assets. Flutter assemble failed during the asset-bundling
stage:
Error detected in pubspec.yaml:
No file or variants found for asset: assets/zano_node_list.yml.
Remove the line — same intent as the original deletion, just the
second half of the change.
The previous commit's bash heredoc for the 7-Zip SFX config:
cat > sfx-config.txt <<'SFXCFG'
;!@Install@!UTF-8!
...
SFXCFG
…had its body at the left margin (since heredoc lines are taken
literally). But the surrounding YAML used `run: |` which requires
every line to be indented more than the `run:` key. The unindented
heredoc body broke out of the block scalar — Gitea Actions
rejected the workflow with "could not find expected ':'" at the
;!@Install line.
Switch to a single `printf '...' > sfx-config.txt` line that fits
cleanly inside the YAML block. Same on-disk file content, no YAML
indentation conflict.
Replace the .zip artifact with a 7-Zip self-extracting .exe wrapper.
On launch, the wrapper extracts the Flutter Windows build tree to
%TEMP% and runs HashWallet.exe — user experience is one .exe to
download and double-click. Each launch re-extracts; subsequent launches
are slower than a real install but zero-touch portable is the user's
explicit preference over an installer.
SFX module probe handles 7-Zip naming variations (7zS.sfx / 7zSD.sfx /
7zSfx.sfx) so the step doesn't break across 7-Zip releases.
Replaced upstream Cake brand references in user-visible ARB *values*
(keys untouched, so generated S.current.<key> references still work):
- "Cake 2FA" → "Hash Bags 2FA"
- "Cake Pay" → "Hash Bags Pay" (still gated off via
FeatureFlag.isCakePayEnabled = false; rebranding the string so it
doesn't ship literal "Cake" copy if the flag ever flips)
- "Cake Wallet" → "Hash Bags"
- "Cake Labs" → "Such Software"
- "by Cake Pay" → "by Hash Bags Pay"
- "cakewallet.com" → "such.software"
English-only adjustments (other locales had their own translations):
- new_first_wallet_text "Keeping your crypto safe is a piece of
cake" → "Keeping your crypto safe shouldn't be hard" (drops the
Cake pun, which now reads odd post-rename)
- outdated_electrum_wallet_description + _receive_warning rewritten
to Hash-Bags-neutral phrasing: "Hash Bags now uses Electrum V2
wallets. Bitcoin wallets created in older builds need to be
replaced; receiving to old wallets may lose funds." Same Hash-Bags-
neutral rewrite propagated to all locales that had a translated
version of the original Cake-Electrum-V2 message.
Deliberately not touched:
- ARB *keys* (cake_2fa_preset, cake_pay_*, cakepay_*, disable_cake_2fa,
etc.) — keys must stay stable so generated S.current.x references
keep resolving.
- "Cupcake" / "cupcake" — a separate companion app referenced in
restore_*_from_cupcake strings, not part of the Cake brand we're
rebranding from.
- Internal Dart identifiers like CakeImageWidget class name.
Also fixed hardcoded titles in lib/src/screens/setup_2fa/setup_2fa.dart:
- Line 18: title 'Cake 2FA' → 'Hash Bags 2FA'
- Line 22: cake2FAGuideTitle 'Cake 2FA Guide' → 'Hash Bags 2FA Guide'
(Local variable name cake2FAGuideTitle kept as-is — it's a code
identifier, not user-visible.)
33 files touched: 31 ARBs (ar, bg, cs, de, en, es, fa, fr, gn, ha, hi,
hr, hy, id, it, ja, ko, my, nl, pl, pt, pt_BR, ru, th, tl, tr, uk, ur,
vi, yo, zh, zh_tw) + lib/src/screens/setup_2fa/setup_2fa.dart.
User flagged the Windows .exe icon as having the hash sign visually
biased toward the top. Root cause: the previous icon source SVG used
<text> with dominant-baseline="central" — the rasterizer fell back to
whatever monospaced font it found, and that font's # glyph isn't
positioned at the geometric center of its em-box (most monospaced
fonts have the # mark biased upward in their em-square).
Replace the text-rendered # with 4 vector <line> elements forming a
mathematically symmetric # centered at (512, 512) of the 1024 canvas.
No font dependency, deterministic across rasterizers.
Re-rendered:
- assets/images/app_logo.png — master 1024x1024 PNG
- windows/runner/resources/app_icon.ico — 7-size .ico (16..256)
- assets/images/cakewallet_android_icon/mipmap-*/ic_launcher.png
(5 densities, the legacy launcher PNG; CI symlinks from here)
NOT regenerated in this commit (follow-up):
- assets/images/cakewallet_android_icon/mipmap-*/ic_launcher_{foreground,background,monochrome}.png
(adaptive icons used on Android 8+ — need separate foreground/
background composition logic)
- assets/images/ios_icons/cakewallet_ios_icons/* (70 files)
- assets/images/macos_icons/cakewallet_macos_icons/* (7 files)
Cake Wallet → Hash Bags / Such Software LLC across the user-facing
platform configs:
- macos/Runner/InfoBase.plist: CFBundleDisplayName + CFBundleName
- windows/runner/main.cpp: top-level window title
- ios/Runner/InfoBase.plist: URL scheme cakewallet → hashbags,
CFBundleURLName y.at → com.suchsoftware.hashwallet
- android/app/src/main/AndroidManifestBase.xml: URL scheme
- scripts/windows/build_exe_installer.iss: full rebrand (name,
publisher, URL, exe name, AppId, DefaultDirName, output filename)
- assets/text/disclaimer_deuro.txt: legal disclaimer text
Version unification → 1.0.0 build 1 across all platforms:
- pubspec_description.yaml (was 0.0.0, description was "Cake Wallet.")
- scripts/android/app_env.sh (was 6.1.2 build 4410)
- scripts/ios/app_env.sh (was 6.1.2 build 418)
- scripts/macos/app_env.sh (was 6.1.0 build 147)
- scripts/windows/build_exe_installer.iss (was 6.1.0)
env.json (Windows) was already 1.0.0.
FAQ contact info → support@such.software / @such_software across all
13 locale faq_*.json files (was support@cakewallet.com /
@CakeWalletXMR / @cakewallet_bot).
Node lists:
- Delete assets/zano_node_list.yml — Zano not enabled in any
configure flag; file was dead code.
- Remove zcashDefaultNodeUri const from
lib/entities/default_settings_migration.dart — Zcash not in
product, const was unreferenced anywhere.
Note on BTC + LTC defaults: audit suggested these were Cake-by-default
but they were already non-Cake (blockstream and electrum-ltc.bysh.me).
Cake stays in the auto-switch pool — flagged for separate decision on
whether to disable auto-switching to Cake nodes for privacy.
Self-hosted Windows runner builds an unsigned Windows Release zip with
the Flutter .exe + bundled monero/wownero DLLs + MinGW runtime DLLs +
MSVC runtime DLLs. workflow_dispatch only — triggered from the Gitea
Actions UI.
Major design points hammered out during initial bring-up:
- Use explicit Git Bash path for `shell:` rather than `shell: bash`,
since act_runner under LocalSystem resolves bare `bash` to
C:\Windows\System32\bash.exe (WSL) which refuses to run as
LocalSystem.
- Configure git identity + safe.directory before any submodule ops,
since LocalSystem has no global git config and scripts/monero_c/
apply_patches.sh does git-commit-equivalents.
- Clone torch_dart + reown_flutter from git instead of tarball — the
upstream-published .tar.gz tarballs contain example-app
.plugin_symlinks with absolute /home/runner paths that Windows tar
refuses to extract.
- Fetch monero_c prebuilt release-bundle.zip (same one Android/iOS
use) and re-stage its x86_64-w64-mingw32 wallet DLLs into the
layout that windows/CMakeLists.txt expects:
scripts/monero_c/release/<coin>/x86_64-w64-mingw32_libwallet2_api_c.dll
CMake's install() rules RENAME them to <coin>_libwallet2_api_c.dll
in the Release dir.
- Mirror those wallet DLLs under their lib<coin>_wallet2_api_c.dll
Dart-FFI-expected names — package:monero's DynamicLibrary.open()
uses a different naming convention than windows/CMakeLists.txt's
RENAME rules.
- Source MinGW runtime DLLs (libssp-0.dll, libwinpthread-1.dll) from
Git for Windows' bundled mingw64/bin — has modern symbols including
pthread_cond_timedwait64 that monero_c requires. Flutter's bundled
mingit is too old (2017-vintage libwinpthread).
setup-windows-runner.ps1 (the one-shot toolchain installer for the
runner host) is gitignored — it's runner-side infra, not part of the
app.
Final public name is Hash Bags (Such Software). iOS CFBundleDisplayName
and Android app.properties (generated by scripts/android/app_env.sh) were
already on Hash Bags; this catches the two remaining user-facing places:
- env.json CW_WIN_APP_NAME (Windows launcher label)
- linux/com.suchsoftware.HashWallet.desktop Name= (Linux app menu label)
Bundle IDs, Dart package name, and the .desktop file's app-id all stay
on hashwallet — those are internal identifiers that nobody sees.
The wow CryptoCurrency had iconPath pointing at assets/images/crypto/wownero.svg,
which doesn't exist — so the row in Create New Wallet rendered with no logo.
The actual asset lives at assets/new-ui/crypto_full_icons/wownero.svg (same
location every other coin uses). Point iconPath there and add flatIconPath
for the balance-card-flat icon, mirroring the Monero definition.
The runtime sudo rewrite step hung 51m waiting for a password the runner
can't enter. The Mac mini's /etc/hosts already points git.such.software
at 127.0.0.1, and the host-mode act_runner inherits it directly, so no
runtime modification is needed. Replace the rewrite with a passive grep
assertion that fails fast with instructions if the entry is missing.
Add a diagnostic step before the upload that prints ACTIONS_RUNTIME_URL,
ACTIONS_RESULTS_URL, GITHUB_SERVER_URL + probes each with curl -kIv. The
last run never reached the upload step, so we still don't know whether
upload-artifact targets git.such.software (uses our hosts override) or
a different address injected by act_runner.
The Mac runner doesn't honor changes to the host's /etc/hosts (whether
because of act_runner sandboxing, container isolation, or DNS caching).
Instead of fighting that, override /etc/hosts INSIDE the runner's job
environment, right before the upload step.
Resolution priority:
1. host.docker.internal — magic name in Docker Desktop containers
that resolves to the Mac host. Use this if available.
2. 127.0.0.1 — works for host-mode runners (runs natively on Mac).
NODE_TLS_REJECT_UNAUTHORIZED=0 (already set) bypasses the self-signed
cert Gitea presents on its internal listener.
Brought back actions/upload-artifact so the .app lands in Gitea's
Actions UI like APK + AAB.
Mac runner spawns each job in a Docker container (via Docker Desktop).
The container has its own /etc/hosts and DNS — the host's hosts entries
don't propagate. DNS for git.such.software resolves to the public IP,
the container can't NAT-hairpin back to its own WAN IP, ECONNREFUSED.
actions/upload-artifact is doubly silly when the runner IS the Gitea
server. Copy the .zip to $GITHUB_WORKSPACE/dist/ instead. That dir
is mounted from the host, so it persists at:
/Users/such-git/.cache/act/<runner-id>/hostexecutor/dist/
Find latest:
find /Users/such-git/.cache/act -name 'hash_wallet_ios_sim_*.zip' -exec ls -lt {} + 2>/dev/null | head
Step also echoes the exact path + a find command in the build log.
Verbose log of yesterday's AAB build revealed:
java.lang.OutOfMemoryError: Java heap space
at java.util.zip.DeflaterOutputStream.write
at com.android.zipflinger.StreamSource.<init>
at com.android.builder.internal.packaging.AabFlinger$writeZip$1.call
AAB packaging streams compressed native libs through ByteArrayOutputStream
buffers. With universal-bundle (all 3 ABIs in one AAB) and parallel
ForkJoinPool workers all drawing from the same 6G heap, the peak exceeds
budget mid-zip. APK build doesn't hit this because per-ABI APKs are
packaged separately, much smaller per-file working set.
Two changes:
1) org.gradle.jvmargs: -Xmx6144M → -Xmx10240M, + MaxMetaspaceSize=1024M
(the daemon-side metaspace also creeps up over a long build).
2) org.gradle.workers.max=2 — fewer parallel workers = lower simultaneous
buffer footprint. Build still parallel, just gentler peak.
Gitea is on the same Mac mini as the runner, behind NPM in a separate
container. The proper fix is a hosts entry on the Mac that points
git.such.software at NPM's LAN IP so the runner reaches Gitea via
NPM's Let's Encrypt cert.
The env var stays in the workflow as a belt-and-suspenders fallback —
no harm if certs already validate cleanly, saves the workflow from
breaking again if the hosts entry ever gets blown away (system reset,
container migration, etc).
iOS:
- Build itself succeeds (Runner.app sits in build/ios/iphonesimulator/).
- actions/upload-artifact's Node.js HTTP client hits git.such.software
via LAN (NAT hairpin), bypassing the public NPM/Let's Encrypt cert
and landing on Gitea's internal self-signed cert. Node refuses with
DEPTH_ZERO_SELF_SIGNED_CERT.
- Setting NODE_TLS_REJECT_UNAUTHORIZED=0 at job env disables verification
for all Node-based actions in the job. Acceptable here because we're
talking to our own server on our own LAN — no external MITM surface.
- Long-term cleaner fix: install Gitea's CA cert into the runner's
system trust store, or have the runner reach Gitea via the public
hostname so NPM's Let's Encrypt cert is presented.
Android AAB:
- APK built + signed + uploaded fine (~14 min). AAB build then failed
at FinalizeBundleTask$BundleToolRunnable / :app:signReleaseBundle
with no visible error.
- Added --verbose to 'flutter build appbundle' so the next run prints
the actual gradle stacktrace. Once we see WHY signReleaseBundle is
failing, we can target the real fix.
Build was failing with:
component download failed for rust-std-aarch64-apple-ios-sim:
could not rename downloaded file from '.../9362b66...partial' to
'.../9362b66...': No such file or directory
One of the iOS pods (likely reown_walletkit's yttrium FFI or similar)
invokes rustup mid-build to fetch iOS targets. When the pod build runs
multiple targets in parallel, multiple rustup processes race on the
same .partial download file — one finishes/renames, the next looks
for the .partial that's no longer there.
Fix: add a workflow step that runs before pod install + flutter build:
- Install rustup if missing (idempotent — self-hosted runner persists).
- Wipe any stale *.partial files (debris from previous racing).
- rustup target add the three iOS triples serially.
That way the pod's mid-build rustup invocation finds everything already
installed and just no-ops.
Two fixes that together address the 'There is no XCFramework found at...'
build failure:
1) ios/Runner.xcodeproj/project.pbxproj: remove 10 lines referencing
ZanoWallet.xcframework + WarpApiFFI.xcframework (zcash). Those were
stale — both chains were removed in commit 4eb7db0b7 weeks ago, but
the Xcode project still listed them in BuildFile, CopyFiles, FileReference,
and Group sections. xcode error referenced them as missing.
2) Both iOS workflows now run scripts/ios/gen_framework.sh after staging
the monero_c bundle. The script reads
aarch64-apple-ios/lib<wallet>_wallet2_api_c.dylib and
aarch64-apple-ios-simulator/lib<wallet>_wallet2_api_c.dylib from the
bundle, wraps each as a .framework with an Info.plist, then bundles
device + simulator into ios/MoneroWallet.xcframework +
ios/WowneroWallet.xcframework via xcodebuild -create-xcframework.
These are exactly the paths project.pbxproj's CopyFiles phase
references.
The 'Inspect iOS targets' debug step was expanded to print bundle
subdirs + sample target contents, so if the monero_c bundle doesn't
actually ship those exact dirs we'll see it in the log.
Empty action-row buttons (Send/Receive/Swap/Scan) and empty bottom
navbar tabs are the same root cause on Linux + Android (+ likely iOS):
the workflows weren't running Cake's compile_graphics.sh, which is
what converts res/pictures/*.svg sources → assets/new-ui/*.svg.vec
precompiled vector assets that CakeImageWidget loads via
AssetBytesLoader.
History: commit 89e07fbba (Cake upstream, March 2026) moved to
precompiled SVGs, added compile_graphics.sh, and added a 'Compile
graphics' workflow step. Then 980c0278b added \*.svg.vec to .gitignore
(so the artifacts are no longer tracked). End result: CI MUST run
compile_graphics.sh to produce the .vec files, or every CakeImageWidget
silently renders nothing.
Fix:
- Add vector_graphics_compiler: ^1.1.16 to dev_dependencies (the script
invokes 'dart run vector_graphics_compiler ...' which needs the
package in the project's deps for 'dart run' to find it).
- Add a 'Compile SVG assets' step to all four workflows, between
generate_localization and the build/pod-install step.
This will populate assets/new-ui/ with ~82 .svg.vec files at build
time, so every CakeImageWidget reference (action row, navbar, top bar,
settings rows, etc.) resolves to its actual icon.
Sideloaded APK was showing Cake Wallet's original icon (white envelope
with rainbow stripes) because scripts/android/app_icon.sh copies from
assets/images/cakewallet_android_icon/ — which still held Cake's PNGs.
Regenerated all five density variants (mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi)
from assets/images/hash_wallet/02_app_icon.svg:
- ic_launcher.png (legacy non-adaptive, 48/72/96/144/192 px)
- ic_launcher_foreground.png (adaptive icon # mark layer, 108→432 px)
- ic_launcher_background.png (solid brand-green #1A5C38)
- ic_launcher_monochrome.png (themed icons API — # only on transparent)
Also overwrote assets/images/app_logo.png, cakewallet_logo.png, and
cakewallet_android_icon.png (500x500) so any consumer of those gets
the Hash Bags mark too.
File names with 'cakewallet' prefix kept on purpose — they're internal
to Cake's app_icon.sh script and renaming them would touch the script
without benefit. End users see only the rendered icon, not the path.
Next APK build from the workflow should sideload with the proper #
mark icon.
pod install fails with Encoding::CompatibilityError 'Unicode
Normalization not appropriate for ASCII-8BIT' when the shell locale
isn't UTF-8. CocoaPods' own warning tells you: 'CocoaPods requires
your terminal to be using UTF-8 encoding'.
Gitea Actions runner on macOS launches subprocesses with LANG unset,
which Ruby treats as ASCII-8BIT. Setting env-level LANG=en_US.UTF-8
and LC_ALL=en_US.UTF-8 fixes it for all steps in the job.
build_bindings.sh in bitbox_flutter unconditionally runs
'gomobile bind -target=android -androidapi 24' which produces an
api.aar — needed only for Android. The Mac runner has no Android SDK,
so gomobile fails with 'could not locate Android SDK'.
iOS doesn't need the .aar. BitBox's iOS support is a separate native
plugin under bitbox_flutter/ios/Classes/ + bitbox_flutter.podspec,
linked via flutter pub get + pod install. We just need the directory
present so the path-dep resolves.
Replace the wrapper invocation in iOS workflows with the clone steps
only; skip build_bindings.sh entirely. Android workflow keeps using
the wrapper as-is.
Auto-triggers on push were noisy — every merged PR kicked off the
linux + android + ios-sim builds. Switching to workflow_dispatch only.
Trigger via Gitea Actions UI when you want a build. workflow_dispatch
runs as the authenticated user, so secrets are always present.
Adds the push trigger back when we want pre-merge gating or CI on every
commit (e.g., release-tag automation).
The Android workflow's 'ANDROID_KEYSTORE_BASE64 not set' failure is
secrets-not-reaching-runner, not a missing secret. The secret IS set
in Builds/hash-wallet's repo secrets. The issue: workflows are being
triggered by 'pull_request' events when github-such-software/hash-wallet
is mirrored into Builds/hash-wallet via PR. Gitea (and GitHub) Actions
deliberately strip secrets from workflows triggered by PRs from forks
to prevent rogue PRs from exfiltrating secrets.
Fix: drop the pull_request trigger from all auto-build workflows. Each
workflow now runs exactly once, on push to dev/main (post-merge), in
the destination repo's context, with secrets intact.
Also added a debug step in the Android keystore decode that prints
secret LENGTHS (never values) so future failures of this shape are
diagnosable immediately.
scripts/build_bitbox_flutter.sh invokes build_bindings.sh which calls
gomobile to generate iOS bindings for the Go BitBox client. Without
Go + gomobile on the Mac runner, that step fails with 'gomobile:
command not found'.
Adds an idempotent step: install Go via brew if missing, then 'go
install golang.org/x/mobile/cmd/{gomobile,gobind}@latest' if missing.
Self-hosted runner persists $GOPATH/bin between runs, so steady-state
is zero-cost after the first install.
Same conditional-install pattern as Flutter + CocoaPods.
Android:
- monero_c prebuilt bundle doesn't ship i686-linux-android (32-bit x86
Android — essentially extinct, sim/emulators use x86_64). Was failing
loud on the missing target. Now: missing target = skip + log; only
the truly-modern ABIs (arm64-v8a, x86_64) are REQUIRED. armeabi-v7a
(32-bit ARM) is shipped if available but not required. The resulting
APK is universal across whichever ABIs got staged.
iOS:
- 'sudo gem install cocoapods' was hitting a password prompt because
the Gitea runner user has no passwordless sudo configured (rightly).
Switched to brew-first install (Homebrew lives in its own prefix,
no sudo needed on Mac). Falls back to 'gem install --user-install'
(no sudo) if brew is somehow unavailable. The whole step is still
no-op if 'pod' is already on PATH.
Avoids any confusion / potential trademark friction with HashWallet
(Spanish hardware-wallet company). Hash Bags is a more distinctive
name, harder to collide with, and we can still rank for 'hash wallet'
search via App Store / Play Store subtitle ('Hash Bags - A Wallet
for your Coins').
What changed (all user-visible strings):
- Launcher labels: android/app.properties, scripts/android/app_env.sh,
scripts/ios/app_env.sh, linux window title (CMakeLists +
my_application.cc)
- In-app text: lib/generated/i18n.dart + ARB sources under res/values/
- Brand docs: README.md, CONTRIBUTING.md, PRIVACY.md,
assets/text/Terms_of_Use.txt, assets/text/Release_Notes.txt
- hash.boats website: index.html, og-image.svg + regen og-image.png,
site.webmanifest, styles.css comment, splash/confirm/pulse/mark/
lockup svg titles, report/index.html, server/server.js
- CI workflow display names ("Hash Bags Linux build", etc.)
What did NOT change (deliberate):
- Bundle ID / Package: com.suchsoftware.hashwallet (immutable app
identity on App Store + Play Store — changing == orphaning the app)
- Binary name: hash_wallet (Linux executable)
- Dart import paths: package:hash_wallet/...
- Git repo: hash-wallet (Such-Software/hash-wallet)
- URL scheme: hashwallet:// (internal deep links)
- support@such.software (no domain change yet)
- App Store / Play Store listing names (whatever was registered there
stays; we updated launcher label only)
- hash.boats domain (still the wallet site)
Also kept the '#ash Wallet' wordplay in the nav but updated to
'#ash Bags' so the # mark still serves as the H. Nav reads:
[# mark] #ash Bags
Website hero tagline now matches the App Store sub-title concept:
'A wallet for your coins. Open-source, non-custodial, multi-chain.
Privacy-leaning by default.'
108 files modified, ~674 string replacements.
iOS workflows were broken on the Mac mini runner because Flutter
wasn't installed. Three fixes:
1) Conditional Flutter setup. Check 'flutter --version' first; only
pull subosito/flutter-action@v2 if missing or wrong version. Self-
hosted Mac runner persists SDKs between runs, so steady-state is
zero-cost. First run on the runner triggers a full SDK download
via subosito (with cache: true so subsequent fresh installs are
still fast).
2) wget → curl across all iOS workflow steps. macOS doesn't ship
wget by default; curl is always present and respects -fsSL +
redirect properly.
3) Concurrency groups on all four workflows. A burst of commits no
longer queues N copies of the same build — newer pushes cancel
in-flight runs on the same branch. Groups:
linux-<ref>, android-<ref>, ios-sim-<ref>, ios-testflight-<ref>
This addresses the '6 runs from one merge' issue: each commit in
the merge triggered all 3 push-listening workflows. Concurrency
cancels the earlier 3 the moment the newer commit's runs start.
4) CocoaPods install is also conditional (was already 'if ! command -v
pod' under the hood; just tightened the step name).
Phase 2 of iOS CI. Full signed pipeline that:
1) Decodes secrets:
- APPLE_DIST_CERT_P12_BASE64 + password → temp keychain
- APPLE_PROVISIONING_PROFILE_BASE64 → ~/Library/MobileDevice/...
- APPLE_API_KEY_P8_BASE64 → ~/.appstoreconnect/private_keys/
2) Parses the .mobileprovision for its UUID + Name (used in ExportOptions).
3) Generates ios/ExportOptions.plist dynamically with the right Team ID +
profile name + 'app-store' method.
4) Runs flutter build ipa --release --export-options-plist=...
5) Uploads the .ipa to TestFlight via xcrun altool with the API key.
6) Always cleans up keychain + temp credentials, even on failure.
Trigger: workflow_dispatch ONLY (manual). TestFlight uploads consume a
build number with every push — we don't want auto-trigger spamming
TestFlight. Bump the build number in scripts/ios/app_env.sh before
each TestFlight push (or wire a workflow_dispatch input later).
All Apple secrets (and the Trocador ones reused from Linux/Android) are
already configured in Gitea Actions.
If signing fails on first run: most likely the profile name extracted
from the .mobileprovision doesn't match what Xcode expects, OR the
distribution cert in the .p12 doesn't match what the profile pins. The
keychain identity sanity-check + ExportOptions.plist echo in the logs
will tell us which.
Phase 1 of iOS CI. Validates the Mac runner end-to-end with the
simplest possible target: an unsigned simulator .app.
Runs on the mac-mini-m2 self-hosted runner (label: macos-latest).
Mirrors the Linux/Android workflows for the shared prep steps
(torch_dart, reown_flutter, bitbox_flutter, monero_c prebuilt bundle,
configure, codegen, Trocador secrets), then does flutter build ios
--simulator --no-codesign and zips Runner.app.
Artifact: hash-wallet-ios-sim-<sha>. Unzip on any Apple Silicon Mac,
drag Runner.app into Simulator to run.
Phase 2 (TestFlight, separate file) follows after Phase 1 is green —
adds keychain setup, .p12 + provisioning profile install, ipa build,
xcrun altool upload using the App Store Connect API key. All Apple
secrets are already configured in Gitea.
The 'Inspect iOS targets in monero_c bundle' debug step will reveal
which Apple targets the prebuilt bundle ships — we'll need
aarch64-apple-ios-sim (or similar) for arm64 simulator builds on
M-series Macs. If missing, follow-up will either fetch from a different
source or build the iOS .a archives from source via Cake's scripts/ios/
build_*.sh chain (slow but reliable).
Mirrors the Linux workflow's approach — uses the Cake-Labs prebuilt
Docker image (Flutter 3.32.0 + NDK r28 + Go + Android SDK + JDK17 +
all native build deps) and the prebuilt monero_c .so bundle, so we
skip the multi-hour native cross-compile.
Stages monero + wownero .so files into android/app/src/main/jniLibs/
for all 4 Android ABIs (arm64-v8a, armeabi-v7a, x86, x86_64). Zano
libs in the bundle are ignored.
Decodes the upload keystore from secret ANDROID_KEYSTORE_BASE64,
writes android/key.properties (which build.gradle's signingConfigs
already reads), builds a universal release APK for sideload + an
AAB for Play Console. Both signed with the upload key; Play App
Signing re-signs the AAB server-side with the production key on
upload.
Required Gitea Actions secrets:
ANDROID_KEYSTORE_BASE64
ANDROID_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS
ANDROID_KEY_PASSWORD
TROCADOR_API_KEY (already set, reused from Linux)
TROCADOR_MONERO_API_KEY (same)
TROCADOR_EXCHANGE_MARKUP (same)
mwebd (Litecoin MWEB privacy) is NOT built. LTC works without it,
just no MWEB feature. Add the build step + go build pipeline if we
want MWEB on Android — separate effort.
Artifacts:
- hash-wallet-android-apk-<sha> (single universal APK, 14-day retention)
- hash-wallet-android-aab-<sha> (Play Console upload, 30-day retention)
Same root cause as the bottom navbar invisibility: iconColor was set to
theme.colorScheme.primary, which on the light theme is our dark forest
green (#1A5C38) and goes invisible on dark surfaces. onSurfaceVariant
is Material's de-emphasized-foreground token and has guaranteed contrast
against the surface across all our themes.
Two fixes:
1) lib/src/screens/seed/seed_verification/seed_verification_success_view.dart
After verifying a BTC wallet's seed, the original flow pushed the user to
the lightning_username_page which suggests an @cake.cash Lightning
address. We disabled Lightning entirely (no Greenlight infra) and the
page is meaningless without it. Collapse the conditional so the button
always pops back to the wallet list. Restore the conditional if/when
Hash Wallet ever runs its own Lightning backend.
2) lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart
inactiveColor was set to theme.colorScheme.primary, which is our brand
dark forest green (#1A5C38) post-retheme. That renders nearly invisible
against the navbar's semi-transparent dark surface backdrop, so the
three non-active tab icons (Wallets / Contacts / Apps) disappear and
the user just sees three blob shapes. Material's onSurfaceVariant is
the standard token for de-emphasized icons and is visible across our
light/dark/black themes.
The source-of-truth pubspec template — gitignored pubspec.yaml is
regenerated from this on each configure.dart run, so the dep would have
reappeared on next build. Follow-up to a8e071a05/4c09950e0.
flutter_mailer has no Linux implementation (throws MissingPluginException)
and on other platforms required users to have a configured mail client.
Vanishingly few error reports ever reached anyone. The 'Send' button on
the error popup was effectively a no-op.
Replaced with a url_launcher call that opens a prefilled GitHub issue at
github.com/Such-Software/hash-wallet/issues/new with the error trace
inlined into the body (truncated to ~6.5KB to stay under GitHub's URL
length cap; full log path is shown in the body for users who want to
attach it manually). Adds a 'bug,from-app' label and a short prompt
asking the user to describe what they were doing.
Downsides: people without a GitHub account will get nothing useful when
they hit 'Send' — they'll see GitHub's sign-up page. Net still better
than the silent no-op we had. Adding a real reporting endpoint
(Cloudflare Worker) is the obvious follow-up if low report volume
turns out to matter.
Also removed flutter_mailer from pubspec.yaml — was only used here.
The .selected computed getter always called monero!.getCurrentAccount(_wallet),
which does an internal 'wallet as MoneroWallet' cast and threw
'WowneroWallet is not a subtype of MoneroWallet' whenever the dashboard
CardsView Observer rebuilt for a Wownero wallet. The .accounts getter
above already dispatched correctly by wallet.type; .selected didn't.
Now matches the same monero/wownero dispatch pattern.
Stragglers that weren't in any agent's batch but referenced the removed
identifiers:
- tool/configure.dart: stripped 'backupSeeds(Box<HavenSeedStore>)'
abstract method from generateWownero's emitted template + the
haven_seed_store.dart import. CI regenerates lib/wownero/wownero.dart
from this template each build, so the previous lib/wownero/wownero.dart
cleanup got reverted on rebuild. This is the actual fix.
- lib/core/trade_monitor.dart + lib/view_model/trade_details_view_model.dart:
removed JupiterExchangeProvider import + ExchangeProviderDescription.jupiter
switch cases (provider + URL builder).
- lib/src/screens/dashboard/dashboard_page.dart: deleted _showHavenPopup
helper + its call site + the haven_wallet_removal_popup import.
- lib/src/widgets/haven_wallet_removal_popup.dart: deleted (no callers).
- lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart:
removed the zcash 'missing funds' InfoCard and the decred 'syncing'
InfoCard widgets, plus their _dismissZcash and _rescanInternalChangeZcash
handlers.
- lib/src/screens/settings/connection_sync_page.dart: removed the TronGrid
history toggle row.
- lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart:
_isTokenSelectionFlow now only checks evmNetworkSelection (was also
branching on solana/tronTokenSelection enum values that no longer exist).
Generated .g.dart files for dashboard_view_model and connection_sync_view_model
still reference removed methods (rescanInternalChangeZcash, dismissZcash,
canUseTronGrid, setUseTronGrid). Those will be regenerated by build_runner
on next CI run and the methods will fall out — no manual fix needed.
94 files changed, -5926 net lines. No stubs, no generators, no dead enum
values, no orphan switch branches. Removed for principled reasons (we
do not ship these chains) and to make the codebase smaller and friendlier
to contribute to.
What's gone:
- lib/{solana,tron,zano,decred,zcash,haven}/ directories
- lib/exchange/provider/jupiter_exchange_provider.dart (Solana-only)
- lib/entities/haven_seed_store.dart
- WalletType.{solana,tron,zano,decred,zcash,haven} enum values
(HiveField IDs are pinned: Wownero stays at 12, all surviving types
keep their IDs, so existing user-saved wallets deserialize unchanged)
- All ~70 lib/ files that imported the removed chain stubs or branched
on the removed WalletType values
- backupSeeds(Box<HavenSeedStore>) abstract method + Wownero implementation
- Jupiter exchange branches in send/exchange view models
- TronGrid usage settings, Zano alias lookups, Zcash transparent address
handling, Decred default-spv-nodes branches, Haven seed migration
What stays (intentional):
- CryptoCurrency.{xhv,sol,trx,dcr,zano,zec} enum entries in cw_core —
they're still used as exchange/swap currency identifiers in
address_validator.dart and elsewhere. The wallet types are gone but
the *currencies* are still tradeable through Trocador.
- cw_core/spl_token.dart and cw_core/tron_token.dart Hive adapters in
main.dart — the tokens still register as Hive types for backwards-
compat with any old box data.
- lib/buy/{moonpay,robinhood,onramper,dfx,kryptonim,meld}/ provider
source files (just stripped of solana/tron/etc references; providers
themselves remain dormant per provider_types.dart).
Pinned-ID note: WalletType enum now has gaps in @HiveField IDs (4, 10,
11, 13, 14, 18 removed). This is deliberate — re-using a freed ID later
would mis-deserialize old hive boxes. Don't renumber.
Bundled config / CI hasn't changed; configure.dart was already free of
these chains in commit 4eb7db0b7.
CONTRIBUTING.md explains the fork relationship, branch model, generated-
code workflow, and what changes we will and won't accept (no re-enabling
Solana/Tron/Zano/Decred/Zcash, no Cake-domain endpoints, no Lightning
yet, no telemetry).
README.md: removed the stale ./scripts/prepare_zcash.sh reference (file
doesn't exist) and added a link to CONTRIBUTING.
.gitignore: untrack docs/UPSTREAM_SYNC.md (internal merge-strategy
playbook, kept locally alongside docs/links.md).