Compare commits

...

47 Commits

Author SHA1 Message Date
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
2abff1fe06 Android version-from-pubspec + proper iOS tinted icons
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.
2026-06-03 09:31:24 -04:00
jwinterm
21f3a75aa5 Wownero: dispatch getCurrentAccount by wallet.type in AccountCustomizer
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.
2026-06-03 09:15:10 -04:00
jwinterm
a1b3add2a6 iOS: source CAKEWALLET build number from pubspec_description.yaml
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.
2026-06-03 08:02:55 -04:00
jwinterm
5ff5ebf910 iOS: declare ITSAppUsesNonExemptEncryption=false (open-source exemption)
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.
2026-06-02 20:16:27 -04:00
jwinterm
6e6cf33bf1 iOS icons: replace Cake artwork with Hash Bags # mark, rename dir
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.
2026-06-02 20:11:07 -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
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
jwinterm
4f38322c92 iOS TestFlight: macOS base64 wants --decode, not -d
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").
2026-06-01 18:59:37 -04:00
jwinterm
5e6a9d5743 iOS TestFlight: sanity-check Apple secrets reach the runner
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.
2026-06-01 18:54:42 -04:00
jwinterm
b6f6f3f4da Wire error reporting to hash.boats/report, surface Wownero primary address
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.
2026-06-01 18:39:57 -04:00
jwinterm
e6195d1e98 iOS TestFlight: override signing in Release.xcconfig for CI
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.
2026-06-01 18:30:34 -04:00
jwinterm
59d7833668 Windows CI: revert SFX wrapper, ship as zip artifact
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.
2026-05-24 19:00:10 -04:00
jwinterm
0ad4a4eb8a Windows CI: fetch SFX modules from LZMA SDK, not "7-Zip Extra"
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).
2026-05-23 20:44:56 -04:00
jwinterm
bdbd3388a3 Windows CI: look in x64/ subdir for SFX modules
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.
2026-05-23 20:03:26 -04:00
jwinterm
3bf9509f08 Windows CI: pin 7-Zip Extra URL to 23.01 (only version still hosted)
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.
2026-05-22 21:57:40 -04:00
jwinterm
b8ca749e1e Windows CI: download 7-Zip Extra to get installer-mode SFX module
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.
2026-05-22 21:03:53 -04:00
jwinterm
8c4fbed869 Drop zano_node_list.yml from pubspec_base.yaml assets list
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.
2026-05-22 19:22:47 -04:00
jwinterm
1ad70a210a Windows CI: fix YAML break — replace SFX-config heredoc with printf
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.
2026-05-22 18:09:59 -04:00
jwinterm
713eba2a53 Windows CI: ship single-file portable HashBags.exe via 7-Zip SFX
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.
2026-05-22 12:18:15 -04:00
jwinterm
bab0d252ee Rebrand: Cake Pay / Cake 2FA / Cake Wallet → Hash Bags across all 31 locale ARBs
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.
2026-05-22 12:16:32 -04:00
jwinterm
20ad49a403 Icon: redraw # as vector path for true centering
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)
2026-05-22 12:16:07 -04:00
jwinterm
a2a9c1b0aa Pre-release rebrand: core platform branding + versions + node lists
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.
2026-05-22 11:26:18 -04:00
jwinterm
bba8a975cf CI: add Hash Bags Windows build workflow (manual trigger)
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.
2026-05-22 10:48:38 -04:00
jwinterm
a6bb3c595d Branding: Hash Wallet → Hash Bags for Windows + Linux app name
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.
2026-05-19 17:58:22 -04:00
jwinterm
52b8cd090d Wownero: fix missing icon in wallet picker
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.
2026-05-19 17:58:22 -04:00
jwinterm
c506679086 iOS sim: drop sudo /etc/hosts rewrite, add upload diagnostic
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.
2026-05-19 17:37:00 -04:00
jwinterm
6c765a6e0e iOS sim: write /etc/hosts at job runtime so upload-artifact can reach Gitea
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.
2026-05-19 15:30:13 -04:00
jwinterm
ee4ccacab0 iOS sim: skip artifact upload, just copy .app to workspace/dist
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.
2026-05-19 15:01:59 -04:00
jwinterm
0aa9f03f77 Android: bump gradle JVM heap to 10G + cap workers to fix AAB OOM
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.
2026-05-19 14:38:57 -04:00
jwinterm
ba5c81e78b CI: clarify NODE_TLS_REJECT_UNAUTHORIZED is a fallback now
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).
2026-05-19 12:52:29 -04:00
jwinterm
9f832633b9 CI: skip TLS verify on iOS artifact upload + verbose AAB build
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.
2026-05-19 12:29:53 -04:00
jwinterm
1c844c58cf CI: pre-install Rust + iOS targets to fix rustup-during-podbuild race
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.
2026-05-19 11:11:14 -04:00
jwinterm
8f583b14ea iOS: strip Zano/Zcash xcframework refs + generate Monero/Wownero XCFrameworks in CI
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.
2026-05-19 10:54:48 -04:00
jwinterm
e277d1ba33 CI: run compile_graphics.sh to generate .svg.vec icon assets
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.
2026-05-19 10:50:44 -04:00
jwinterm
a19d0615c8 App icon: regenerate Android assets from Hash Bags # mark
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.
2026-05-19 09:48:30 -04:00
jwinterm
e21430e1d6 CI: force LANG/LC_ALL to UTF-8 in iOS workflows (CocoaPods needs it)
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.
2026-05-19 08:20:17 -04:00
jwinterm
bf2a35893d CI: iOS workflows clone BitBox without running Android bindings build
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.
2026-05-19 08:02:33 -04:00
jwinterm
d21c944f3f CI: manual-trigger only for all build workflows
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).
2026-05-19 07:57:22 -04:00
jwinterm
1e4af3dbde CI: drop pull_request triggers (Gitea strips secrets from fork PRs)
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.
2026-05-19 07:54:09 -04:00
jwinterm
71da372d83 CI: install Go + gomobile if missing (for BitBox Flutter iOS bindings)
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.
2026-05-19 07:50:02 -04:00
239 changed files with 1526 additions and 782 deletions

View File

@@ -5,10 +5,9 @@ name: Hash Bags Android build
# - PRs targeting dev/main (gate merges)
# - manual via workflow_dispatch
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
# Manual-only for now. Trigger via Actions → "Hash Bags Android build"
# → Run workflow when you want a build. workflow_dispatch runs as the
# triggering user, so secrets are always available (unlike PR triggers).
workflow_dispatch:
concurrency:
@@ -189,8 +188,19 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -e
# Debug visibility — prints lengths only, never values. If a secret
# length is 0, Gitea Actions is not passing it to this run (most
# commonly because the run was triggered by a from-fork PR).
echo "ANDROID_KEYSTORE_BASE64 length: ${#ANDROID_KEYSTORE_BASE64}"
echo "ANDROID_KEYSTORE_PASSWORD length: ${#ANDROID_KEYSTORE_PASSWORD}"
echo "ANDROID_KEY_ALIAS length: ${#ANDROID_KEY_ALIAS}"
echo "ANDROID_KEY_PASSWORD length: ${#ANDROID_KEY_PASSWORD}"
if [[ -z "$ANDROID_KEYSTORE_BASE64" ]]; then
echo "FATAL: ANDROID_KEYSTORE_BASE64 not set — configure Gitea Actions secrets first"
echo "FATAL: ANDROID_KEYSTORE_BASE64 not reaching the runner."
echo "Check: (1) secret is in repo Settings → Actions → Secrets;"
echo " (2) workflow was triggered by 'push' or 'workflow_dispatch'"
echo " (PR triggers from a fork strip secrets);"
echo " (3) Gitea's runner is configured to pass secrets."
exit 1
fi
# Write decoded keystore next to build.gradle (storeFile path
@@ -218,15 +228,20 @@ jobs:
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# ---- Compile + sign --------------------------------------------------
# Universal APK (single binary covers all 4 ABIs, easy to sideload).
- name: Build release APK (universal, all ABIs)
run: flutter build apk --dart-define-from-file=env.json --release
# AAB for Play Console upload (Google generates per-ABI APKs server-side
# via Play App Signing).
# via Play App Signing). --verbose surfaces the full gradle stacktrace
# when FinalizeBundleTask / signReleaseBundle fails — without it the
# error is opaque ('FinalizeBundleTask$BundleToolRunnable failed').
- name: Build release AAB
run: flutter build appbundle --dart-define-from-file=env.json --release
run: flutter build appbundle --dart-define-from-file=env.json --release --verbose
- name: Sanity-check signature on APK
run: |

View File

@@ -8,10 +8,8 @@ name: Hash Bags iOS Simulator build
# Phase 2 (separate workflow): full TestFlight pipeline with signing.
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
# Manual-only for now. Trigger via Actions → "Hash Bags iOS Simulator
# build" → Run workflow when you want a build.
workflow_dispatch:
# Cancel in-flight runs when a newer commit lands on the same branch — so a
@@ -30,6 +28,16 @@ jobs:
env:
APP_IOS_TYPE: cakewallet
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# CocoaPods refuses to operate when LANG is ASCII-8BIT (default for the
# Gitea runner shell). Force UTF-8 globally so `pod install` succeeds.
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
# If you added an /etc/hosts entry on the Mac mini that points
# git.such.software at NPM's LAN IP (so the runner hits NPM's
# Let's Encrypt cert instead of Gitea's internal self-signed),
# this env var can be removed. Left in as a belt-and-suspenders
# fallback — has no effect if certs already verify.
NODE_TLS_REJECT_UNAUTHORIZED: '0'
steps:
- uses: actions/checkout@v4
@@ -62,11 +70,9 @@ jobs:
pod --version
exit 0
fi
# Prefer Homebrew on macOS — no sudo, installs into its own prefix.
if command -v brew >/dev/null; then
brew install cocoapods
else
# Fallback: user-local gem install (no sudo).
export GEM_HOME="$HOME/.gem"
export PATH="$GEM_HOME/bin:$PATH"
echo "GEM_HOME=$HOME/.gem" >> "$GITHUB_ENV"
@@ -75,6 +81,45 @@ jobs:
fi
pod --version
- name: Install Rust + iOS targets (if missing)
run: |
if ! command -v rustup >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --no-modify-path
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.cargo/bin:$PATH"
fi
# Clean any partial downloads from a previously crashed install — those
# are the source of the 'could not rename .partial → final: No such
# file or directory' race that breaks parallel pod builds.
rm -rf "$HOME/.rustup/downloads"/*.partial 2>/dev/null || true
# Pre-install iOS targets serially so the pod build doesn't try to
# do it under concurrency.
rustup target add aarch64-apple-ios
rustup target add aarch64-apple-ios-sim
rustup target add x86_64-apple-ios
rustup show
- name: Install Go + gomobile (if missing)
run: |
if ! command -v go >/dev/null; then
if command -v brew >/dev/null; then
brew install go
else
echo "Go missing and brew not available"; exit 1
fi
fi
go version
# Ensure $(go env GOPATH)/bin is on PATH for subsequent steps.
GOPATH=$(go env GOPATH)
echo "$GOPATH/bin" >> "$GITHUB_PATH"
export PATH="$PATH:$GOPATH/bin"
# gomobile + gobind are needed by scripts/build_bitbox_flutter.sh
if ! command -v gomobile >/dev/null; then
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
fi
which gomobile && gomobile version || true
- name: Show toolchain
run: |
set -x
@@ -108,11 +153,23 @@ jobs:
rm reown_flutter.tar.gz
popd
- name: Clone BitBox Flutter
- name: Clone BitBox Flutter (iOS — skip Android bindings)
run: |
# Pubspec has bitbox_flutter as a path: dep at scripts/bitbox_flutter,
# so the directory must exist for pub get. The bundled build_bindings.sh
# runs `gomobile bind -target=android` which needs the Android SDK we
# don't have on the Mac runner — and the resulting .aar is Android-only.
# iOS uses bitbox_flutter's native ios/Classes plugin, no .aar required.
set -x -e
pushd scripts
./build_bitbox_flutter.sh
if [[ ! -d bitbox_flutter ]]; then
git clone https://github.com/konstantinullrich/bitbox_flutter
fi
cd bitbox_flutter
git fetch -a
git reset --hard
git checkout 5a6e6dd388ef64003f86094af80d5453518b601d
git reset --hard
popd
# ---- Native crypto cores (monero_c prebuilt bundle) ------------------
@@ -130,17 +187,27 @@ jobs:
ls
popd
# iOS native lib staging: Cake's scripts/ios/setup.sh + gen_framework.sh
# expect pre-built .a archives in specific paths. The first run will
# likely surface what's missing; we iterate from there.
- name: Inspect iOS targets available in monero_c bundle
run: |
set -x
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG"
# List anything iOS-y
find "$BUNDLE_DIR" -maxdepth 1 -name '*ios*' -o -name '*apple*' 2>/dev/null || true
echo "=== bundle top-level ==="
ls "$BUNDLE_DIR" || true
echo "=== iOS-y subdirs ==="
find "$BUNDLE_DIR" -maxdepth 1 -type d \( -name '*ios*' -o -name '*apple*' \) 2>/dev/null
echo "=== sample contents of one apple-ios target ==="
for d in "$BUNDLE_DIR"/aarch64-apple-ios "$BUNDLE_DIR"/aarch64-apple-ios-simulator; do
[[ -d "$d" ]] && { echo "--- $d ---"; ls "$d"; }
done
- name: Build MoneroWallet + WowneroWallet XCFrameworks
run: |
set -x -e
pushd scripts/ios
bash ./gen_framework.sh
popd
ls -la ios/MoneroWallet.xcframework ios/WowneroWallet.xcframework
# ---- Configure: pubspec.yaml, Info.plist, GeneratedPluginRegistrant ---
- name: Run iOS configure (cakewallet profile)
@@ -182,6 +249,9 @@ jobs:
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# CocoaPods: the iOS Podfile installs Flutter plugin pods. Required
# before flutter build can link them.
- name: pod install
@@ -202,6 +272,58 @@ jobs:
cd build/ios/iphonesimulator
zip -r hash_wallet_ios_sim_${{ github.sha }}.zip Runner.app
# Confirm git.such.software resolves to a local target (the Mac itself).
# The runner inherits the Mac's /etc/hosts directly (host-mode act_runner),
# so the user should have:
# 127.0.0.1 git.such.software
# already in /etc/hosts. We only assert that here, don't modify (sudo
# without NOPASSWD prompts for a password the runner can't enter).
#
# macOS's `getent` doesn't exist and `nslookup` bypasses /etc/hosts.
# We grep /etc/hosts directly — that's what Node's getaddrinfo will
# actually consult.
- name: Verify Gitea hostname resolves locally
run: |
if grep -qE '^[[:space:]]*(127\.0\.0\.1|host\.docker\.internal|192\.168\.)[[:space:]]+git\.such\.software' /etc/hosts; then
echo "✓ /etc/hosts points git.such.software at a local target:"
grep git.such.software /etc/hosts
else
echo "✗ /etc/hosts is MISSING a local override for git.such.software."
echo " Node will resolve to the public IP and fail on NAT hairpin."
echo " Add (once, on the Mac mini):"
echo " sudo sh -c 'echo \"127.0.0.1 git.such.software\" >> /etc/hosts'"
exit 1
fi
# Diagnostic: print exactly where upload-artifact will try to send data.
# act_runner injects these env vars; their values tell us whether the
# upload targets git.such.software (uses our /etc/hosts override) or
# a LAN IP directly (override irrelevant). Also probe the URL with
# curl -k so we see the cert + reachability before the real upload.
- name: Diagnose Actions API endpoint
run: |
set +e
echo "=== Actions runtime env ==="
echo "ACTIONS_RUNTIME_URL=${ACTIONS_RUNTIME_URL:-<unset>}"
echo "ACTIONS_RESULTS_URL=${ACTIONS_RESULTS_URL:-<unset>}"
echo "ACTIONS_CACHE_URL=${ACTIONS_CACHE_URL:-<unset>}"
echo "GITHUB_SERVER_URL=${GITHUB_SERVER_URL:-<unset>}"
echo "GITHUB_API_URL=${GITHUB_API_URL:-<unset>}"
echo "NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-<unset>}"
echo
# Probe each non-empty URL to see what Node will actually hit.
for var in ACTIONS_RUNTIME_URL ACTIONS_RESULTS_URL GITHUB_SERVER_URL; do
url="${!var}"
[ -z "$url" ] && continue
host=$(echo "$url" | sed -E 's|^https?://([^/:]+).*|\1|')
echo "--- $var → $url ---"
echo "DNS for $host:"
dscacheutil -q host -a name "$host" 2>/dev/null || nslookup "$host" 2>/dev/null || true
echo "HEAD probe (curl -kIv, 10s timeout):"
curl -kIv --max-time 10 "$url" 2>&1 | head -40 || true
echo
done
- name: Upload .app artifact
uses: actions/upload-artifact@v3
with:

View File

@@ -35,6 +35,12 @@ jobs:
env:
APP_IOS_TYPE: cakewallet
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# CocoaPods needs UTF-8; Gitea runner shell defaults to ASCII-8BIT.
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
# See note in build-ios-sim.yml — fallback if /etc/hosts trick
# isn't in place. Safe no-op once it is.
NODE_TLS_REJECT_UNAUTHORIZED: '0'
steps:
- uses: actions/checkout@v4
@@ -76,6 +82,48 @@ jobs:
fi
pod --version
- name: Install Rust + iOS targets (if missing)
run: |
if ! command -v rustup >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --no-modify-path
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.cargo/bin:$PATH"
fi
rm -rf "$HOME/.rustup/downloads"/*.partial 2>/dev/null || true
# Stable + iOS targets (most plugins use these)
rustup target add aarch64-apple-ios
rustup target add aarch64-apple-ios-sim
rustup target add x86_64-apple-ios
# Nightly + iOS targets — at least one plugin (sp_scanner / payjoin
# / something in the cargokit chain) builds its rust component
# with +nightly. The TestFlight archive bombs at "Missing manifest
# in toolchain 'nightly-aarch64-apple-darwin'" if nightly isn't
# installed alongside stable.
rustup toolchain install nightly --profile minimal
rustup target add aarch64-apple-ios --toolchain nightly
rustup target add aarch64-apple-ios-sim --toolchain nightly
rustup target add x86_64-apple-ios --toolchain nightly
rustup show
- name: Install Go + gomobile (if missing)
run: |
if ! command -v go >/dev/null; then
if command -v brew >/dev/null; then
brew install go
else
echo "Go missing and brew not available"; exit 1
fi
fi
go version
GOPATH=$(go env GOPATH)
echo "$GOPATH/bin" >> "$GITHUB_PATH"
export PATH="$PATH:$GOPATH/bin"
if ! command -v gomobile >/dev/null; then
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
fi
which gomobile && gomobile version || true
- name: Show toolchain
run: |
set -x
@@ -96,6 +144,25 @@ jobs:
tar -xzf torch_dart.tar.gz -C torch_dart
rm torch_dart.tar.gz
popd
# LibTorch.xcframework ships with a lie: each slice's Info.plist
# claims MinimumOSVersion=12.0, but the Mach-O LC_BUILD_VERSION
# in the binaries says 13.0 (device) and 14.0 (simulator).
# Apple's TestFlight validator catches this mismatch and rejects
# the upload with ITMS-90208. Patch the Info.plists to match
# the actual binary build versions.
DEVICE_PLIST=scripts/torch_dart/ios/LibTorch.xcframework/ios-arm64/LibTorch.framework/Info.plist
SIM_PLIST=scripts/torch_dart/ios/LibTorch.xcframework/ios-arm64-simulator/LibTorch.framework/Info.plist
for entry in "$DEVICE_PLIST:13.0" "$SIM_PLIST:14.0"; do
plist="${entry%:*}"
min="${entry#*:}"
if [[ -f "$plist" ]]; then
plutil -replace MinimumOSVersion -string "$min" "$plist"
echo "patched $plist -> MinimumOSVersion $min"
plutil -p "$plist" | grep -E "MinimumOSVersion|CFBundleName" || true
else
echo "WARNING: $plist not found"
fi
done
- name: Fetch prebuilt reown_flutter
run: |
@@ -108,11 +175,20 @@ jobs:
rm reown_flutter.tar.gz
popd
- name: Clone BitBox Flutter
- name: Clone BitBox Flutter (iOS — skip Android bindings)
run: |
# See note in build-ios-sim.yml — iOS uses bitbox's native plugin,
# not the .aar that build_bindings.sh generates.
set -x -e
pushd scripts
./build_bitbox_flutter.sh
if [[ ! -d bitbox_flutter ]]; then
git clone https://github.com/konstantinullrich/bitbox_flutter
fi
cd bitbox_flutter
git fetch -a
git reset --hard
git checkout 5a6e6dd388ef64003f86094af80d5453518b601d
git reset --hard
popd
- name: Fetch prebuilt monero_c bundle
@@ -127,6 +203,14 @@ jobs:
rm release-bundle.zip
popd
- name: Build MoneroWallet + WowneroWallet XCFrameworks
run: |
set -x -e
pushd scripts/ios
bash ./gen_framework.sh
popd
ls -la ios/MoneroWallet.xcframework ios/WowneroWallet.xcframework
# ---- Configure: pubspec.yaml, Info.plist, etc. ----------------------
- name: Run iOS configure (cakewallet profile)
run: |
@@ -153,6 +237,40 @@ jobs:
lib/.secrets.g.dart
# ---- Apple signing setup --------------------------------------------
- name: Sanity-check Apple secrets are reaching the runner
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }}
APPLE_DIST_CERT_P12_PASSWORD: ${{ secrets.APPLE_DIST_CERT_P12_PASSWORD }}
APPLE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
run: |
# Print LENGTHS only — never the secret values. Length 0 means the
# secret isn't actually reaching the runner (most often: secret is
# set on a different Gitea repo than this workflow is running in).
set +e
echo "APPLE_TEAM_ID length: ${#APPLE_TEAM_ID}"
echo "APPLE_KEY_ID length: ${#APPLE_KEY_ID}"
echo "APPLE_ISSUER_ID length: ${#APPLE_ISSUER_ID}"
echo "APPLE_API_KEY_P8_BASE64 length: ${#APPLE_API_KEY_P8_BASE64}"
echo "APPLE_DIST_CERT_P12_BASE64 length: ${#APPLE_DIST_CERT_P12_BASE64}"
echo "APPLE_DIST_CERT_P12_PASSWORD length: ${#APPLE_DIST_CERT_P12_PASSWORD}"
echo "APPLE_PROVISIONING_PROFILE_BASE64 length: ${#APPLE_PROVISIONING_PROFILE_BASE64}"
echo "APPLE_KEYCHAIN_PASSWORD length: ${#APPLE_KEYCHAIN_PASSWORD}"
set -e
# Fail fast on any zero-length secret so we don't waste time on the
# subsequent steps.
for n in TEAM_ID KEY_ID ISSUER_ID API_KEY_P8_BASE64 DIST_CERT_P12_BASE64 DIST_CERT_P12_PASSWORD PROVISIONING_PROFILE_BASE64 KEYCHAIN_PASSWORD; do
var="APPLE_$n"
if [[ -z "${!var}" ]]; then
echo "FATAL: $var is empty — set it as a Gitea repo secret on the repo this workflow runs in (Builds/hash-wallet)."
exit 1
fi
done
- name: Create temp keychain + import distribution cert
env:
APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }}
@@ -167,9 +285,22 @@ jobs:
security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Add to user search list so codesign can find it
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
# Import the .p12
# Decode the user-uploaded .p12 directly. macOS Keychain Access
# exports .p12 with legacy PKCS12 crypto (RC2-40-CBC, PBE-SHA1)
# which is exactly what `security import` expects — no openssl
# conversion needed. Earlier attempts to "convert to legacy"
# via openssl 3.x failed because openssl 3.x has RC2-40-CBC
# disabled by default; that was a non-problem we made for
# ourselves. The original "Unknown format" failure was caused
# by a truncated base64 secret value, since fixed by re-pasting
# the secret via Gitea's API.
P12="$RUNNER_TEMP/dist.p12"
echo "$APPLE_DIST_CERT_P12_BASE64" | base64 -d > "$P12"
echo "$APPLE_DIST_CERT_P12_BASE64" | openssl base64 -d -A > "$P12"
echo "=== decoded .p12 ==="
ls -la "$P12"
xxd "$P12" | head -1
file "$P12" || true
security import "$P12" -k "$KEYCHAIN_PATH" -P "$APPLE_DIST_CERT_P12_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
@@ -185,7 +316,7 @@ jobs:
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILES_DIR"
PROFILE="$RUNNER_TEMP/Hash_Wallet.mobileprovision"
echo "$APPLE_PROVISIONING_PROFILE_BASE64" | base64 -d > "$PROFILE"
echo "$APPLE_PROVISIONING_PROFILE_BASE64" | openssl base64 -d -A > "$PROFILE"
# Parse the .mobileprovision to get its UUID + Name (CMS-decoded plist).
PROFILE_UUID=$(security cms -D -i "$PROFILE" | plutil -extract UUID raw -o - -)
@@ -206,9 +337,29 @@ jobs:
run: |
set -e
mkdir -p "$HOME/.appstoreconnect/private_keys"
echo "$APPLE_API_KEY_P8_BASE64" | base64 -d > "$HOME/.appstoreconnect/private_keys/AuthKey_${APPLE_KEY_ID}.p8"
P8="$HOME/.appstoreconnect/private_keys/AuthKey_${APPLE_KEY_ID}.p8"
echo "$APPLE_API_KEY_P8_BASE64" | openssl base64 -d -A > "$P8"
ls -la "$HOME/.appstoreconnect/private_keys/"
# Diagnostics — verify the PEM file is well-formed without
# printing the private key body. A valid AuthKey .p8 must:
# - Start with "-----BEGIN PRIVATE KEY-----"
# - End with "-----END PRIVATE KEY-----"
# - openssl pkey -in file -noout must succeed (real ASN.1 parse)
echo "=== .p8 first/last lines (no key body) ==="
head -1 "$P8"
tail -1 "$P8"
echo "=== openssl parse check (silent on success) ==="
if openssl pkey -in "$P8" -noout 2>&1; then
echo "OK — .p8 parses as a valid EC private key"
else
echo "FATAL: .p8 doesn't parse — secret likely truncated or wrong content"
echo "Re-paste APPLE_API_KEY_P8_BASE64 via the Gitea API (same flow"
echo "as the .p12 re-paste); the file decoded to:"
wc -c "$P8"
exit 1
fi
# ---- Flutter init + codegen + pod install ---------------------------
- name: Initialize Flutter SDK (iOS precache)
run: |
@@ -221,11 +372,44 @@ jobs:
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
- name: pod install
run: |
cd ios
pod install --repo-update
# ---- Override signing in project.pbxproj for the archive step -------
# The committed Release config uses Automatic signing + "Apple
# Development" identity (set up for local dev where Xcode is signed
# into an Apple ID). On CI we have only the cert + provisioning
# profile we installed in the temp keychain; no Xcode account.
#
# First attempt was an xcconfig append, but Xcode build-setting
# precedence puts target-level pbxproj settings ABOVE xcconfig —
# so the pbxproj values won and the build did Automatic signing
# anyway. Patch the Release config in pbxproj directly, only
# within the Runner target block (id 97C147071CF9000F007C117D);
# leaves Debug/Profile configs untouched so local builds stay
# on Automatic signing.
- name: Override signing settings in pbxproj for archive
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -x
PBX=ios/Runner.xcodeproj/project.pbxproj
# BSD sed (macOS): -i '' for in-place, regex range scoped to
# the Runner target's Release config by its stable UUID.
sed -i '' "/97C147071CF9000F007C117D \/\* Release/,/name = Release;/ {
s/CODE_SIGN_IDENTITY = \"Apple Development\";/CODE_SIGN_IDENTITY = \"Apple Distribution\";/
s/CODE_SIGN_STYLE = Automatic;/CODE_SIGN_STYLE = Manual;/
s/DEVELOPMENT_TEAM = [A-Z0-9]\{1,\};/DEVELOPMENT_TEAM = ${APPLE_TEAM_ID};/
s|PROVISIONING_PROFILE_SPECIFIER = \"\";|PROVISIONING_PROFILE_SPECIFIER = \"${PROFILE_NAME}\";|
}" "$PBX"
echo "=== patched Runner Release config ==="
sed -n '/97C147071CF9000F007C117D \/\* Release/,/name = Release;/p' "$PBX"
# ---- Generate ExportOptions.plist + build IPA -----------------------
- name: Write ExportOptions.plist
env:

View File

@@ -5,10 +5,9 @@ name: Hash Bags Linux build
# - on PRs targeting dev/main (gates merges)
# - manual via workflow_dispatch ("Run workflow" button in the UI)
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
# Manual-only for now — auto-run on every push was too noisy. Trigger
# via Actions → "Hash Bags Linux build" → Run workflow when you want a
# build. Add `push:` back here if/when we want pre-merge validation.
workflow_dispatch:
concurrency:
@@ -162,22 +161,93 @@ jobs:
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# ---- Compile + package ----------------------------------------------
- name: Build Linux app
run: flutter build linux --dart-define-from-file=env.json --release
- name: Compress release bundle
# ---- Package as AppImage --------------------------------------------
# Self-contained .AppImage is the user-facing Linux deliverable: single
# file, double-click to run, no install, works on any glibc Linux from
# the last few years. appimagetool is run with --appimage-extract-and-run
# so it works inside the container without FUSE.
- name: Stage AppDir
run: |
pushd build/linux/x64/release
zip -r hash_wallet_linux_${{ github.sha }}.zip bundle
popd
set -e -x
REL=build/linux/x64/release
APPDIR=$REL/Hash_Bags.AppDir
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin" \
"$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/256x256/apps"
# The whole Flutter Linux bundle (binary + data/ + lib/) lives
# under usr/bin/. The Flutter binary has RPATH=$ORIGIN/lib, so
# plugin/native libs in usr/bin/lib/ resolve naturally.
cp -r "$REL/bundle"/* "$APPDIR/usr/bin/"
# Icon: scale the iOS marketing icon to 256 if convert is around,
# else copy as-is (appimagetool only requires that AppDir root and
# the hicolor path contain a same-named PNG).
ICON_SRC=assets/images/ios_icons/hashwallet_ios_icons/Icon-App-1024x1024@1x.png
ICON_DEST="$APPDIR/usr/share/icons/hicolor/256x256/apps/hash_bags.png"
if command -v convert >/dev/null 2>&1; then
convert "$ICON_SRC" -resize 256x256 "$ICON_DEST"
else
cp "$ICON_SRC" "$ICON_DEST"
fi
cp "$ICON_DEST" "$APPDIR/hash_bags.png"
# .desktop file (at both canonical locations; appimagetool checks both)
cat > "$APPDIR/hash_bags.desktop" <<'DESKTOP'
[Desktop Entry]
Name=Hash Bags
Comment=Non-custodial multi-chain crypto wallet
Exec=hash_wallet
Icon=hash_bags
Type=Application
Categories=Office;Finance;
Terminal=false
DESKTOP
# Strip the leading whitespace from the heredoc
sed -i 's/^ //' "$APPDIR/hash_bags.desktop"
cp "$APPDIR/hash_bags.desktop" "$APPDIR/usr/share/applications/"
# AppRun: the entry point AppImage executes. Adds the bundled lib/
# dir to LD_LIBRARY_PATH and exec's the Flutter binary.
cat > "$APPDIR/AppRun" <<'APPRUN'
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
export LD_LIBRARY_PATH="${HERE}/usr/bin/lib:${LD_LIBRARY_PATH}"
exec "${HERE}/usr/bin/hash_wallet" "$@"
APPRUN
sed -i 's/^ //' "$APPDIR/AppRun"
chmod +x "$APPDIR/AppRun"
ls -la "$APPDIR"
- name: Build AppImage
run: |
set -e -x
REL=build/linux/x64/release
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /tmp/appimagetool
chmod +x /tmp/appimagetool
# --appimage-extract-and-run avoids the FUSE requirement (no fusermount
# in the build container). ARCH= tells appimagetool which arch tag to
# embed in the filename.
ARCH=x86_64 /tmp/appimagetool --appimage-extract-and-run \
"$REL/Hash_Bags.AppDir" \
"$REL/Hash_Bags-x86_64.AppImage"
ls -la "$REL"/*.AppImage
# actions/upload-artifact@v4 uses an HTTP API that Gitea Actions does
# not implement (only the v1/v3 wire format). Pinned to @v3 for Gitea
# compatibility — also works on GitHub Actions.
- name: Upload artifact
- name: Upload AppImage artifact
uses: actions/upload-artifact@v3
with:
name: hash-wallet-linux-${{ github.sha }}
path: build/linux/x64/release/hash_wallet_linux_*.zip
name: hash-wallet-linux-appimage-${{ github.sha }}
path: build/linux/x64/release/Hash_Bags-x86_64.AppImage
retention-days: 14

348
.github/workflows/build-windows.yml vendored Normal file
View File

@@ -0,0 +1,348 @@
name: Hash Bags Windows build
# Phase 1 of Windows CI: produce an unsigned Windows release bundle (.zip of
# the Flutter Release output + MSVC runtime DLLs). Drop the zip on any Win
# 10/11 box, extract, run hash_wallet.exe.
#
# Phase 2 (separate iteration): wire up Inno Setup installer once the
# scripts/windows/build_exe_installer.iss file is rebranded from Cake Wallet
# → Hash Bags.
on:
# Manual-only. Trigger via Actions → "Hash Bags Windows build" → Run workflow.
# workflow_dispatch runs as the triggering user, so secrets are always
# available (PR triggers from forks would strip them — not relevant here
# since the runner is self-hosted and listening on a self-hosted Gitea).
workflow_dispatch:
# Cancel in-flight runs when a newer commit lands on the same branch.
concurrency:
group: windows-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
# Explicit Git Bash path (not `shell: bash`) because Windows resolves
# bare `bash` to C:\Windows\System32\bash.exe (WSL), which refuses to
# run when the act_runner service is hosted under LocalSystem:
# "Running WSL as local system is not supported."
# Git Bash works fine under LocalSystem and matches how GitHub's
# hosted windows-latest runner invokes bash.
shell: '"C:\Program Files\Git\bin\bash.exe" --noprofile --norc -eo pipefail {0}'
jobs:
build:
# Matches the label our act_runner.exe advertised: "windows:host" → executor=host.
runs-on: windows
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# Belt-and-suspenders: bypass Node TLS verification for actions/* that
# talk back to git.such.software. Same fallback as build-ios-sim.yml.
NODE_TLS_REJECT_UNAUTHORIZED: '0'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
# ---- Git config for LocalSystem ---------------------------------------
# The runner service runs as LocalSystem, which has no global git
# identity. scripts/monero_c/apply_patches.sh (and a few other
# submodule operations) do `git commit`-equivalents that need
# user.email + user.name set. Also pre-empt git's "safe.directory"
# check, which fires when the checkout dir owner != runner identity.
- name: Configure git identity + safe.directory
run: |
git config --global --add safe.directory '*'
git config --global user.email "ci@suchsoftware.com"
git config --global user.name "Hash Bags CI"
git config --global --list
# ---- Toolchain sanity ------------------------------------------------
# Our self-hosted runner has Flutter / Rust / VS Build Tools / Inno
# Setup pre-installed (see scripts/windows/setup-windows-runner.ps1).
# This step just prints versions so failures are easy to diagnose.
- name: Show installed toolchain versions
run: |
set -x
# PATH may need refreshing after install — refresh from the registry.
# Git Bash inherits Windows PATH at shell start, so just printing
# `which` is enough.
which flutter dart cargo rustc git 2>/dev/null || true
flutter --version
dart --version
rustc --version
cargo --version
# ---- External path-dep plugins ----------------------------------------
# Several plugins are path-deps in pubspec.yaml. The path directories
# must exist BEFORE `flutter pub get` runs against the regenerated
# pubspec, or pub fails with "could not find package X at <path>".
#
# The Android + iOS + Linux workflows use upstream-published .tar.gz
# tarballs. We don't, because those tarballs contain example/*
# symlinks pointing to absolute /home/runner/.pub-cache/ paths
# (artifacts from the upstream build host) — Linux tar tolerates
# dangling symlinks, Windows tar bails the whole extraction. Clone
# the source repos at the matching tagged versions instead. Same
# pattern as bitbox_flutter further down.
- name: Clone torch_dart at v1.0.17
run: |
set -x -e
pushd scripts
rm -rf torch_dart
git clone --depth 1 --branch v1.0.17 https://github.com/MrCyjaneK/torch_dart
popd
- name: Clone reown_flutter at v0.0.4
run: |
set -x -e
pushd scripts
rm -rf reown_flutter
git clone --depth 1 --branch v0.0.4 https://github.com/cake-tech/reown_flutter
popd
- name: Clone BitBox Flutter
run: |
# Pubspec has bitbox_flutter as a path dep — the directory must
# exist for pub get even if BitBox Windows support isn't
# functional. Same approach as the iOS workflow: clone the repo
# at a pinned commit, don't run any Android-binding build step.
set -x -e
pushd scripts
if [[ ! -d bitbox_flutter ]]; then
git clone https://github.com/konstantinullrich/bitbox_flutter
fi
cd bitbox_flutter
git fetch -a
git reset --hard
git checkout 5a6e6dd388ef64003f86094af80d5453518b601d
git reset --hard
popd
# ---- Native crypto cores (monero_c prebuilt bundle) ------------------
# The same release-bundle.zip that Android + iOS use. Contains
# pre-cross-compiled native libs for many target triples. We'll need
# the x86_64-w64-mingw32 (Windows MinGW) or x86_64-pc-windows-msvc
# entries — inspection step below logs what's actually shipped.
- name: Fetch prebuilt monero_c bundle
run: |
set -x -e
./scripts/prepare_moneroc.sh
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
echo "monero_c TAG: $MONERO_C_TAG"
mkdir -p "scripts/monero_c/release/$MONERO_C_TAG"
pushd "scripts/monero_c/release/$MONERO_C_TAG"
curl -fsSL -O https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip
unzip -q release-bundle.zip
rm release-bundle.zip
echo "=== bundle contents (top level) ==="
ls
popd
- name: Inspect Windows targets in monero_c bundle
run: |
set -x
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG"
echo "=== full mingw32 dir contents ==="
ls -la "$BUNDLE_DIR/x86_64-w64-mingw32" 2>/dev/null || true
# ---- Stage monero_c DLLs into the layout windows/CMakeLists.txt expects
# Upstream Cake's Windows build cross-compiles monero_c on Linux via
# scripts/windows/build_all.sh, which lays files out as:
# scripts/monero_c/release/<coin>/<triple>_<basename>.dll
# The CMakeLists.txt at windows/CMakeLists.txt:84-94 hardcodes those
# paths in install(FILES ...) rules — it copies + RENAMEs them to the
# final names (monero_libwallet2_api_c.dll etc.) in the Release dir.
#
# We're using the prebuilt bundle from MrCyjaneK's release, which has
# a different layout:
# scripts/monero_c/release/<TAG>/<triple>/lib<coin>_wallet2_api_c.dll
# So we restage the bundle's files into the upstream-build-all-style
# layout before flutter build windows runs.
- name: Stage monero_c bundle into upstream cross-compile layout
run: |
set -x -e
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
SRC="scripts/monero_c/release/$MONERO_C_TAG/x86_64-w64-mingw32"
MONERO_DST="scripts/monero_c/release/monero"
WOWNERO_DST="scripts/monero_c/release/wownero"
mkdir -p "$MONERO_DST" "$WOWNERO_DST"
# The wallet C-API DLLs, one per coin
cp -v "$SRC/libmonero_wallet2_api_c.dll" "$MONERO_DST/x86_64-w64-mingw32_libwallet2_api_c.dll"
cp -v "$SRC/libwownero_wallet2_api_c.dll" "$WOWNERO_DST/x86_64-w64-mingw32_libwallet2_api_c.dll"
# MinGW runtime DLLs (libssp-0, libwinpthread-1) — the wallet libs
# are dynamically linked against these. MrCyjaneK's prebuilt
# release-bundle.zip does NOT ship them (only the wallet *.dll +
# *.dll.a import libs), so we source them from Git Bash's bundled
# MinGW-w64 distribution, which ships the same x86_64-w64-mingw32
# ABI used to build the wallet libs.
# ORDERING MATTERS: monero_c's prebuilt wallet DLLs are built against
# recent MinGW-w64 (post-2022 winpthreads ABI with pthread_cond_timedwait64).
# Flutter's bundled mingit ships a 2017-vintage libwinpthread that's
# missing newer symbols and crashes wallet creation at runtime. Prefer
# Git for Windows' mingw64/bin which is updated frequently.
RUNTIME_PATHS=(
"/c/Program Files/Git/mingw64/bin" # Git for Windows — modern MinGW runtime (preferred)
"$SRC" # monero_c bundle (in case it grows DLLs)
"/c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/bin" # choco install -y mingw
"/c/flutter/bin/mingit/mingw64/bin" # Flutter's bundled mingit — old (2017), use only as last resort
"/mingw64/bin" # MSYS2 fallback
)
for runtime in libssp-0.dll libwinpthread-1.dll; do
src=""
for d in "${RUNTIME_PATHS[@]}"; do
if [[ -f "$d/$runtime" ]]; then
src="$d/$runtime"
break
fi
done
if [[ -z "$src" ]]; then
echo "FATAL: $runtime not found in any of:"
printf ' %s\n' "${RUNTIME_PATHS[@]}"
for d in "${RUNTIME_PATHS[@]}"; do
echo "--- contents of $d (if exists) ---"
ls "$d" 2>/dev/null | head -20 || echo "(missing)"
done
exit 1
fi
cp -v "$src" "$MONERO_DST/x86_64-w64-mingw32_${runtime}"
done
echo "=== final staged layout ==="
ls -la "$MONERO_DST/" "$WOWNERO_DST/"
# ---- Configure: pubspec.yaml + per-coin enablement -------------------
# Mirror of hashwallet.bat — drives tool/configure.dart with the same
# set of coin flags that upstream Cake's Windows build uses.
- name: Configure pubspec + Wallet types
run: |
set -x -e
cp -f pubspec_description.yaml pubspec.yaml
flutter pub get
dart run tool/generate_pubspec.dart
flutter pub get
dart run tool/configure.dart \
--monero --bitcoin --ethereum --polygon --nano --bitcoinCash \
--wownero --dogecoin --base --arbitrum --bsc
# ---- Secrets ---------------------------------------------------------
- name: Generate per-module secrets.g.dart files (empty defaults)
run: dart run tool/generate_new_secrets.dart
- name: Inject Trocador affiliate secrets into lib/.secrets.g.dart
env:
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }}
TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }}
run: |
# Length-only debug visibility — never prints actual secrets.
echo "TROCADOR_API_KEY length: ${#TROCADOR_API_KEY}"
echo "TROCADOR_MONERO_API_KEY length: ${#TROCADOR_MONERO_API_KEY}"
echo "TROCADOR_EXCHANGE_MARKUP length: ${#TROCADOR_EXCHANGE_MARKUP}"
if [[ -z "$TROCADOR_API_KEY" ]]; then
echo "WARN: TROCADOR_API_KEY not reaching runner — build will proceed"
echo " but Trocador exchange features won't work in this binary."
fi
sed -i \
-e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \
-e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \
-e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \
lib/.secrets.g.dart
grep '^const trocador' lib/.secrets.g.dart
# ---- Codegen ---------------------------------------------------------
- name: Build generated code (mobx + hive adapters)
run: bash model_generator.sh
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# ---- Build the Windows .exe ------------------------------------------
- name: Build Windows release
run: |
set -x -e
flutter config --enable-windows-desktop
flutter build windows --dart-define-from-file=env.json --release --verbose
# ---- Reconcile monero/wownero DLL naming for Dart FFI ----------------
# windows/CMakeLists.txt's install(FILES ... RENAME ...) rules produce
# <coin>_libwallet2_api_c.dll, but package:monero's Dart FFI loader
# (mrcyjanek's package) opens lib<coin>_wallet2_api_c.dll — different
# filename layout, runtime error "Failed to load dynamic library".
# Create the lib-prefixed copies so both name conventions resolve.
- name: Mirror monero/wownero DLLs under lib-prefixed names
run: |
set -x -e
DST="build/windows/x64/runner/Release"
for coin in monero wownero; do
src="$DST/${coin}_libwallet2_api_c.dll"
dst="$DST/lib${coin}_wallet2_api_c.dll"
if [[ -f "$src" ]]; then
cp -v "$src" "$dst"
else
echo "WARN: $src not in Release dir — CMake install may have skipped it"
ls "$DST" | head -20
fi
done
# ---- Bundle MSVC runtime DLLs with the .exe --------------------------
# Standalone Windows builds need msvcp140.dll + vcruntime140.dll +
# vcruntime140_1.dll next to the .exe (or installed via vc_redist on
# the user's machine). Bundling avoids the "VCRUNTIME140.dll not found"
# error on machines without VC++ redistributable installed.
- name: Copy MSVC runtime DLLs next to the .exe
run: |
set -x -e
# Find the redist dir for the currently-installed MSVC version (the
# version number rolls forward with VS updates, so do NOT hardcode).
REDIST_BASE="/c/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Redist/MSVC"
REDIST_DIR=$(find "$REDIST_BASE" -type d -name 'Microsoft.VC*.CRT' -path '*/x64/*' 2>/dev/null | sort | tail -1)
if [[ -z "$REDIST_DIR" ]]; then
echo "FATAL: could not find VC redist dir under $REDIST_BASE"
ls "$REDIST_BASE" 2>/dev/null || echo "(redist base dir missing entirely)"
exit 1
fi
echo "Using redist dir: $REDIST_DIR"
cp -v "$REDIST_DIR/msvcp140.dll" "$REDIST_DIR/vcruntime140.dll" "$REDIST_DIR/vcruntime140_1.dll" \
build/windows/x64/runner/Release/
# ---- Package + upload ------------------------------------------------
- name: List built artifacts (for debug visibility)
run: |
ls -la build/windows/x64/runner/Release/
# ---- Package + upload as zip -----------------------------------------
# The Flutter Windows build produces a directory tree (~500 files —
# HashWallet.exe + plugin DLLs + data/ + locales/ + bundled monero_c
# + MinGW runtime DLLs). Ship as a zip; users extract and run.
#
# Tried wrapping with 7-Zip SFX for a single-file "portable" .exe
# earlier but path_provider_windows computes the wallet storage dir
# from the .exe location, and SFX extracts to a different %TEMP%
# subdir on every launch — wallet keys couldn't be found across
# runs. Multi-file zip is what Firefox Portable / VS Code Portable
# / every other Flutter Windows desktop app does too.
- name: Create Hash Bags Windows release zip
run: |
set -x -e
cd build/windows/x64/runner/Release
# PowerShell's Compress-Archive is in PATH; no extra deps needed.
powershell -NoProfile -Command "Compress-Archive -Path .\* -DestinationPath ..\..\..\..\..\HashBags-windows-${{ github.sha }}.zip -Force"
cd "$GITHUB_WORKSPACE"
ls -la HashBags-windows-*.zip
echo "Size: $(du -h HashBags-windows-${{ github.sha }}.zip | cut -f1)"
- name: Upload Hash Bags Windows zip
uses: actions/upload-artifact@v3
with:
name: hash-bags-windows-${{ github.sha }}
path: HashBags-windows-${{ github.sha }}.zip
retention-days: 14

1
.gitignore vendored
View File

@@ -233,6 +233,7 @@ scripts/reown_flutter
# Hash Wallet: untracked notes / internal playbooks
docs/links.md
docs/UPSTREAM_SYNC.md
scripts/windows/setup-windows-runner.ps1
# Hash Wallet: local mirror of hash.boats website (deploy target on suchwow server)
hash.boats/

View File

@@ -60,7 +60,7 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="cakewallet"
android:scheme="hashbags"
android:host="y.at" />
</intent-filter>
<!-- currencies qr code scheme -->

View File

@@ -1,4 +1,9 @@
org.gradle.jvmargs=-Xmx6144M
# AAB universal-bundle packaging (AabFlinger.writeZip) OOMs at 6G because
# parallel ForkJoinPool workers all share the heap while buffering compressed
# native libs. Bumped to 10G + capped worker count to flatten the peak.
# linux-beast Docker container has plenty of RAM; bump higher if needed.
org.gradle.jvmargs=-Xmx10240M -XX:MaxMetaspaceSize=1024M -Dfile.encoding=UTF-8
org.gradle.workers.max=2
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Wie kontaktiere ich den Hash Wallet-Support?",
"answer" : "Senden Sie eine E-Mail an support@cakewallet.com, schließen Sie sich dem Telegramm unter @cakewallet_bot an oder twittern Sie @CakeWalletXMR!\n"
"answer" : "Senden Sie eine E-Mail an support@such.software, schließen Sie sich dem Telegramm unter @such_software an oder twittern Sie @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "How do I contact Hash Wallet support?",
"answer" : "Email support@cakewallet.com, join the Telegram at @cakewallet_bot, or tweet @CakeWalletXMR!\n"
"answer" : "Email support@such.software, join the Telegram at @such_software, or tweet @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "¿Cómo contacto al soporte de Hash Wallet?",
"answer" : "¡Envíe un correo electrónico a support@cakewallet.com, únase al Telegram en @cakewallet_bot o envíe un tweet a @CakeWalletXMR!\n"
"answer" : "¡Envíe un correo electrónico a support@such.software, únase al Telegram en @such_software o envíe un tweet a @such_software!\n"
}
]

View File

@@ -54,6 +54,6 @@
},
{
"question" : "Comment puis-je contacter le support de Hash Wallet ?",
"answer" : "Envoyez un email à support@cakewallet.com, rejoignez le groupe Telegram @cakewallet_bot, ou @CakeWalletXMR sur Twitter!\n"
"answer" : "Envoyez un email à support@such.software, rejoignez le groupe Telegram @such_software, ou @such_software sur Twitter!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "मैं केक वॉलेट से कैसे संपर्क करूं?",
"answer" : "ईमेल support@cakewallet.com, @cakewallet_bot पर टेलीग्राम में शामिल हों, या @CakeWalletXMR पर ट्वीट करें!\n"
"answer" : "ईमेल support@such.software, @such_software पर टेलीग्राम में शामिल हों, या @such_software पर ट्वीट करें!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Hash Walletサポートに連絡するにはどうすればよいですか",
"answer" : "support@cakewallet.comにメールを送信するか、@cakewallet_botで電報に参加するか、@CakeWalletXMRにツイートしてください。\n"
"answer" : "support@such.softwareにメールを送信するか、@such_softwareで電報に参加するか、@such_softwareにツイートしてください。\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Hash Wallet 지원팀에 연락하려면 어떻게해야합니까?",
"answer" : "support@cakewallet.com로 이메일을 보내거나 @cakewallet_bot에서 전보에 가입하거나 @CakeWalletXMR을 트윗하십시오!\n"
"answer" : "support@such.software로 이메일을 보내거나 @such_software에서 전보에 가입하거나 @such_software을 트윗하십시오!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Hoe neem ik contact op met Hash Wallet-ondersteuning?",
"answer" : "E-mail support@cakewallet.com, word lid van het Telegram op @cakewallet_bot of tweet @CakeWalletXMR!\n"
"answer" : "E-mail support@such.software, word lid van het Telegram op @such_software of tweet @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Jak skontaktować się z obsługą Hash Wallet?",
"answer" : "Wyślij e-mail na adres support@cakewallet.com, dołącz do telegramu na @cakewallet_bot lub tweet @CakeWalletXMR!\n"
"answer" : "Wyślij e-mail na adres support@such.software, dołącz do telegramu na @such_software lub tweet @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Como entro em contato com o suporte da Hash Wallet?",
"answer" : "Envie um e-mail para support@cakewallet.com, participe do Telegram em @cakewallet_bot ou envie um tweet para @CakeWalletXMR!\n"
"answer" : "Envie um e-mail para support@such.software, participe do Telegram em @such_software ou envie um tweet para @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Как мне связаться со службой поддержки Hash Wallet?",
"answer" : "По электронной почте support@cakewallet.com, присоединитесь к Telegram по адресу @cakewallet_bot или отправьте твит @CakeWalletXMR!\n"
"answer" : "По электронной почте support@such.software, присоединитесь к Telegram по адресу @such_software или отправьте твит @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "Як мені зв'язатися зі службою підтримки Hash Wallet?",
"answer" : "По електронній пошті support@cakewallet.com, приєднайтеся до Telegram за адресою @cakewallet_bot або надішліть твіт @CakeWalletXMR!\n"
"answer" : "По електронній пошті support@such.software, приєднайтеся до Telegram за адресою @such_software або надішліть твіт @such_software!\n"
}
]

View File

@@ -53,6 +53,6 @@
},
{
"question" : "如何联系蛋糕钱包支持?",
"answer" : "电子邮件support@cakewallet.com通过@cakewallet_bot加入电报或在@CakeWalletXMR上发布推文!\n"
"answer" : "电子邮件support@such.software通过@such_software加入电报或在@such_software上发布推文!\n"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -5,14 +5,18 @@
hash_wallet_logos.svg (#1a5c38). iOS/Android round the corners
automatically; we ship a square fill. -->
<rect width="1024" height="1024" fill="#1a5c38"/>
<!-- # mark, white, centered. ui-monospace falls back to the platform's
default monospace; the rasterizer bakes this into a PNG so font
availability on user devices doesn't matter. -->
<text x="512" y="512"
text-anchor="middle"
dominant-baseline="central"
font-family="ui-monospace, 'JetBrains Mono', 'Courier New', monospace"
font-size="720"
font-weight="700"
fill="#ffffff">#</text>
<!-- # mark, drawn as 4 vector lines centered at (512, 512). Previous
version used <text> with dominant-baseline="central", but the
rendered # was visually biased toward the top because monospaced
font glyph metrics don't put the # at the geometric center of the
em-box. Path-based rendering removes the font dependency
entirely. -->
<g stroke="#ffffff" stroke-width="80" stroke-linecap="square">
<!-- horizontal bars (top, bottom) — at y = 512 ± 100 -->
<line x1="212" y1="412" x2="812" y2="412"/>
<line x1="212" y1="612" x2="812" y2="612"/>
<!-- vertical bars (left, right) — at x = 512 ± 100 -->
<line x1="412" y1="212" x2="412" y2="812"/>
<line x1="612" y1="212" x2="612" y2="812"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 861 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More