A Flutter SDK for Zcash - FFI over librustzcash, with a drop-in shielded wallet UI

Hi all! I’ve been following the ecosystem and the LCWG side for a while, and I recall the Flutter SDK gap coming up on one of the LCWG calls, @pacu, I think it was you who flagged it; correct me if I’m misremembering. So this might be of interest.

How it started: I’m building yet another decentralised messenger - the thousand-and-first, I know - and I wanted users to be able to send ZEC to each other. So I went looking for a straightforward way to do shielded ZEC in Flutter, and there basically wasn’t one. Zodl is excellent, but it’s a wallet app built on the native SDKs - on android and iOS. If your app is Flutter, there’s nothing to embed. So I built the wallet into the messenger, then pulled it out as a standalone SDK - partly because it clearly wanted to be one, partly so nobody else has to rediscover this hole the way I did.

Background, briefly: about two decades building and shipping products across information security, blockchain analytics, and decentralised systems. Some of those years were spent on the analytics side - the business of extracting signal from transparent chains - which is a big part of why I ended up building on the one chain designed to make that hard

It’s three packages (MIT/Apache-2.0, all of it):

  • zec_wallet - the core Dart SDK: a WalletHandle API over a rust engine (librustzcash - zcash_client_backend / zcash_client_sqlite), with the native library bundled inside the Dart package rather than published as separate crates. Unified addresses, shielded send/receive (Orchard + Sapling), lightwalletd sync. The headline rule: seeds and spending keys never enter Dart - Dart sees addresses, balances, statuses, and typed errors. That boundary is enforced by CI policy tests, not by good intentions.
  • zec_wallet_ui - a drop-in Flutter UI: onboarding, backup, the full wallet surface, send/receive/shield/swap, 16 locales. It ships no native code of its own - platform concerns go through host seams.
  • zec_wallet_ui_platform - the optional native companion: Android FLAG_SECURE, iOS privacy cover and backup exclusion. Hosts that want full control simply omit it.

Both layers come with a complete example wallet app (boot, theming, routing — the whole integration path), and the messenger consumes the identical package surface: the host maps its theme, wires a session seam, and implements zero wallet logic of its own.

Where it sits relative to existing work, since dart tooling isn’t a blank slate:

The approach is deliberately the opposite of a pure-dart protocol port. sapling-crypto and orchard are used as-is, so the consensus- and proof-critical code stays in the audited upstream rather than being reimplemented in Dart. That sidesteps the proof-generation performance problem a pure-Dart port runs into, and means nothing has to be hand-synced when the protocol moves. The cost is the honest one: a heavier native build per platform and an FFI surface to maintain. So the trade-offs line up as pure-dart (simpler, more portable) vs. native-per-platform like the official Kotlin/Swift SDKs (fast, but two codebases) vs. this -aiming for audited-upstream parity and cross-platform from a single codebase.

Status: the SDK currently lives as a self-contained package set inside the messenger’s monorepo (zero messenger dependencies, that boundary is CI-enforced too). Core and UI carry around 2,300 tests. The money paths have been proven on mainnet with real ZEC: shielded sends including ZIP-320 two-step (TEX) transactions, a force-kill mid-send that recovered to exactly one pair on-chain, an offline send queue, and detection plus recovery sweeps of funds landing on already-used ephemeral addresses - all verified on-chain. Total losses across the exercise: transaction fees, which I’m told is how it’s supposed to work. It runs on-device on Android and iOS, including StrongBox / Secure Enclave-backed key custody. I’m extracting it into a standalone repo for public release and will post the link in this thread; happy to give committee members access to the pre-release tree in the meantime.

What a grant would cover - the SDK was built to be embedded from day one, and it already has two consumers: the messenger it grew out of, and the example app that doubles as the reference integration. But both are mine, and “designed for embedding” and “picked up cold by someone who isn’t the author” are very different claims. That’s the gap:

  1. A clean-room reference integration: a fresh app built strictly against the published packages on a clean machine, the exact path a third-party dev takes, plus the API, docs, and packaging fixes that surfaces
  2. CI and publication to pub.dev, including prebuilt native binaries per platform, so a flutter developer never needs a Rust toolchain
  3. Docs, examples, a semver contract, and native-speaker review of the 15 non-English locales.
  4. External security review and remediation
  5. A maintenance commitment for a defined period

Before I write a full application: does this look like a fit for ZCG, and is the reference-integration-plus-security-review framing the right shape, or would you draw the scope somewhere else? The wishlist mentions shielded wallets for smartphones and network-layer privacy for light-client SDKs, which is what pointed me here

Thanks for reading

1 Like

The rule that seeds and spending keys never cross into Dart, with CI tests enforcing that boundary, is the right instinct for a wallet SDK. Do the viewing keys used for sync stay on the Rust side too, or does Dart hold them to render balances and history?

Yes, they stay in Rust.

The rust core owns the wallet database (zcash_client_sqlite over zcash_client_backend). The account’s UFVK is derived from the seed in Rust and stored there. Sync runs entirely on that side: scan_cached_blocks, trial decryption, note and nullifier bookkeeping. Dart never sees the UFVK.

What comes back across the bridge is the output of the scan: balances in zatoshi, spendable amounts, transaction history rows, memos, addresses. Ints, strings, status enums

The one bit of key material that does cross is the mnemonic, in two places: restore, and the backup screen. Both as a List, not persisted on the dart side. Signing and scanning never see it

If the UFVK never leaves Rust, what does the host app get when a user wants to export a viewing key for their own accounting, or is that deliberately out of scope for the SDK?

Yes, it’s out of scope at least for v1 on purpose.

Basically history rows already carry the txid, signed amount, fee, timestamp and so on, so in app accounting doesn’t need a key at all. And UFVK isn’t something that we want to fall out of a getter. If it’s ever exposed it’d be a deliberate user action - so same footing as revealing the backup phrase. Probably the case with watch-only is the case which would actually want one and even UIVK wouldn’t cover it. I dunno if anyone really wants it. So after v1.0 either way, and it’d go through the same CI gate as an allowlisted exception, the way restore and the backup screen are allowlisted for the mnemonic today

Putting a UFVK export behind a deliberate user action, same footing as the backup phrase, sounds right. On whether anyone wants one, the case I keep running into is handing a viewing key to an outside bookkeeper, so would watch only be the shape you build for after v1?

Yeah, watch-only is what’s on the list for after v1. Though for a bookkeeper I’d check what they actually need first. Most of the time it’s the records not the key - and the app already has every row with data so a CSV covers it. Key only really matters if they have to verify against the chain themselves instead of trusting your export.
So anyway both consumers are mine at the moment: the messenger and the example app. And it’s the messenger’s needs that set the order right now. Which is part of why the maintenance line is in the ask. If people embedding it actually want things I’d rather that drove the queue than my own roadmap. Stuff like this thread, basically

@ZcashGrants ,while I’m here, the two questions at the end of my first post are still open if anyone has a minute (does this look like a fit for ZCG and is the reference-integration-plus-security-review framing the right shape)

Fyi, there are several wallets that use flutter for UI and librustzcash for BL.

1 Like

Yeah fair, should’ve said that better. Thing is I was the guy trying to put zcash into a flutter app while those existed and none of them helped easily. Everything I found was a wallet you install, not something you can depend on, so I ended up building it anyway. That’s the gap I meant - not that nobody’s wired flutter to librustzcash, but that when they do it’s welded to their app and the next person basically starts from zero.

If any of them publishes as a package I’d want to look, that changes what I should be doing. Otherwise what I’m after is a shop app or a messenger just adding a dependency and taking/sending ZEC, without having to become a wallet team first

1 Like

Does that move watch only up the queue, or is it after v1 either way? Verifying against the chain instead of trusting the export is the whole point of an outside audit, so that case shows up any time the reader does not work for the company.

1 Like

Yeah, it moved and actually done. So will be in the first release.

Honestly didn’t take much - the sync engine already does watch-only, I just never wired an export to it. So it was the export/import path plus the gating around it. You get your UFVK out as a uview string, ZIP-316 so it’s not tied to my SDK, and you paste or scan it into another instance which syncs against the chain itself.

And one thing that annoys me: uview doesn’t carry a birthday height. So the api makes you pass a height and the ui defaults to six months with a warning to go earlier if the wallet’s older.
So just need more time for overall polishing the SDK and then I’ll release