Shared wallet packages for Skylight Wallet (Monero-only) and Spice Wallet (multi-coin).
This repo contains the shared components for both apps, which makes maintenance easier. Each app only pulls the specific pieces that they need.
packages/
wallet_infra/ transport, storage, logging, at-rest crypto (no coin or app knowledge)
wallet_domain/ CryptoWallet, WalletManager, seeds, stores, alias policy
wallet_monero/ Monero: light-wallet server and full node; polyseed, BIP39, 25-word
wallet_bitcoin/ Bitcoin and testnet: BIP84 over Electrum
wallet_ethereum/ Ethereum, Sepolia and ERC-20: JSON-RPC plus Blockscout history
wallet_openalias/ OpenAlias resolution over Tor, DNSSEC-validated (Rust FFI)
wallet_fhse/ FHSE: security keys (FIDO2 hmac-secret) guarding the wallet password (C FFI)
wallet_backup/ metadata backup: payment destinations, tx keys and contacts as sealed files (pure Dart)
wallet_backup_platform/ the backup's iCloud location (Swift), Auto Backup folder, backup file, screens
wallet_background/ background sync and incoming-transaction notifications, view-only
wallet_fiat/ fiat exchange rates from Kraken
wallet_ui/ shared Flutter widgets, the route policy and the payment-link gate
scripts/ reproducible-build tooling for monero_c and the Tor toolchain
Dockerfile.builder pinned build image
test_fixtures/ manifest for the generated Monero test wallets
One package per capability, so an app compiles only what it uses.
Alias resolution is split in two. wallet_domain holds the policy: bounds,
sanitization, per-coin address validation, and a Tor gate that fails closed.
wallet_openalias holds the resolver, which is a Rust build and would otherwise
drag a Rust toolchain into every consumer. AliasResolver is the seam; the app
installs resolveOpenAlias in main().
Anything that runs without the user is view-only: a Periodic run and Android's
Continuous Sync (the "Sync Frequency" setting) open Monero's view-only
background copy (or ask its light-wallet server with the view key), a Bitcoin
account xpub or an Ethereum address, never the wallet password, the seed or a
spend key. When the spend wallet locks, the app's own wallets drop their
spending keys the same way. packages/wallet_background/README.md has what
each Sync Frequency runs, per platform and per coin, and the details.
Logging is a privacy surface. Seeds, keys and raw server payloads never reach a
log; addresses and transaction ids only as salted fingerprints; amounts only as
an order of magnitude. Redact in wallet_infra is the toolkit.
Requires Flutter 3.47.7. The packages form a Dart pub workspace, so one command at the root resolves all of them.
flutter pub get
flutter analyze
dart format packages/*/lib packages/*/test packages/*/toolRun the tests for one package from its own directory:
cd packages/wallet_domain && flutter testFormat the three directories listed above rather than .. dart format reads
page_width from analysis_options.yaml but ignores its exclude:, so .
rewrites the vendored cargokit build tool under wallet_openalias.
Add a gitignored pubspec_overrides.yaml to the app repo, pointing at your
local clone:
dependency_overrides:
# Every wallet-core package the app's pubspec.yaml pins (all twelve for
# Spice; Skylight leaves out wallet_bitcoin and wallet_ethereum):
wallet_infra:
path: ../wallet-core/packages/wallet_infra
wallet_domain:
path: ../wallet-core/packages/wallet_domain
wallet_monero:
path: ../wallet-core/packages/wallet_monero
wallet_openalias:
path: ../wallet-core/packages/wallet_openalias
wallet_bitcoin:
path: ../wallet-core/packages/wallet_bitcoin
wallet_ethereum:
path: ../wallet-core/packages/wallet_ethereum
wallet_fhse:
path: ../wallet-core/packages/wallet_fhse
wallet_backup:
path: ../wallet-core/packages/wallet_backup
wallet_backup_platform:
path: ../wallet-core/packages/wallet_backup_platform
wallet_background:
path: ../wallet-core/packages/wallet_background
wallet_fiat:
path: ../wallet-core/packages/wallet_fiat
wallet_ui:
path: ../wallet-core/packages/wallet_ui
# Plus every override from the app's own pubspec.yaml, copied verbatim:
# hashlib, bip39, blockchain_utils, web3dart, intl. See the table below.pubspec_overrides.yaml replaces pubspec.yaml's dependency_overrides
rather than merging with them, so it has to restate every override the app
already declares, not just the paths. Dropping one usually fails at resolution;
dropping hashlib is worse, because it resolves fine and then fails as a
compile error inside polyseed.
These packages are not published, so an app depends on them by git SHA with a
path: into the repo.
wallet_monero:
git:
url: https://github.com/MAGICGrants/wallet-core
ref: <sha>
path: packages/wallet_moneroApps must repeat this repo's dependency_overrides in their own pubspec.yaml.
Pub honours overrides only in the package it resolves from, so the ones here
apply when you run flutter pub get in this repo and nowhere else. Which ones
you need depends on the packages you take:
| Override | Needed when you depend on |
|---|---|
hashlib: 1.19.2 |
wallet_domain or wallet_monero (both pull polyseed), so also wallet_backup |
bip39 at the cypherstack/stack-bip39 SHA |
any coin package or wallet_domain |
blockchain_utils at the cake-tech SHA |
wallet_bitcoin, wallet_ethereum or wallet_monero |
web3dart at the cake-tech SHA |
wallet_ethereum |
intl: 0.20.3 |
wallet_bitcoin (its bitcoin_base asks for intl: ^0.19.0, which it never imports) |
Copy the values from the root pubspec.yaml.
Everything is pinned to a commit to make builds verifiable.
monero_c:magicgrants/monero_c@757bf8e, a fork carryingWallet_estimateTransactionFee.tor_ffi_plugin:MAGICGrants/tor@72e455f, a fork ofcypherstack/tornative-0.2.0whose build hook sets a fixedSOURCE_DATE_EPOCHand remaps the cargo home, so the Tor library is reproducible.bip39:cypherstack/stack-bip39@0cd6d54, which fixes an entropy bug upstream still has.
scripts/ holds the build tooling, since this repo owns the pins. Apps get it
as a git submodule. The F-Droid recipe already sets submodules: true, so
nothing changes there, and F-Droid still builds monero_c from source.
Two of the scripts look odd:
pin-tor-rust-toolchain.shpatches cargokit after it is fetched. cargokit hardcodesrustup run stable, a channel that moves over time, and its config accepts only a channel name rather than an exact version, so patching the fetched package is the only way to fix the compiler version. Sincenative-0.2.0Tor no longer uses cargokit: its native-assets build hook builds with the toolchain pinned in the package's ownrust-toolchain.toml(Rust 1.99.1) andcargo --locked, so the script now matters only forwallet_openalias, whose cargokit copy it patches the same way.fix-linux-moneroc-execstack.shclears the executable-stack flag on the Linux monero_c library. Without it, hardened kernels refuse to load the library.
Two more guard TLS:
check-moneroc-openssldir.shfails when a monero_c library's OpenSSL would look for certificates oropenssl.cnfunder its build directory. CI builds in/tmp, so on a user's machine that path is one any local account can create.update-cacert.shrefreshes the CA bundle (see below).
Every TLS connection to an LWS or a node is verified against one CA set,
Mozilla's, shipped as packages/wallet_infra/assets/cacert.pem. The native
library gets it as a file (CaBundle.path(), written into the app's own
directory); Dart's requests to the same server use CaBundle.securityContext().
The library's OpenSSL cannot use the Android or iOS store, so this is the only
way to verify there, and using it everywhere keeps the platforms alike.
Refresh the bundle at least once a year with scripts/update-cacert.sh; the
nightly job fails once it is 400 days old.
package:wallet_monero/testing.dart holds the TLS checks: LWS and node, direct
and through SOCKS, against loopback servers with a throwaway PKI
(package:wallet_infra/testing.dart, regenerated by
packages/wallet_infra/tool/make_test_pki.sh). They run against a host build
here and against the shipped binaries in each app:
cd packages/wallet_monero
MONERO_LIB_PATH=/path/to/libwallet2_api_c.so flutter test test/native_tls_ffi_test.darttest/lwsf_send_ffi_test.dart has LWSF build, sign and submit real
transactions through the wallet's backend, against a loopback light-wallet
server (test/support/fake_lws.dart) that reports outputs the test wallet
really owns. It covers a send with change from the first account, the same from
a second account, and a sweep of each account back to itself; Monero refuses
to build any of them if LWSF gets the change address wrong. It then reads the
submitted transaction back and checks that every output pays who it should,
and opens to the right amount, and that the amount the wallet reports sending,
before and after the send, is the amount the transaction pays.
It needs a host build. The backend loads the library by name in each isolate
it spawns, so its directory goes on the loader path too, as in native.yml:
cd packages/wallet_monero
MONERO_LIB_PATH=/path/to/monero_libwallet2_api_c.so LD_LIBRARY_PATH=/path/to \
flutter test test/lwsf_send_ffi_test.dartTen prebuilt wallets that pin the seed-to-address derivation. Each is a real wallet directory in one state (restored from a polyseed, from a 25-word seed, node mode, a corrupt cache) plus the answer the code should give for it. A changed address means the derivation changed, which is otherwise silent: the wallet still opens, it just belongs to someone else.
Generated rather than committed; only the manifest is tracked. The seeds are published and the wallets are stagenet, so none can hold real value.
cd packages/wallet_monero
dart run tool/make_fixtures.dart # generate
dart run tool/make_fixtures.dart --verify # check against the tracked manifestSeven of the ten need a real Monero library, so set MONERO_LIB_PATH to the
built libwallet2_api_c. Without it the generator builds only the three that
need no FFI, and rewrites the tracked manifest without the rest. CI is a
fresh checkout, so that is harmless there. Locally, run git checkout -- test_fixtures/wallets/MANIFEST.json afterwards and do not commit it.
Recorded addresses are cross-checked against a second, independent Monero implementation, so this does not just check monero_c against itself.
ci.yml runs on every push: analysis, formatting and the unit tests for every
package (wallet_fhse's against its native library, built in the job), plus
cargo test for the OpenAlias resolver and a native-crypto job.
Everything there uses FakeMoneroBackend rather than a real Monero library.
native.yml builds monero_c and runs wallet_monero's tests against it,
generating the test wallets on the way, and runs the TLS checks against the
Windows build on Windows. It runs nightly rather than per-push, because
building Monero from source takes tens of minutes. So between nightly runs a
change to wallet_monero has only been tested against the fake.
Each app's native-tls.yml runs the same TLS checks inside the app on an
Android emulator, an iOS simulator, Linux and Windows, against the binaries it
ships. It runs on pull requests that change those binaries or the lockfile.
build-monero-c.yml is manual (workflow_dispatch). It builds the library for
each target and uploads it as an artifact. Use it when you want a library to
develop against locally.