Unified flutter_rust_bridge bindings for Bitcoin and Liquid wallet operations.
Over the past years, we contributed to the open source community by creating flutter_rust_bridge bindings for several Bitcoin/Liquid libraries:
- ark-wallet-dart — Ark protocol
- bbqr-dart — Animated QR encoding
- boltz-dart — Boltz atomic swaps
- lwk-dart — Liquid Wallet Kit
Each of these packages works standalone and is used by other projects in the community.
However, our wallet app Bull Bitcoin depends on all of them. This creates two problems:
- Build time — Each package compiles its own native library (
.so/.dylib). Building them all individually is slow, especially for Android where each targets 3 architectures. - Binary size — Shared Rust dependencies (bitcoin, secp256k1, tokio, openssl, etc.) are duplicated across each native library.
bull_sdk is a single flutter_rust_bridge package that generates unified Dart bindings by scanning the Rust API of each sub-crate as external dependencies:
# flutter_rust_bridge.yaml
rust_input: crate::api, ark_wallet::ark, bbqr, dart_bbqr::api, boltz::api, lwk::apiThis produces one native library containing all the Rust code, with one FRB dispatcher handling all FFI calls.
- Standalone packages are preserved — Each sub-crate remains a fully functional standalone flutter_rust_bridge package. The community can keep using
boltz-dartorlwk-dartindependently. - Sub-crate
frb_generatedis cfg-gated — When used as a dependency ofbull_sdk, each sub-crate'sfrb_generated.rsis disabled via#[cfg(not(feature = "bull_sdk"))]to avoid duplicate trait implementations.bull_sdkprovides its own unifiedfrb_generated.rs. - Mirror types for data-variant enums — Rust enums with data (like
TxFee,ArkTransaction) become opaque when scanned as external crate types. We use#[frb(mirror)]to generate proper sealed Dart classes. - Post-processing — After FRB codegen,
fix_frb_generated.shpatches the generated Rust code to wrap error types inFrbWrapperand convert mirrored types via.into().
bull-sdk/
├── Cargo.toml # Cargo workspace
├── packages/
│ ├── bull_sdk/ # Unified FRB package (single native library)
│ │ ├── flutter_rust_bridge.yaml
│ │ ├── fix_frb_generated.sh # Post-processing script
│ │ ├── rust/ # Bridge crate
│ │ └── lib/
│ │ ├── bull_sdk.dart # BullSdk.init()
│ │ ├── bbqr.dart # BBQr types
│ │ ├── bitbox.dart # BitBox types
│ │ ├── boltz.dart # Boltz swap types
│ │ ├── lwk.dart # Liquid Wallet Kit types
│ │ ├── bdk.dart # Re-exports the SDK-pinned bdk_dart
│ │ └── main.dart # flutter_rust_bridge template demo app
│ ├── bbqr/ # git submodule → SatoshiPortal/bbqr-dart
│ ├── bitbox/ # git submodule → SatoshiPortal/bitbox-dart
│ ├── boltz/ # git submodule → SatoshiPortal/boltz-dart
│ ├── lwk/ # git submodule → SatoshiPortal/lwk-dart
│ ├── bitbox-transport/ # Pure Dart — BitBox transport
│ ├── boltz-stream/ # Pure Dart — BoltzWebSocket (depends on bull_sdk)
│ └── satoshifier/ # Pure Dart — payment-string parsers
The submodules are not a build input: cargo resolves each crate from the rev
pinned in packages/bull_sdk/rust/Cargo.toml, which is the source of truth, and
no Dart path dependency points into packages/. Keep a submodule pointer equal
to its cargo rev so the checked-out source matches what actually builds.
cd packages/bull_sdk
flutter_rust_bridge_codegen generate
bash fix_frb_generated.sh
cargo check -p rust_lib_bull_sdkAlways run fix_frb_generated.sh after codegen — it patches error type wrapping and mirror type conversions that FRB cannot handle automatically for external crate types.
import 'package:bull_sdk/bull_sdk.dart';
import 'package:bull_sdk/boltz.dart' as boltz;
import 'package:bull_sdk/lwk.dart' as lwk;
import 'package:bull_sdk/bbqr.dart' as bbqr;
import 'package:bull_sdk/ark.dart' as ark;
await BullSdk.init();