Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,42 @@ jobs:
- run: go vet ./...
- run: go test -race -timeout 60s ./...

android:
name: Android example build
runs-on: ubuntu-latest
defaults:
run:
working-directory: example
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
channel: stable
cache: true
- run: flutter pub get
- run: flutter build apk --debug

ios:
name: iOS example build
runs-on: macos-latest
defaults:
run:
working-directory: example
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
channel: stable
cache: true
- run: flutter pub get
- run: flutter build ios --no-codesign

yaml-lint:
name: Workflow YAML lint
runs-on: ubuntu-latest
Expand Down
44 changes: 38 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
## 0.0.8

* Android: run the blocking `initBitBox` (Noise pairing handshake) off the serial
MethodChannel task queue so concurrent `getChannelHash` polls are serviced while
init waits for the on-device confirmation. The pairing code now appears in the app
and on the device simultaneously instead of only after confirming on the device
(iOS was already unaffected). `initBitBox` now also propagates the real init result.
* Android: show the pairing code in the app and on the device simultaneously by
running the blocking Noise pairing handshake off the serial MethodChannel queue
so concurrent `getChannelHash` polls are serviced while init awaits confirmation.
* Refactor the BitBox testkit API surface and regenerate the gomobile artefacts.
* Drop `print()` from production `lib/` and harden `flutter analyze` to `--fatal-infos`.
* Add `CONTRIBUTING.md` aligning with the BitBox-stack workflow.

## 0.0.7

* Add a reusable BitBox testkit.

## 0.0.6

* Rename the Go module path to `github.com/DFXswiss/bitbox_flutter`.
* CI: add Go tests, `dart format`, YAML lint, and workflow permission lockdown.

## 0.0.5

* BLE: dedup single-frame init retransmits and add panic recovery for `GetDevice`.

## 0.0.4

* Add the pull-request CI workflow and `BIPPath` smoke tests.

## 0.0.3

* BLE read path: scoped deduplication plus a 60s timeout for long signing flows.

## 0.0.2

* BLE read path: remove broken packet deduplication.
* BLE write: use guard-unwrapped locals to prevent crashes.

## 0.0.1

* TODO: Describe initial release.
* Initial release of the BitBox02 Flutter plugin.
* Support BTC, LTC, ETH, and ERC20 chains over a gomobile-exported Go core.
* Sign PSBTs, Ethereum RLP transactions, and Bitcoin messages; fetch addresses
and the master fingerprint.
* USB transport on Android plus iOS BLE support for the BitBox02 Nova.
* Add the auto release-PR and auto-tag/GitHub-release CI workflows.
22 changes: 21 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
TODO: Add your license here.
MIT License

Copyright (c) 2026 DFX AG

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
79 changes: 70 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,76 @@
# bitbox_flutter

A flutter plugin for bitbox
A Flutter plugin for the [BitBox02](https://bitbox.swiss) hardware wallet. It connects over USB on Linux and Android and over BLE on iOS, then exposes signing and address operations for Bitcoin, Litecoin, Ethereum, and ERC20 tokens. The plugin keeps its native bridges thin and pushes the hardware-wallet protocol into a shared Go core (gomobile-exported) so the same firmware logic backs every platform.

## Getting Started
## Supported devices and chains

This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
| | Supported |
|---|---|
| Devices | BitBox02 |
| Transports | USB (Linux, Android), BLE (iOS) |
| Chains | Bitcoin (BTC), Litecoin (LTC), Ethereum (ETH), ERC20 tokens |

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Operations exposed through the plugin include device discovery, pairing and channel-hash verification, master-fingerprint and capability queries (`supportsETH`, `supportsERC20`, `supportsLTC`), BTC xpub export and PSBT/message signing, and ETH address export plus transaction (legacy and EIP-1559), message, and EIP-712 typed-message signing. The full Dart surface is declared on `BitboxUsbPlatform` in `lib/usb/bitbox_usb_platform_interface.dart`.

## Usage

```dart
import 'package:bitbox_flutter/bitbox_flutter.dart';
```

`bitbox_manager.dart` and the `BitboxDevice` model are the public entry points. For tests, import the standalone simulator instead — see [Testing](#testing).

## Architecture

The plugin is layered so that protocol logic lives in one place and the per-platform code stays minimal:

```
Dart (lib/)
bitbox_manager.dart, lib/usb/* — public API, method-channel client
|
v MethodChannel
Native bridge (thin)
android/ Kotlin — MethodCallRegistry + operations/*Operation.kt
ios/ Swift — BitboxFlutterPlugin, MethodCallRegistry, Bluetooth
|
v gomobile FFI
Go core (go/api)
api.go, bitcoin.go, ethereum.go, bitbox_device.go
every gomobile-exported entry point starts with `defer recoverPanic("<name>")`
```

- **Dart (`lib/`)** declares the abstract `BitboxUsbPlatform`, implements it over a `MethodChannel` in `lib/usb/bitbox_usb_method_channel.dart`, and wraps it for consumers in `bitbox_manager.dart`.
- **Native bridges** are deliberately thin. On Android each method maps to a `*Operation.kt` registered in `MethodCallRegistry.kt`; on iOS the equivalent dispatch lives in `MethodCallRegistry.swift`, with `Bluetooth.swift` handling the BLE transport. Bridges marshal arguments and delegate to the Go core rather than implementing wallet logic themselves.
- **Go core (`go/api`)** is gomobile-exported and built on `github.com/BitBoxSwiss/bitbox02-api-go`. Every exported function (for example `GetDevice`, `GetChannelHash`, `InitDevice`, `SupportsETH`) begins with `defer recoverPanic("<name>")` (see `go/api/safety.go`) so a Go-side panic returns a zero value instead of crossing the gomobile boundary into the host engine. A separate `go/u2fhid` package holds the USB/BLE framing layer.

## Rebuilding the gomobile bindings

The native binding artifacts are vendored and checked in; rebuild them only when the gomobile-exported Go surface changes.

- **Android** — run `run_build_tool_android.sh <output>` to produce `android/libs/api.aar` via `gomobile bind -target=android`.
- **iOS** — regenerate the `Api.xcframework` through the equivalent gomobile iOS pipeline.
- **Dart FFI header** — `ffigen_config.yaml` drives `ffigen` to regenerate `lib/generated_bindings.g.dart` from `go/bitbox.h`.

The checked-in artifacts must match the Go module path declared in `go/go.mod` (`github.com/DFXswiss/bitbox_flutter`). Regenerate and commit the artifacts together with the Go change so the boundary stays consistent.

## Testing

The plugin ships a reusable, hardware-free Dart testkit. Consumer apps can drive their real `BitboxManager` flow against a simulator with no USB, BLE, native code, or firmware:

```dart
import 'package:bitbox_flutter/testing.dart';

final bitbox = installSimulatedBitboxPlatform(
channelHash: 'hash-shown-to-the-user',
);
```

The simulator swaps out `BitboxUsbPlatform.instance` and covers device discovery, pairing, capability checks, BTC/ETH signing, per-method delays and errors, and an assertable call log. See [TESTING.md](TESTING.md) for the full test layers (official simulator, U2FHID/BLE contract tests, native API fake, Flutter API fake) and the regressions they guard.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for the branching model (`develop` is the default branch), the local/CI PR gate, the steps to add a new platform method end to end, and the automated release flow. Source: [github.com/DFXswiss/bitbox_flutter](https://github.com/DFXswiss/bitbox_flutter).

## License

MIT — see [LICENSE](LICENSE).
16 changes: 0 additions & 16 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ android {

sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}

defaultConfig {
Expand All @@ -60,21 +59,6 @@ android {

dependencies {
implementation (name: 'api', ext: 'aar')

testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}

testOptions {
unitTests.all {
useJUnitPlatform()

testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cakewallet.bitbox_flutter.operations

import android.content.Context
import android.util.Log
import com.cakewallet.bitbox_flutter.BitboxManager
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
Expand All @@ -14,7 +15,8 @@ class CloseOperation(private val manager: BitboxManager) :
) {
try {
manager.close()
} catch (_: Exception) {
} catch (ex: Exception) {
Log.w("bitbox_flutter", "close failed, gracefully resetting: ${ex.message}")
manager.gracefullyReset()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cakewallet.bitbox_flutter.operations

import android.content.Context
import android.util.Log
import com.cakewallet.bitbox_flutter.BitBoxException
import com.cakewallet.bitbox_flutter.BitboxManager
import io.flutter.plugin.common.MethodCall
Expand All @@ -18,6 +19,7 @@ class ConnectBitBoxOperation(private val manager: BitboxManager) :
this.manager.connectBitBox(identifier!!)
result.success(true)
} catch (ex: BitBoxException) {
Log.w("bitbox_flutter", "connectBitBox failed (${ex.getErrorCode()}): ${ex.message}")
this.manager.gracefullyReset()
result.error(ex.getErrorCode(), ex.message, null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.cakewallet.bitbox_flutter.operations
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import api.Api
import com.cakewallet.bitbox_flutter.BitboxManager
import io.flutter.plugin.common.MethodCall
Expand Down Expand Up @@ -35,6 +36,7 @@ class InitBitBoxOperation(manager: BitboxManager) : UsbMethodCallOperation(manag
val success = try {
Api.initDevice()
} catch (e: Throwable) {
Log.w("bitbox_flutter", "initDevice failed: ${e.message}")
false
}
Handler(Looper.getMainLooper()).post { result.success(success) }
Expand Down

This file was deleted.

5 changes: 1 addition & 4 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down Expand Up @@ -31,9 +31,6 @@ target 'Runner' do
use_frameworks!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end

post_install do |installer|
Expand Down
Loading
Loading