Commit Graph
321 Commits
Author SHA1 Message Date
jwinterm dd449f2594 Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
2026-10-24 23:49:29 -04:00
046e57c598 Add Robinhood Chain (#3398)
* fix android CI

* feat: Add Robinhood Chain, all major flows done. minor issue now is our providers don't support it yet as its still pretty early, two weeks from launch

* feat: Add Robinhood Chain, all major flows done. minor issue now is our providers don't support it yet as its still pretty early, two weeks from launch

* Cleanup

* auto-reformat

* fix ui issue and contact listing for tokens

* fix: missing icons and crash for fee priority in settings

* chore: cleanup

* fixes from feedbacks

* fix: review issues and sync to latest dev
feat: switch history api to etherscan, add history toggle to privacy settings, exclude robETH from exchange pickers, skip tokens discovery for chains that moralis hasn't indexed, add tests

* fix: address Robinhood review comments

* fix: show Solana token icons in history

* fix: address review comments and default Robinhood to PublicNode

* fix:  revert filter and refuse unmapped networks in provider

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
2026-10-04 13:22:05 +02:00
cyanandGitHub 77e4b946eb chore: migrate to hosted scalable CI (#3620) 2026-10-03 15:41:27 +02:00
jwinterm d74a6aeeb6 ci(android): name the APK and AAB after the version they carry
Flutter writes app-release.{apk,aab} for every build, so each release's
artifact unzips to the same filename. A stale 1.0.3 app-release.aab in
Downloads was uploaded to the 1.0.4 Play release and refused for reusing
versionCode 13. Copy the outputs to hash-bags-<version>-build<N>.* from
pubspec_description.yaml and upload those, refusing if the version line
does not parse.
2026-10-02 13:42:11 -04:00
jwinterm b07adfff5b ci(macos): port the prerequisites instead of guessing them, and stop printing secrets
The first macOS run died after 88 seconds on `cd scripts/monero_c: No such file
or directory`. That was the cheap failure. The workflow had been written fresh
rather than ported, and behind that missing clone it had also skipped the step
that pins the crypto salts. Had it got further it would have shipped a build
that mints random salts, which is the 2026-08-29 corrupted-seeds incident on a
new platform: wallets created on it would not decrypt after the next upgrade.

So the prerequisites are now ported step for step from build-linux.yml, the
desktop build known to pass: torch_dart, reown_flutter, bitbox (the Apple
variant, without the gomobile Android bindings), prepare_moneroc.sh, configure,
secrets generation, salt pinning with the same refusal, Trocador injection,
codegen, localization, and compile_graphics.sh. curl for wget and BSD sed's
-i.bak form, because the runner is a Mac.

Two more things the fresh version got wrong, found while porting:

configure was run without sourcing app_env.sh, and app_config.sh writes
CFBundleName, the bundle id and the version into Info.plist from variables only
app_env.sh sets. The app would have shipped with an empty name. The step now
prints the three values it wrote.

And app_env.sh hardcoded 1.0.0 (1) on macOS, 6.1.0 (79) on Linux and 1.9.0 (29)
in the fish variant, the last two being Cake Wallet's own version numbers. On
macOS PlistBuddy writes that into the bundle, so every macOS build would have
called itself 1.0.0. Linux only escaped because its substitution looks for a
version: 0.0.0 placeholder that pubspec_description.yaml does not contain. All
three now derive from pubspec_description.yaml the way Android already does, and
the two sh variants resolve to 1.0.4 (14). The fish one is unverified: fish is
not installed here and CI does not use it.

Separately, four workflows verified their secret injection by grepping
lib/.secrets.g.dart into the log, which prints the values. Android's printed the
Trocador, WalletConnect, MoonPay and exchange-helper keys. Gitea likely masks
registered secrets, but masking is a best-effort layer and values never belong
in output. Each now reports every key's name and length and nothing else.

Windows is not in this commit. Its run refused correctly, on the guard added in
c11c79b0c: the runner has Flutter 3.38.5, not the pinned 3.32.0. That is fixed on
the runner, not in the workflow.
2026-09-26 11:16:53 -04:00
Blazebrain 63d67da5f7 fix: fail the integration tests check on branches without the test suites 2026-09-25 18:55:42 +01:00
Blazebrain 3a31bbacab fix: skip integration tests on branches without the test suites 2026-09-25 18:49:11 +01:00
Blazebrain 45e06ec9d4 fix: flaky integration test runs on the CI emulator 2026-09-25 17:20:44 +01:00
malik1004xandGitHub 0d14b57ee2 add commit/branch overlay for ci builds (#3653)
* add commit/branch overlay for ci builds

* add ability to show/hide overlay

* add alert when disabling the overlay
2026-09-24 23:01:05 +02:00
David AdegokeandGitHub dfa5165766 Integration tests (#3477)
* fix android CI

* feat: Add integration test harness and new-ui test keys

This change:
- Adds a BaseRobot with bounded waits to replace hard sleeps in integration tests
- Adds AppLauncher, TestConfig and TestWallets as the core test harness
- Adds onboarding flows composing the existing onboarding robots
- Adds robots for the new dashboard, home page and lightning username page
- Adds a tier0 onboarding create suite targeting the new UI
- Applies the declared navbar action keys that were never attached to the InkWell
- Fixes AlertWithOneAction dropping its buttonKey since the alert redesign
- Adds stable ValueKeys to the home page action row, wallet name, top bar and sync bar
- Makes the legacy tap and text entry helpers wait for widgets instead of sleeping

* fix: Test secrets drift and integration test runner parameterization

This change:
- Adds missing test wallet seed and receive address entries to the secrets generator
- Fixes trailing space typos in two secret key names
- Adds bloc_test to pubspec_base.yaml so pubspec regeneration keeps it
- Parameterizes the integration test runner with suite dir, tier, platform and device knobs
- Fixes the runner aborting on first failure and misaligning durations after retries
- Adds adb data reset so Android suites start from a fresh install

* feat: Android emulator integration test workflows for dev PRs

This change:
- Adds a reusable integration test workflow building the app with prebuilt native deps and running the suites on an Android emulator
- Adds the PR gate workflow triggering fund-free tier0 suites on pull requests into dev
- Fixes the old workflow discarding the test runner exit code so failures now fail the job
- Removes the disabled automated integration test workflow on the stale flutter image
- Removes the commit-message gated Linux desktop test steps, unit tests stay
- Removes the vestigial emulator config from the reusable build workflow

* feat: Revive fund-free integration test suites for the new UI

This change:
- Adds tier0 suites for restore, seed confirmation, wallet switching, receive address, settings navigation and the show keys auth gate
- Adds tier1 suites for sync status, swap quotes and transaction history running non-blocking on PRs
- Adds robots for the settings, receive, swap and send sheets
- Adds auth, wallet switching and restore flows
- Applies widget keys to settings rows through their route names, they were declared but never attached
- Adds keys to the swap amount fields, send inputs, confirm swiper and history tiles
- Adds an integrationTest wrapper containing background async app errors that are not test failures
- Makes taps wait out running route transitions before hitting the screen
- Adds a watchdog to the test runner so a wedged driver fails bounded instead of hanging
- Removes the old UI suites, robots, helpers and the stale widget_test counter template

* chore: Add integration testing docs and suite template

This change:
- Adds docs/INTEGRATION_TESTS.md covering the architecture, local runs, tier rules and the flakiness playbook
- Adds a copy-paste suite template teammates start new feature tests from

* feat: Manual funds integration test workflow with funded-chain auto-discovery

This change:
- Adds a funds suite sending a real self transaction on every funded chain
- Adds a funds suite creating a real swap and broadcasting its deposit
- Discovers funded chains from the funded wallet seeds map, adding a seed to the secret adds the chain
- Adds the manual workflow dispatched from the Actions tab with flow, chain and recording inputs
- Gates the run behind the funds-tests environment and posts the result to slack
- Keeps funded seeds out of PR builds, the checked in seed map is empty

* fix: Harden integration test harness from review findings

This change:
- Fixes body failures being swallowed by the guarded test zone, failing suites hung until the watchdog instead of failing fast
- Fixes multi chain CHAINS dispatch values being split apart by the define forwarding
- Fixes the swap limits wait timing out on pairs without a provider minimum
- Makes unknown chain names in CHAINS fail with a readable message
- Keys favorite token rows by index, duplicate token symbols crashed on duplicate keys
- Guards the recording encryption so a keyserver flake cannot eat the test result
- Removes the dead security and backup robot and corrects the screenshot claim in the docs

* chore: Hold two funded seeds per chain in the funded wallets map

This change:
- Changes the funded wallet seeds map to a list of seeds per chain
- Keeps the first seed as the primary for sends and swaps, the second is reserved for cross wallet scenarios

* feat: Try each funded wallet per chain until one has a balance

This change:
- Adds a funds flow restoring a chain's funded wallets in turn and using the first with a spendable balance
- Treats a wallet that finishes syncing while still empty as drained so the next seed gets its turn quickly
- Fails a chain with a top up message when every funded wallet of that chain is empty

* reformat integration test related files

* fix: Run the integration test emulator on the kvm capable runner

This change:
- Moves the test job to the linux-amd64 runner the old emulator workflow targeted, the android builders have no /dev/kvm
- Exposes /dev/kvm through a docker device flag instead of a volume, a bind mount materializes a directory when the host node is missing
- Fails the kvm check in seconds with a clear error instead of waiting out the emulator boot timeout

* fix: Queue the integration test job on the live android runner pool

This change:
- Moves the job back to the android runner labels, no runner carries the old linux-amd64 label anymore so the job queued forever
- Keeps the kvm device flag and check so a host without kvm fails immediately with a clear error

* feat: Run the integration test gate on github hosted runners

This change:
- Moves the test job to ubuntu-latest which has working kvm, the self-hosted android builders do not expose /dev/kvm yet
- Runs directly on the vm since the cake container image does not fit hosted disk, flutter is pinned through the flutter action
- Adds rust android targets for the breez cargokit build and java 17 for gradle
- Keeps the prebuilt native deps from the ghcr docker cache, they pull fine on hosted docker
- Adds pub, gradle and flutter caching plus a disk cleanup step
- Keeps every input, the runner script, tier1 soft run, recording and artifacts unchanged

* fix: Install flutter from the same source as the ci container image

This change:
- Clones the flutter 3.41.9 tag directly like the container image instead of using the release archive
- The archive ships a stricter dart that rejects the checked in mweb ffi bindings, the tag clone compiles them
- Caches the cloned sdk between runs

* fix: Regenerate the mweb ffi bindings with the workflow dart

This change:
- Regenerates cw_mweb/lib/generated_bindings.g.dart after the deps step, the docker deps builder generates it with an older dart whose ffigen output the current compiler rejects

* fix: Pin ffigen so fresh resolutions generate compliant mweb bindings

This change:
- Pins ffigen to ^20.1.1 in cw_mweb, the unconstrained dependency resolved to 7.1.0 on fresh checkouts
- Old ffigen emits ffi classes without the base or final modifiers the current dart compiler requires

* fix: Restore the pinned cw_mweb pubspec before regenerating bindings

This change:
- Restores cw_mweb/pubspec.yaml after the deps step, the deps image rsync clobbers it with the stale unpinned copy so the ffigen pin never applied
- Wipes the rsynced pubspec.lock and .dart_tool so pub resolves the pinned ffigen fresh
- Asserts the regenerated bindings carry class modifiers so a bad generation fails in seconds
- Skips the libclang install when it is already present, apt spent fourteen minutes on it

* fix: Run the emulator test logic from a file, the action splits script lines

This change:
- Moves the whole emulator script into a file written before the emulator step, the action executes every script line as a separate sh command so multi line logic broke with a syntax error
- Passes the action a single line script invoking that file with bash

* fix: Give the ci emulator a real phone profile

This change:
- Sets the avd to the pixel 5 profile, the default 320x640 skin cannot fit the seed word grid so every wallet creation suite failed its seed display checks
- Caps tier0 attempts at ten minutes, two failing suites at the thirty minute worst case blew the job timeout

* fix: Wait for pages on the ci emulator instead of asserting immediately

This change:
- Makes the legacy page assertion poll for the page, the wallet keys page arrives after the ci emulator finishes unlocking the wallet so both auth gated suites failed on a race
- Raises the tier0 attempt timeout, the first attempt for each suite compiles that target's kernel and was being killed then passing on retry
- Raises the job timeout to fit the per suite compiles

* fix: Recover from driver attach failures instead of burning the timeout

This change:
- Detects attempts where the flutter driver never attached to the app and retries them without spending the real retry, restarting adb first
- Lowers the ci attempt timeout back to ten minutes, every passing attempt finishes within four so a longer cap only makes an attach failure cost more

* fix: Bound the adb recovery and give up when the driver never attaches

This change:
- Bounds every adb call in the recovery path, adb wait-for-device blocks forever when the emulator is gone and silently ate a whole ci job
- Aborts the suite when adb cannot be brought back instead of retrying against a dead device
- Stops the run after a few driver attach failures, an environment that never attaches will not fix itself
- Dumps logcat after every failed attempt so a failure to start the app leaves evidence behind

* fix: Stop the runner failing when every suite passes

This change:
- Assigns the result arrays empty instead of only declaring them, set -u treats a declared but never assigned array as unbound so the summary died on the first run where nothing failed

* fix: Make the ci build cacheable and keep tier results when the runner dies

This change:
- Splits the flutter and pub caches into restore and save steps, actions/cache only saves when the whole job succeeds so they were never written and every run paid for a cold build
- Lets setup-gradle write its cache, it is read only on branches other than the default one
- Caps the gradle heap before the test phase, the six gigabyte daemon plus the emulator left the runner too little to stay alive
- Runs tier0 and tier1 as separate emulator steps so each uploads its logs before the next can take the runner down
- Logs memory and disk around each tier
- Documents how ci runs the suites and the two environment quirks that only bite there

* fix: token decimals defaulting to zero and breaking amount parsing in solana

(cherry picked from commit c220bbb46df9468faf9a8967cb0ef672af7cc664)

* fix: Install the test error handler after the app boots and drain benign errors

This change:
- Installs the test error handler after main() instead of before it, the app replaces FlutterError.onError while booting so the handler installed earlier never ran
- Replaces the app's async error handler during tests, it returns at its first await without recording anything so the framework asserted instead of reporting the real error
- Drains background errors the app tolerates at runtime from the wait loops, anything unrecognised still fails the test where it happened
- Collects the tolerated categories in one place, layout and semantics assertions from debug builds plus the transient network failures exception_handler.dart already ignores

* fix: Import FlutterExceptionHandler from foundation so the test build compiles

This change:
- Imports package:flutter/foundation.dart in the launcher, material.dart only re-exports Brightness and UniqueKey from foundation so FlutterExceptionHandler was not in scope
- Drops the material.dart import, every symbol the launcher uses comes from foundation

* chore: Drop the funds-tests environment from the funds workflow and docs

This change:
- Removes the environment input from the funds workflow, we no longer use a funds-tests GitHub environment so passing it would only auto create an unprotected one on the next dispatch
- Says plainly in the docs that nothing gates the dispatch, the doc still claimed the run waits for environment approval
- Names write_funds_secrets as the one thing keeping funded seeds out of a PR build, that used to be backed by environment scoping too

* chore: Trim the integration test comments down to the ones that carry weight

This change:
- Drops the doc comment sitting on top of every robot primitive, flow and config getter, they restated the method name and nothing else
- Collapses the multi paragraph docstrings on the launcher, the error drain and the funded wallet lookup into the one or two lines that actually explain why the code is shaped that way
- Removes the file and class header paragraphs, the funded wallets stub keeps a short note because an empty map needs one
- Keeps the notes worth having, the chain quirks in the onboarding flow, the route transition tap, the dust thresholds, the void attempt handling in the runner

* chore: Fold the components folder into core and clear out the leftover comments

This change:
- Moves common_test_cases.dart into core, the components folder was down to that one file after common_test_constants.dart went
- Drops six methods from it that nothing calls, dragUntilVisible alone was 95 lines of scroll logic with no caller
- Removes the comments narrating the line below them across the older robots, "Drag to the left" above a moveBy and "Confirmation for buttons 1-9" above the loop
- Deletes the commented out body of takeScreenshots and says instead that it does nothing, all fifteen callers have been no-ops
- Notes in the docs that new robots extend BaseRobot while the older ones still carry a CommonTestCases

* chore: Clear out pending items

* refactor: Move the onboarding robots onto BaseRobot and drop the sleeps

This change:
- Deletes common_test_cases.dart, the sixteen robots that held a CommonTestCases now extend BaseRobot like the newer ones do
- Moves the primitives those robots needed onto BaseRobot with the same behaviour, isSpecificPage, hasType, hasValueKey, hasText, hasTextAtLeastOnce and swipePage
- Renames each robot's page check to isDisplayed so every robot answers the same question the same way
- Removes the twenty six defaultSleepTime calls, every one of them sat in front of an action that already waits for what it needs
- Waits for the view model to move to the next word in seed verification instead of sleeping a second and hoping, and fails with a message when it does not
- Settles after opening a tab on the wallet keys page, the assertions there read the tree straight after the tap
- Drops the fifteen takeScreenshots calls, the method's body has been commented out the whole time so none of them did anything
- Formats the three files dart format was still rewriting

* fix: Wait out the navigation the removed sleeps used to cover

This change:
- Settles after a page mounts in isSpecificPage, the route being replaced stays in the tree while it animates out and assertions on text were matching both screens, seed_confirmation_test failed on finding two "Verify Seed" widgets
- Settles after the last pin digit, the gate sends itself away and returning early landed the caller's next navigation on a locked navigator, wallet_switching_test failed on the !_debugLocked assertion

Both are places where waiting for a widget to exist is not the same as waiting for the app to
be idle, which is what the sleeps were quietly doing.

* fix: Recognise every way a flutter drive attach fails and give tier1 a retry

This change:
- Matches the other wordings flutter drive uses when the driver never attaches, we had only the connect one and missed the isolate initialise, the service disappeared and the driver extension variants
- Gives tier1 the same single retry tier0 has, running it with none meant a driver that never attached was reported as a failing test

Two of the three tier1 failures on the last run were attaches that never happened, neither
of them reached any test code.

* fix: Sync status test watched the wrong status class, history scroll had no guard

This change:
- Accepts SyncronizingSyncStatus in the sync status suite, cw_core spells it without the h and it is a separate class from SyncingSyncStatus, so chains reporting it sat out the full three minute timeout and then failed saying the wallet never synced while printing Synchronizing
- Waits for the home scroll view before handing it to scrollUntilVisible, which calls single on the finder every drag and reported a missing scroll view as a bare Bad state: No element
- Skips the scroll entirely when the history tiles are already on screen

* fix: Test the transaction history the way the screen actually works

The suite looked for history tiles on the home page and scrolled for more when it found
none, which could never pass. The home page renders the history under a tab, and for a
wallet with tokens the assets tab is the one selected first, so the history section was
never built. It also shows a three item preview, itemsShort caps it at
shortHistoryLength, and everything else sits behind the All button in a modal.

This change:
- Opens the history tab before looking for tiles, wallets with no assets tab render the history with no tab bar at all and skip that step
- Asserts the preview never renders more than the three the view model hands it, instead of scrolling for tiles that were never coming
- Opens the All button and checks the full list in the modal, which nothing covered before
- Adds keys for the tab items, the All button, the single tab history bar and the modal root

* fix: Stop the runner wiping a real wallet install and document what CI does that the docs did not

Following app_config.sh and then running the tests locally clears com.cakewallet.cake_wallet
between every suite, which is a developer's own wallets. CI never hits this because the
workflow renames the app afterwards, a step that only existed in the workflow.

This change:
- Refuses to clear a package that is not named for testing, with the rename command in the message
- Adds that rename to the local prerequisites, next to the reason it matters
- Spells out the assets/images vector pass, compile_graphics.sh does not cover that folder and those vec files are neither committed nor ignored, so a fresh checkout fails on the welcome screen
- Notes that a second Gradle daemon from Android Studio deadlocks the build

* test: Make the tier0 and tier1 assertions check what they claim to

An audit of every robot against the screen it drives turned up assertions that pass without
telling you anything, the same shape as the transaction history suite looking for tiles on a
tab it never opened.

This change:
- Reads the receive address off the screen instead of the view model, the view model holding the right address says nothing about what the user is told to send to, and reads both the chunked and the plain render because only handling the first comes back empty
- Verifies a restored wallet knows the address its seed derives, restoring the wrong wallet from a correct seed used to look the same as success, and this found two stale addresses in the secrets
- Replaces the sync bar check, the key it asserted sits on a widget that renders for every status including none, so it was true whenever the home page was up
- Requires the All transaction view to render more than the home preview, it could not tell the two apart before
- Fails the keys page check when a wallet type matches none of its branches instead of passing having verified nothing

* refactor: Remove outdated items from integration tests

* test: Wait for the wallet to actually change instead of pumping a fixed half second

Tapping a wallet row does nothing when that wallet is already open, and the load runs behind
a progress overlay, so a tap that went nowhere looked the same as a successful switch.

This change:
- Waits for the app store to report the wallet before returning, and fails with a message when it never does

* test: Open wallet groups before looking for a wallet by name

Wallets that share a seed are listed as children of their group's tile and are not on screen
while it is closed, so switching to one would time out looking for a name that is only
rendered once the group is opened. Single seed wallets carry their own name as the tile
title, which is why this has not come up yet.

This change:
- Opens the group tiles when the wallet is not already on screen

* chore: Keep the receive address widget diff to the key it needed

A formatter pass rewrote the whole file, quotes, body style and trailing commas, for what was
a one line addition. Restored the original shape so the diff shows only the key.

* chore: Drop trailing whitespace left by the comment cleanup

* test: Cover the send screen refusing what it cannot send

Nothing covered the send screen, which is the one place in the app where getting it wrong
costs the user money. The property worth holding is that the swiper, the point of no return,
is only ever offered for a transaction the wallet actually built.

This change:
- Adds a tier0 suite driving an empty form, an address the chain cannot parse, and a well formed address on a wallet that has never held anything, and requires that none of the three ever produce a swiper
- Waits for the swiper and requires it not to arrive rather than checking once, the screen passes through syncing, building and failed states on the way and any of them can be the one you catch
- Notes in the docs that a merge from dev leaves the generated chain proxies and the assets/images vec files behind, which has broken the local build three times now

* test: Add send and swap dry runs and a flows mode that never spends

The funds suites were all or nothing: the only way to find out whether a funded wallet still
works was to spend from it. Almost everything worth checking happens before the swipe, the
wallet finds its balance, prices the fee and builds a transaction, or the provider returns a
live quote, and none of that costs anything.

This change:
- Adds a send dry run that stops at the swiper, reaching it means the transaction was really built
- Adds a swap dry run that stops before the swap button, creating a trade registers an order with the provider even though no funds move, so it goes no further than the quote
- Adds a dry-run value to FLOWS that runs both and neither of the spending suites, and makes it the default so a mis-click cannot send anything

* test: Cover a wallet name that is already taken being refused

Two wallets answering to the same name is how someone ends up sending from the wrong one, and
nothing checked that the form stops it.

This change:
- Adds a tier0 suite that creates a wallet, starts a second one and gives it the same name, and requires the form to refuse it and stay put
- Splits the walk to the naming form out of createAdditionalWalletFromWalletList so a suite can stop there instead of going through with the wallet

* test: Cover a seed the wallet cannot parse restoring nothing

Restoring from a seed the wallet does not understand either fails outright or, worse, derives
some other wallet the user has no keys for. The restore button is disabled until the words
check out and nothing checked that.

This change:
- Adds a tier0 suite that types twelve words outside the wordlist, presses restore, and requires the form to stay put
- Follows it with the real seed in the same run, so the refusal above cannot pass just because the form restores nothing for any seed at all
- Splits the walk to the restore form out of restoreFirstWalletFromSeed so a suite can stop there

* refactor: Make spending its own switch instead of a value hidden in flows

flows says which flow to run, so folding a dry-run value into it made the dropdown answer two
questions at once and buried the one that matters.

This change:
- Adds a spend input, off by default, and gates the broadcasting suites on it
- Puts flows back to all, send or swap
- Runs the dry runs whenever their flow is picked, they cost nothing either way

* chore: cleanup minor issues

* test: Cover one seed backing wallets on two chains

Wallet groups had no coverage at all, and the part that would break quietly is the seed: a
group member that derived its own seed instead of sharing one looks identical from the wallet
list. It is also the only way a wallet ends up inside a collapsed group tile, which is the
path switchToWallet handles and nothing exercised.

This change:
- Adds a tier0 suite that creates a solana wallet, adds an ethereum wallet to its seed, and requires both to report the same seed
- Switches back to the first afterwards, which only works by opening the group it now sits in
- Adds the flow for it, a child wallet skips the seed and verification screens for a page explaining it shares one, so it cannot reuse the normal creation steps
- Adds the one key the group page was missing, its next button

* test: Cover changing the pin replacing the one that unlocks the wallet

Nothing checked that changing the pin actually takes. Watching the setup screen close proves
nothing, the only evidence is the new pin opening something the old one guarded.

This change:
- Adds a tier0 suite that changes the pin and then unlocks the seed and keys page with the new one
- Goes through the pin gate on the way in, so it also covers the current pin being required before the change is allowed

* test: Cover changing the language, and make settings rows and pickers addressable

Writing this turned up two things in the app rather than the test.

Only ListItemRegularRow was given its keyValue as a widget key. Toggles, checkboxes,
dropdowns and selectors all took the same keyValue and dropped it, so every settings control
that is not a plain navigation row could not be found at all.

Picker named three item types and gave everything else an empty string, so a picker over
plain values, the language list among them, rendered every row with the same key.

This change:
- Passes keyValue through as a key for the other four row types
- Falls back to the item itself for the picker name, so each row is distinct
- Adds a tier0 suite that changes the language and requires the row to show the new one

* test: Cover switching the node the wallet talks to

Which node a wallet is pointed at decides what it sees, and a switch that quietly does not
take leaves someone on a node they believe they left. Nothing covered it.

This change:
- Adds a tier1 suite that reads the current node, switches to another one, confirms, and requires the wallet to be pointed somewhere new
- tier1 rather than tier0 because opening the page speed tests every node against the network

* test: Cover the address book keeping the address it was given

Pasting an address once and trusting it afterwards is the whole point of an address book, so
an entry that comes back changed is a way to lose money quietly. Nothing covered it.

This change:
- Adds a tier0 suite that saves a contact through the form and requires the stored address to match what was typed
- Checks the saved record rather than the rows, the list filters what it shows by the currency in play so a contact can be saved and correctly not on screen
- Adds keys for the add button, the name field, the currency picker, the address field and save

* fix: Key picker rows on a name that does not change with the language

Enumerable items fell through to toString, which is translated for some of them, so the
same row carried a different key depending on the language in use.

* test: Cover the fiat currency setting, transaction details and sending to a contact

This change:
- Adds a suite for turning the fiat api off and on, which is what decides whether the
  currency setting is offered at all, then changes the currency through it
- Adds a suite that taps a transaction and checks the details that open belong to it
- Adds a suite that saves a contact and picks it from the send screen
- Adds a suite for restoring a 25 word monero seed, which takes a seed type and a restore
  height the polyseed path never asks for
- Keys the address book button on the send screen so a test can reach it
- Lets SUITE_DIR take a single suite file, for iterating on one test
- Lists every suite and what it proves in the integration tests doc

* test: Cover renaming and deleting a wallet

This change:
- Adds a suite for renaming a wallet, including the refusal when the name is already held
  by another wallet
- Adds a suite for deleting a wallet, checking it leaves storage and not only the list
- Keys the edit button in the wallet list and the name field, the two buttons and both
  dialogs on the wallet edit page, none of which could be reached from a test before
- Fixes the wallets list robot, which looked for a single Wallets title even though the
  navbar carries the same word, and looked for wallet names across the whole tree even
  though every dashboard tab stays mounted in an IndexedStack

* ci: Report every integration test run to slack

This change:
- Posts a report to slack on every run, naming each tier's counts, duration and any suite
  that failed, with the full list of what passed in a reply on the same message
- Adds a SUMMARY_FILE knob to the runner, which writes what it counted in a form the
  report can be built from rather than parsing the run log
- Skips with a notice when SLACK_APP_TOKEN or SLACK_TESTS_CHANNEL is missing, and never
  fails the gate when slack is unreachable
- Leaves the funds workflow alone, the new slack_notify input defaults to off

* chore: Removing unneeded items

* ci: Say which slack scope was missing when a report is rejected

A rejection carries the scope it wanted in a field of its own, and dropping it left the
warning saying missing_scope without saying what to go and add.

* ci: Report funds runs to the tests channel and say when slack refuses them

This change:
- Sends the funds report to SLACK_TESTS_CHANNEL, so both test workflows land in one place
  and the apk channel is left to builds
- Checks the slack response, the post was going out unchecked so a rejected report looked
  like a delivered one
- Builds the message from the environment instead of interpolating workflow inputs into
  the shell, a quote in a dispatch input was enough to break the payload

* ci: Give the test reports their own slack app

Uploading apks and reporting test results are different jobs with different scopes, and
the name on a message is how anyone in the channel tells which one sent it. Both test
workflows now post through SLACK_TESTS_TOKEN, which only needs chat:write, and the apk
uploads keep SLACK_APP_TOKEN and its file scopes.

* fix: Restore the swap amount key and say when a suite never ran

The AnyPay redesign moved the deposit amount field into SwapAmountBox and it lost the key
the swap suite enters an amount through, so that suite could no longer reach it. Both
amount fields are keyed now, deposit and receive.

This change also:
- Records whether a failed suite failed on its own or because the driver never attached,
  which is what happened to send_validation_test in run 31364715120 where the suite never
  actually ran
- Rebuilds the slack report out of blocks, so the status is a heading, the tiers sit in
  their own fields, failures name their reason and the run is a button rather than a bare
  url

* perf: End an attempt early when the driver cannot attach

flutter drive says it is struggling to attach within about a minute and then, when it is
really wedged, sits there until TEST_TIMEOUT kills it. Run 31364715120 lost 20 minutes to
two attempts doing exactly that.

This change:
- Watches the attempt log and ends the attempt VOID_GRACE seconds after the driver first
  reports trouble, 120 by default, so the retry starts on a fresh app instead of waiting
  out the remaining eight minutes
- Starts that clock at the first complaint rather than at the attempt, since the attempt
  spends its first minute building and installing
- Moves the marker list into one variable, the watchdog and the after the fact check were
  going to drift apart otherwise

* perf: Drop the driver grace to 60s and take the whole drive down with it

Measured against run 31364715120: the 19 attempts that attached did so in under a second,
while the two that never attached had the warning out at 5 seconds. 60 leaves a wide
margin over a healthy attach on a runner that was already running slow, and saves another
minute on each dead attempt.

Ending the attempt now walks the process tree rather than signalling the wrapper. On linux
the drive runs under timeout, and a KILL to that cannot be forwarded, so the drive would
have been left holding the device just as the retry went looking for it.

* trigger another test run

* Triggering another run, its been green so far

* fix: Wait for the wallet to be ready before tapping send in the funds suites

The send button is disabled until isReadyForSend, which wants a synced wallet, but the
funds flow moves on as soon as a balance appears. Every funded chain was therefore tapping
a disabled button and then waiting ninety seconds for a confirm sheet that could never
open. The robot now waits on the same value the screen gates on, so a wallet that never
syncs says so instead of looking like a missing widget.

* test: Make a funds send failure say what went wrong and stop it spreading

A send that never reaches the confirm sheet was reported as a missing ConfirmSwiper, which
says nothing about the cause. It now reports the state of whichever sheet is up, what the
wallet holds, and the build error when the chain rejected the transaction.

Recovery between chains was a single pop, so the first chain to fail left the app somewhere
the next chain's restore could not start from and every chain after it failed too. It now
unwinds until the home page is back and stops the suite if it cannot get there.

* test: full testing of all the funded tests and fix issues that came up with them

* fix: update sync key to fix sync test

* fix: out of space error

* fix: integration test review issues

* fix: merge conflicts and update test suites

* fix: false test passes and add funded wallet passphrases
chore: remove unused integration test robot methods
feat: save integration test screenshots on Android
feat: pick the swap pair for the funds dispatch

* fix: Keep the runner output on the step log through a file descriptor

* fix: timeout because of runner crash

* test: handle new seed ui flow and fix hang for runner

* fix: emulator teardown not rendering succesfully
feat: use currency picker for selecting swap options for funded run selected swap currency

* test: add key for compact dot key for sync and link to robot

* minor fixes

* test: review fixes for integration tests
2026-09-23 20:11:37 +03:00
jwinterm c1f618176c ci(desktop): build macOS, and publish every desktop artifact to the release page
Three desktop targets, three different states, one destination.

macOS did not build at all. There was no workflow, only Cake's from-source
scripts and an Xcode project. It turns out most of the work was already done:
monero_c ships aarch64-apple-darwin and x86_64-apple-darwin, and
Runner.xcodeproj already references both dylibs with CodeSignOnCopy. So this
fetches the same prebuilt bundle the iOS and Android legs use, lipos the two
slices into universal libraries, and builds. One download serves Apple Silicon
and Intel, because a single-arch build looks correct on the machine that made
it and fails on half of everyone else's.

It signs with a Developer ID Application certificate, which is NOT the Apple
Distribution certificate the TestFlight workflow uses. They are not
interchangeable: an App Store cert on a directly downloaded app is refused by
Gatekeeper. Signing is inside-out by hand rather than --deep, which Apple
deprecated and which skips nested code it does not recognise. Then notarytool
with the App Store Connect key this repository already has, then stapler, so
Gatekeeper clears the app without a network round trip.

Every credential is optional and the preflight says, before spending forty
minutes, exactly which stages the run can reach and what the user will see if
it cannot reach them. Absent secrets downgrade the build loudly; they do not
fail it and they do not pass silently.

Windows and Linux already built. Windows already Authenticode-signs through the
Fleet broker. What both lacked was a way to get the artifact to anybody:
upload-artifact is the only publication and Gitea Actions artifacts here upload
and then list as zero, so the signed Windows zip has never been reachable. Both
now publish to the GitHub release, on the smirk pattern: tag-triggered, the
release must already exist so a half-populated one never goes public, and a
same-named asset from an earlier attempt is replaced rather than duplicated.
Windows does its API work in PowerShell because git-bash there has neither
python nor node guaranteed and ConvertFrom-Json is native.

Tags now build automatically on all three. A tag is the thing a release is cut
from, and the artifact that gets signed should come from it rather than from
whatever was dispatched by hand.

scripts/release/sign-release.sh detach-signs the collected artifacts and signs
SHA256SUMS, which is the signature that matters when a download reaches someone
from a mirror without its .asc. It verifies even immediately after signing: a
signature written but never checked invites trust it has not earned.

Not done here, and both need a human: GH_RELEASE_TOKEN does not exist in this
repository's Gitea secrets, and the release key's user ID still says Smirk.
2026-09-23 08:13:10 -04:00
jwinterm c11c79b0c5 ci(windows): refuse a drifted Flutter, and let provisioning notice one
Windows built 1.0.3 against Flutter 3.38.5 while every other platform used
3.32.0. Dart 3.10 removed the _macros package, so pub could not solve
hive_generator against build_resolvers and the build died in dependency
resolution, on the same commit Android and Linux had just built cleanly.

The SDK was not missing. C:\flutter is exactly where provisioning puts it and
exactly what PATH resolved; it had been upgraded in place. Provisioning never
corrected it because it skipped on Test-Path alone, so a drifted SDK was
reported as done every time it ran. It now compares the version and replaces
the SDK when it does not match, which is the actual fix and the reason this
went unnoticed for however long the machine has been ahead.

The workflow stops guessing. My first attempt installed Flutter per build via
subosito/flutter-action, which fails on this runner anyway because its setup
script needs jq, and which would have downloaded a gigabyte every run to work
around a machine provisioning already owns. It now refuses early and prints
what is wrong and which script to run.

A build machine's toolchain must not decide which dependencies resolve, and
when it has drifted the build should say so rather than fail eighty steps
later inside pub.

No app code changes; hash-bags-ios-1.0.3 stays at 0697f41c7. Android and Linux
artifacts from that commit are unaffected.
2026-09-19 14:13:39 -04:00
jwinterm 25acfe656b ci(windows): pin Flutter 3.32.0 like every other platform
Windows took whatever Flutter the runner happened to have. On 2026-09-19 that
was 3.38.5 with Dart 3.10.4, and Dart 3.10 removed the _macros package, so pub
could not solve hive_generator against build_resolvers:

  macros >=0.1.3-main.0 depends on _macros from sdk which doesn't exist
  So, because hash_wallet depends on both build_resolvers ^2.4.4 and
  hive_generator ^2.0.1, version solving failed.

The build died at exit 69 before compiling anything, on the same commit that
Android and Linux built successfully minutes earlier. They differ only in
that both run inside the cake_wallet debian13-flutter3.32.0 image, and iOS
installs 3.32.0 explicitly. Windows was the only platform whose dependency
resolution was decided by a machine somebody had updated.

It now checks for 3.32.0 and installs it when absent, the same two steps iOS
already uses. No app code changes; hash-bags-ios-1.0.3 stays at 0697f41c7.
2026-09-19 14:09:26 -04:00
jwinterm d703455ef2 ci(windows): export the Rust env for the step that checks it
The detection branch found the toolchain, wrote RUSTUP_HOME and CARGO_HOME to
$GITHUB_ENV, and then verified with `rustc --version` in the same step.
$GITHUB_ENV only reaches later steps, so that verification ran with neither
variable set and the rustup shim fell back to %USERPROFILE%\.rustup. On this
runner that path is a file, so it failed with os error 183 while a perfectly
good toolchain sat at C:\Rust.

The step's own comment already explained why the shims need RUSTUP_HOME. It
just never exported it for its own use. The install branch does export before
its check, which is why only the detection path broke, and why it broke on a
runner that already had Rust rather than on a fresh one.

No app code changes, so hash-bags-ios-1.0.3 stays where it is: Android, Linux
and iOS are building from that commit and the artifacts are unaffected.
2026-09-19 12:07:18 -04:00
jwinterm 8e45de5928 ci(windows): Authenticode-sign HashWallet.exe with the hardware token
The Windows lane already runs on the machine that physically holds the Sectigo
EV code-signing token, but the runner there is NT SERVICE\act_runner, which
cannot use it: SafeNet holds the token login per user and an operator
establishes that login interactively. So the build queues the launcher for the
signing broker that Fleet installs (such-fleet roles/windows_code_signing) and
waits for it to come back signed.

CI never holds the private key -- it is non-exportable on FIPS 140-2 Level 2
hardware -- and never holds the token password. Sectigo allows three
consecutive failed password attempts before the token needs vendor unlocking,
and a job that never supplies a password cannot spend one.

Only HashWallet.exe is signed. The bundled MSVC runtime DLLs are already
Microsoft-signed and must not be re-signed, and the plugin DLLs are loaded by
an already-trusted process; it is the launcher the user double-clicks that
SmartScreen judges.

The signature, its RFC 3161 timestamp and the expected signer are verified on
the exe that actually goes into the zip, and any of the three failing fails the
build. If the broker's circuit breaker is tripped the job says so and stops
rather than shipping an unsigned binary.

Procedure: docs platform/windows-hardware-token-signing.md.
2026-08-31 18:00:18 -04:00
jwinterm d8aab70fa7 fix: 1.0.2 hotfix — heal wallets broken by per-build random crypto secrets
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
2026-08-29 14:53:18 -04:00
jwinterm d19e52d308 ci(test): generate models and localization before analyzing
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.
2026-08-21 05:25:36 -04:00
jwinterm a144d037d5 ci(test): generate the untracked workspace before resolving
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.
2026-08-21 05:00:41 -04:00
CindyandGitHub 3b098e35ff Privacy: randomized coin selection (BnB changeless + single random draw) (#3408)
* Randomize coin selection with single random draw

The greedy selection walked the unspent pool in a predictable order (address generation order, then per-address server order), a fingerprint an analyst can exploit. Shuffle the pool before the accumulate-until-covered loop so the input set is chosen non-deterministically (single random draw). MWEB coins are still kept last.

Branch-and-bound (changeless exact match) is a larger follow-up.

* Add BnB + SRD coin selection primitives

Pure, wallet-independent coin selector: branch-and-bound for an exact
changeless match within the cost-of-change window, single-random-draw
fallback (shuffled, injectable RNG) when no exact match exists, and
selectCoins tying them together over effective values.

This is the basis for replacing the greedy accumulation in _createUTXOS
so sends produce no change when possible and a non-deterministic
selection otherwise. Validated with dart test (9 cases); wiring into
_createUTXOS is a separate step.

* Use BnB changeless matching in coin selection

Before the shuffled greedy walk, run branch-and-bound over effective
values (value minus per-input fee cost) looking for an input set whose
excess over amount plus fee stays below dust. When found, order those
inputs first and cap the walk at their count: the caller then computes
a change below dust, drops the change output, and absorbs the residue
into the fee, producing a changeless transaction with no residual
change fingerprint.

When no match exists, selection falls back to the shuffled pool, which
the greedy walk turns into a single random draw. BnB is skipped for
sendAll, forced input counts and pools holding MWEB coins.

changelessMatch filters non-positive effective values and maps indices
back to the original pool.

* Cover changeless fee bounds and BnB termination

Assert the end-to-end arithmetic the wallet performs around a
branch-and-bound match: with the 68/34/10 vBytes model, the leftover
the caller absorbs into the fee is never negative (no re-selection
loop) and never exceeds the dust limit (bounded fee overpay).

Also pin termination: a large pool with no possible match returns null
through the maxTries cap instead of exploring the full search tree.

* Use a secure RNG for coin selection randomness

The selection shuffle and the single-random-draw fallback exist to be
unpredictable, so seed them from Random.secure() instead of the default
PRNG, matching what Core and bdk use for coin selection. Pool sizes are
small, so the cost is negligible. Tests keep injecting a seeded Random
for determinism.

* Randomize RBF and payjoin input candidate order

Two secondary paths still consumed unspentCoins in wallet scan order
(address generation order, then per-address age), the same predictable
order removed from the main selection path:

- replaceByFee walked the unused-UTXO list in scan order when the fee
  bump needed extra inputs
- the payjoin receiver handed input candidates to the payjoin library
  in scan order, letting ties inside its selection mirror that order

Shuffle both with a secure RNG so no input-selection path exposes the
wallet's address or coin age ordering.

* ci: skip Linux PR build on forks (no secrets access)

* Account for script-type sizes in changeless matching

* Keep coin selection order stable within a transaction build
2026-08-19 12:51:46 +02:00
cyanandGitHub be694edcc4 feat: pin all dependencies to git sources (#3381) 2026-08-18 16:34:01 +02:00
jwinterm 71706314fb privacy: only domain-anchored address resolvers; drop Cake endpoints
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
2026-08-15 16:28:54 -04:00
jwinterm 77ae91e695 ci: run analyze + tests on every push/PR; make the suite real
- 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
2026-08-15 16:28:44 -04:00
malik1004xandGitHub 26bc7bda3d add compile graphics to linux ci (#3509) 2026-08-11 14:05:45 +02:00
cyanandGitHub 43cd125942 fix: linux builds (#3485) 2026-08-07 22:10:27 +02:00
jwinterm 94bf0e8c41 ci(windows): enable Developer Mode for plugin symlink support
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.
2026-08-07 12:38:36 -04:00
jwinterm 64642818f8 ci: update Windows build workflow 2026-08-07 10:29:15 -04:00
sethforprivacyandcyan c50d0fc6dd ci: sanitize branch name when naming test APK in reusable-build
The 'Rename apk file'/'Find APK file' steps used the raw ${BRANCH_NAME}
as an APK filename, so any branch containing a '/' (e.g. claude/*) made
cp target a non-existent test-apk/<prefix>/ subdirectory and failed the
build. Derive the sanitized branch name from android/app.properties, the
same approach already used in automated_integration_test.yml.
2026-07-24 23:35:59 +02:00
cyanandGitHub 28c2cc152e feat: zkool2 (#3165)
* feat: zkool2

* feat: rescan on zkool
fix: sending to extracted addresses
fix: labeling shielding txs
fix: autoshield endless loop
fix: autoshield t addr not firing
fix: autoshield destinations
chore: remove old routes for rescan with zkool
other minor fixes/cleanups

* fix: zcash hardfork

* fix: update sync progress more reliably

* fix: pending transaction loading
fix: autoshield txs
fix: taddress
fix: fee estimation
fix: tx send errors

* chore: bump zkool2 to latest version
fix: reduce amount of taddrs generated
fix: derivation path for internal wallets
fix: display address in correct hidden/usable spots
fix: get wallet db height before it syncs for the first time
fix: taddr rotation with passphrase
fix: sync progress ui refresh
fix: taddress generation not getting capped
fix: hidden addresses not being hidden
fix: pending transactions not being shown
fix: t address cache misses
fix: t address rotation not getting refreshed
fix: t address high index not being used

* fix: refresh t addresses

* fix: patch rust side to make t addr rotation work

* fix: ui issue in recipient address
fix: zcash multiaddress send to show in the ui
fix: potential race condition

* minor fixes

* format

* fix: flashing on receive screen

* remove unused 'autoShield'

* review
2026-07-17 16:01:31 +03:00
Omar HatemandGitHub 16ddc0837a Reformat the whole codebase with 100 line width (#3403) 2026-07-15 01:20:33 +03:00
Dávid MolnárandGitHub 58e8afedca Remove broken feature request discussion link (#3336)
Remove the extra GitHub issue contact link that pointed to a broken Discussions category.

The repository already has a working Feature request issue template, so the duplicate
Feature or Enhancement Request contact link created a confusing entry in the issue
creation modal and led users to a failing GitHub Discussions URL.
2026-07-14 16:26:13 +03:00
Omar HatemandGitHub bfc2c5ee23 await wallet address saving (#3393)
minor CI fix
2026-07-14 14:09:19 +02:00
01c977e82b add-lints (#3356)
* test add lint as separate action
* nicer logging
* only detect changes in dart files
* fix printV warnings being displayed when editing printV

---------

Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
2026-07-14 12:50:19 +02:00
jwinterm eee5ae83c2 CI: resolve app deps before generate_new_secrets (Android + Linux)
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.
2026-07-09 21:49:08 -04:00
jwinterm d9897f13ea MoonPay: key-gated enablement + inject MOONPAY_API_KEY/EXCHANGE_HELPER_API_KEY in CI
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.
2026-07-02 16:41:45 -04:00
jwinterm 803be5feea CI: inject WalletConnect projectId into Android + iOS TestFlight builds
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.
2026-07-02 15:49:54 -04:00
jwinterm 784a082f15 Legal + release-readiness pass for Hash Bags launch
Legal (aligned with the hosted hash.boats Terms & Privacy):
- Rewrite in-app Terms (assets/text/Terms_of_Use.txt): drop the false 'No Fiat
  Operations'/'No Embedded Applications' clauses; add MoonPay (optional, where
  available), Trocador swaps + ~1% fee disclosure, WalletConnect, sanctions/18+,
  Apple EULA rider, and PA + class/jury-waiver dispute terms.
- Reconcile PRIVACY.md with the canonical hosted policy.
- Add THIRD_PARTY_LICENSES.md; correct the blanket 'MIT' claim in README (app
  code MIT; bundles BSD-3/Apache-2.0/LGPL-3.0); fix README build-doc + LICENSE.md
  links and copyright year.
- Standardize in-app legal naming to 'Terms of Service'; add in-app links to
  hash.boats/terms and /privacy.

Release fixes:
- Align URL scheme hashwallet:// -> hashbags:// (matches registered scheme) so
  WalletConnect/Trezor/quick-action deep links work.
- Store-review gating: hide Buy action until a provider is available; hide the
  empty 'Apps' tab.
- Exclude removed chains (SOL/TRX/ZEC/DCR/ZANO + tokens) from swap pickers.
- Residual Cake branding: macOS CFBundleDisplayName and Windows Runner.rc
  (Hash Wallet -> Hash Bags); remove Android cakewallet://y.at leftover.
- Genericize internal-infra comments in CI workflows.
2026-07-02 14:07:24 -04:00
75f3fe735b Introduce Money class and refactor currency handling (#3157)
* feat: add `Money` class for precise currency handling and amount parsing

- Introduced `Money` class for handling money values with fixed-point precision.
- Added parsing and comparison methods to `CryptoCurrency` and `FiatCurrency` interfaces.
- Implemented `smartAmountSanitizer` for format normalization.

Includes extensive unit tests for validation.

* refactor: optimize Money class with new factories and string formatting improvements

* refactor: unify balance handling with `Money` refactor

* refactor: update `ElectrumBalance` and transaction logic to use `Money` for currency precision

* refactor: standardize balance handling with `Money` across `Monero`, `Wownero`, and `Zano` wallets

* refactor: migrate EVM balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Decred balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate nano balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Solana balance and transaction logic to `Money` for precise currency handling

* refactor: migrate Tron balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Zcash balance and transaction handling to `Money` for consistent currency precision

* refactor: adjust lib to handle money

* refactor: remove legacy Decred amount formatting and migrate to `Money`

* refactor: migrate pending transaction amount and fee to `Money`

* refactor: migrate dEuro savings and transaction logic to `Money` for improved precision and consistency

* refactor: migrate Bitcoin and lightning transaction logic to `Money` for consistent currency precision

* refactor: migrate Wownero, EVM, Zano, and Zcash transaction logic to `Money` for consistent and precise currency handling

* refactor: standardize balance handling across wallets, migrate to `Money.zero` and refine `Balance` class for consistency

* refactor: migrate Solana transaction and wallet logic to `Money` for consistent and precise currency handling

* refactor: improve fiat balance and formatting for consistency on dashboard

* fix: fiat input mode switching and standardize crypto amount display logic

* fix: cw_evm generator

* refactor: migrate exchange amount handling to `Money` for improved precision and consistency

* test: remove redundant and commented-out tests, tidy test cases, and apply minor formatting adjustments

* fix using Money wrapper in BridgeViewModel and USDT0Service

* refactor: streamline `AmountConverter` and `LightningWallet`, add `cw_core` tests to GitHub workflow, and remove unused/deprecated logic

* refactor: enhance `changeReceiveAmount` with `isCanonical` support, improve null handling, and refine amount parsing logic

* minor fix

* try/catch the lightning log file error

* feat: add copy-to-clipboard functionality for transaction amounts, fix typo in trailing widget method name (#3219)

* Cw 1234 disallow sp and mweb addresses as refund for swaps (#3187)

* disallow SP and MWEB for swaps

* Simplify address selection in addressForExchange

Refactor addressForExchange method to simplify address selection logic.

* validate refund and receive address types

* localize address validation messages

* minor ui fix [skip ci]

* minor fix [skip ci]

* Revert "localize address validation messages"

* fix translations

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor: replace manual amount sanitization with `sanitized()` extension across the codebase, add unit tests for `AmountSanitizer`

* fix: correct `sanitized()` test expectations for decimal handling in `amount_sanitizer_test.dart`

* refactor: fee display logic and append currency symbol in `send_confirm_sheet.dart`

* refactor: simplify `ZcashBalance` class by replacing custom fields with inherited properties, update references accordingly

* refactor: replace hardcoded USDT contract address with `DefaultTronTokens` lookup, improve maintainability

* refactor: replace `currency` with `inputAmount.currency` in Solana transaction signing methods to avoid inconsistency

* fix: handle null `fee` in `electrum_transaction_info.dart` to prevent potential runtime errors

* refactor: replace raw fee and amount handling with `Money` object for improved consistency and maintainability

* fix: limit fiat balance display to 2 decimal places in `balance_view_model.dart`

* refactor: replace raw `amount` and `fee` types with `Money` in Solana transaction methods for consistency and precision

* refactor: replace raw `amount` and `fee` handling with `Money` and improve balance calculation consistency across view models

* refactor: replace raw balance strings with `Money` and simplify `UnconfirmedBalanceModal` widget logic

* refactor: replace `print` with `printV` for better logging, add equality and hashCode overrides to `ERC20Currency` for object comparison

* feat: prepare deuro savings migration

* feat: add V1 savings migration support and improve formatting consistency in DEuro view model

* feat: add V1 savings withdrawal support and update associated UI components and logic

* refactor: replace `Image.asset` with `CakeImageWidget`, update balance string handling, and streamline address validation logic

* refactor: use `Money.tryParse` with fallback to `Money.zero` in `NanoBalance.fromRawString` for safer balance parsing

* refactor: replace `ERC20Currency` with `Erc20Token` for consistency and remove unused `evm_erc20_currency.dart` file

* refactor: add computed `hasDepositAmount` for deposit null-checks, optimize balance handling in `zano_wallet`, update Breez SDK to v0.14.0, and implement equality/hashCode for `Currency`

* refactor: rename `amountSats` to `amount` in `PrepareLnurlPayRequest` for consistency

* revert: downgrade Breez SDK to v0.11.0 in `pubspec.yaml` and update relevant references in `pubspec.lock`

* fix: use `cryptoAmount.copyWith` for currency consistency and replace `Image.asset` with `CakeImageWidget` in swap address modal

* fix: ensure currency consistency by using `cryptoAmount.copyWith` in Solana and EVM wallet transactions

* refactor: simplify CSV row generation logic by removing `_splitAmountCurrency` and streamlining amount/fee handling

* fix: LateInitializationError Field 'sdk' has not been initialized

* fix: use `Money.tryParse` for safer parsing of deposit and receive amounts

* chore: remove debug print statement

* refactor: update transaction processing to consistently use `Money` for amounts and fees

* refactor: improve currency and amount handling, simplify logic, and enhance code readability in Send and Swap pages

* refactor: integrate `DecimalInputFormatter` for consistent decimal input across Send and Swap pages, simplify input handling logic

* fix: "fiatMode" per output on sendPage

* refactor: make transaction_details_height applicable more readable

* fix: use correct base unit for deposit

* refactor: use Money in estimateFakeSendAllTxAmount method

* fix decred frozen balance
minor fixes

* fix decred frozen balance
minor fixes

* fix: automatically switch to lightning if lnurl or invoice detected

* fix: getBolt11Amount test cases

* fix: standardise crypto amount formatting and display

* fix: use base unit for Tron transaction fee parsing
* refactor: hardcode fractional digits to 8 in `AmountParsingProxy` display methods
* refactor: ensure consistent 8-decimal precision for fee formatting in `PendingTransaction`
* fix: use `AmountParsingProxy` for available balance display in exchange view model
* fix: improve amount formatting and layout in swap and send confirmation sheets
* chore: remove redundant decimal truncation in balance view model and send confirmation sheet

* fix: allow for editing text field if it is filled with "ALL"

* fix: allow for editing text field if it is filled with "ALL"

* fix: update currency detection logic for lightning mode

* avoid potential infinite loop [skip ci]

* fix: getDisplayCryptoAmount

* fix: update estimated fee calculation

* chore: simplify fee calculation logic [skip-ci]

* fix value display in hidden mode

* fix stale state in send confirm sheet

* fix decimals for mweb

* fix available balance display for sol/trx on swap

* restore proper hint text

* fix swap receive amount calculation for xno

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com>
Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
2026-06-30 02:45:44 +03:00
d09391bf99 Upgrade Flutter version to 3.41.9 (#3213)
* commit generated files and remove codegen metadata

* remove hive_generator dependency

* update dependencies

* update intl

* update ios project files

* update native code

* update android deps

* update image name in ci

* revert impeller

* re-ignore min version error

* fix cw_decred dependency

* update hive in cw_core

* fix docker build cache for decred

* fix conflict

* prevent frozen coins being reset on coin info refresh

* fix for makeSecure

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
2026-06-26 16:03:31 +03:00
cyanandGitHub 32091d9de9 feat: randomize icon based on branch (thx :konsti:) (#3328) 2026-06-11 23:16:48 +02:00
David AdegokeandGitHub 9ba53878f2 fix: linux CI, strip inner wrapper dir from monero_c RC2 bundle (#3325) 2026-06-10 19:08:08 +02:00
jwinterm 86b4040c9b Linux: package release as self-contained AppImage
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>.
2026-06-03 20:11:43 -04:00
jwinterm ac294874bf iOS TestFlight: patch LibTorch Info.plist to match binary LC_BUILD_VERSION
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.
2026-06-02 13:39:30 -04:00
jwinterm 52fd1d784f iOS: bump deployment target 12/13 → 15 (Apple ITMS-90208 fix)
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.
2026-06-02 13:02:08 -04:00
jwinterm be4cf0e49a iOS TestFlight: validate .p8 AuthKey before altool upload
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.
2026-06-01 21:01:38 -04:00
jwinterm fc813368cc iOS TestFlight: also install nightly Rust + iOS targets
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.
2026-06-01 20:42:11 -04:00
5b2a8f6755 Cw 1379 add monero support to trezor rebase dev (#3273)
* refactor: migrate hardware wallet resources to `new-ui`, add Trezor model support, and streamline hardware wallet handling across services

* refactor: migrate hardware wallet resources to `new-ui`, add Trezor model support, and streamline hardware wallet handling across services

* fix: correctly check for Ledger hardware wallet type in `isHardwareWallet` logic

* refactor: unify `isConnected` method for hardware wallets, add Trezor-specific support, and update Monero sync logic

* refactor: add Trezor transaction signing support, streamline hardware wallet handling, and update Monero dependency

* refactor: improve Trezor transaction handling, adjust hardware wallet state logic, and update Monero dependency references

* fix: non-hww tx commit() in xmr
fix: prod monero_c
fix: keyImage import workaround for Trezor/Cupcake
chore: add kotlin's .salive to .gitignore

* fix: universal_ble builds on iOS
chore: remove prints
fix: proper CI prebuilt for linux

* new trezor connection ui

* fix page title

* typo

* fix page title anim

* refactor: enhance Trezor pairing flow, update state management, and simplify UI logic in hardware wallet integration

* fix: correct widget indentation in Trezor pairing failure state UI

* fix: extract Trezor pairing failure state UI to `_errorBox` widget and update Trezor dependency reference

* refactor: simplify hardware wallet pairing error UI, optimize `_errorBox` layout, and streamline widget logic

* Update cw_monero/lib/pending_monero_transaction.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Update lib/buy/robinhood/robinhood_buy_provider.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* feat: add USB intent filters, device filter for hardware wallets, and update Trezor dependency reference

* Update cw_monero/pubspec.yaml [skip ci]

* Update cw_wownero/pubspec.yaml [skip ci]

* Update cw_zano/pubspec.yaml [skip ci]

* fix: use CryptoCurrency.title instead of amount-refactor-only .symbol getter

new_wallet_type_page used curr.symbol, which only exists on the
amount-refactor branch (String get symbol => title). On dev the
equivalent is curr.title. Adapts the Trezor feature to dev's API
without pulling in amount-refactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix wallet list screen not scrollable
minor fixes

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Konstantin Ullrich <konstantin@cakewallet.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:33:17 +02:00
jwinterm 2294bfabe7 iOS TestFlight: patch signing in pbxproj, not Release.xcconfig
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.
2026-06-01 20:25:22 -04:00
jwinterm 2dd7c84246 iOS TestFlight: drop openssl conversion, import .p12 directly
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.
2026-06-01 19:59:50 -04:00
jwinterm 5a43cc1114 iOS TestFlight: re-export .p12 as legacy PKCS12 before security import
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.
2026-06-01 19:17:04 -04:00
jwinterm 91dadfd35e iOS TestFlight: use openssl base64 -d -A, stop fighting BSD base64
`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.
2026-06-01 19:08:20 -04:00