Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
cache: true
- run: flutter pub get
- run: dart format --set-exit-if-changed --output=none lib test example/lib example/test
- run: flutter analyze --no-fatal-infos
- run: flutter analyze --fatal-infos
- run: flutter test

go:
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 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.

## 0.0.1

* TODO: Describe initial release.
67 changes: 67 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Contributing to bitbox_flutter

A Flutter plugin for the BitBox02 hardware wallet (USB on Linux/Android, BLE on iOS). PRs welcome — this guide covers the branching model, the local PR gate, and the release flow so contributions land predictably.

## Branching model

- `develop` is the **default branch**. Open every feature, fix, and chore PR against `develop`.
- `main` is the release branch. The only thing that lands there is a `Release: develop -> main` PR, opened automatically by `.github/workflows/auto-release-pr.yaml` on each push to `develop`.
- Feature branches: `feat/<short-slug>`, `fix/<short-slug>`, `chore/<short-slug>`. Branch off the latest `develop`.

## PR gate

CI runs three jobs on every PR and every push to `develop` / `main` (`.github/workflows/pull-request.yaml`):

| Job | What it does |
|---|---|
| `Flutter analyze + test` | `dart format --set-exit-if-changed`, `flutter analyze --no-fatal-infos`, `flutter test` |
| `Go unit tests` | `go vet ./...`, `go test -race -timeout 60s ./...` against `go/api` and `go/u2fhid` |
| `Workflow YAML lint` | `yaml.safe_load` on every `.github/**/*.y*ml` |

Run the same gate locally — see [TESTING.md → Fast PR gate](TESTING.md#fast-pr-gate). Lint failures upstream are wasted CI minutes; catch them locally.

## Adding a new platform method

1. **Dart side**: declare the abstract method on `BitboxUsbPlatform` (`lib/usb/bitbox_usb_platform_interface.dart`) and implement it on `BitboxUsbMethodChannel` (`lib/usb/bitbox_usb_method_channel.dart`).
2. **Go side** (gomobile-exported): add the corresponding function in `go/api/*.go`, wrapped with `defer recoverPanic("<name>")` so a Go-side crash returns a zero value instead of taking the engine down.
3. **Native bridges**: wire the new method through `android/src/main/kotlin/.../MethodCallRegistry.kt` and the iOS handler in `ios/Classes/BitboxFlutterPlugin.swift`.
4. **Testkit**: implement the method on `SimulatedBitboxPlatform` in `lib/testing/bitbox_testkit.dart` so consumer apps can exercise it without hardware. Add a method-name constant to `SimulatedBitboxMethod`.
5. **Tests**:
- `test/bitbox_testkit_test.dart` — Dart-side flow against the simulator.
- `go/api/fake_bitbox_test.go` — Go-side wiring against the `fakeBitboxDevice` fake.
6. **Docs**: if behaviour is non-obvious, add a paragraph to TESTING.md so future contributors know what's covered.

## Release flow

Releases are fully automated. Once a contributor's PR is merged into `develop`:

1. `auto-release-pr.yaml` fires on the push to `develop` and opens (or refreshes) the `Release: develop -> main` PR.
2. A maintainer reviews the rollup and merges it. The merge commit on `main` triggers `auto-tag.yaml`.
3. `auto-tag.yaml` patch-bumps the latest `vX.Y.Z` tag, pushes the new tag, and creates a GitHub Release with auto-generated notes.

No manual tagging. If a hotfix needs a specific version (skip-patch, force-minor), open an issue first — the auto-tagger is intentionally not overridable to avoid drift.

## Commit messages

- Title in imperative present: `fix(ble): drop stale read buffer after timeout`.
- Reference issues by number when applicable: `Closes #123`.
- Body explains **why**, not what (the diff shows the what).
- No `Co-Authored-By` or tool-generated trailers in DFXswiss repos.

## Hardware-wallet specifics

- Touching `ios/Classes/Bluetooth.swift` requires extra care: U2FHID `readFrame()` has no SEQ validation, so the BLE bridge MUST not let duplicate notifications leak into the read stream. `go/api/ios_bluetooth_regression_test.go` locks in the 60s read timeout — keep it.
- Touching `go/api/api.go` exported (`//export`) functions: every entry point must start with `defer recoverPanic("<name>")`. The `TestExportedAPIsReturnZeroValuesWithoutDeviceInsteadOfCrashing` test exists to enforce that the gomobile boundary never panics into the host engine.
- New gomobile-exported types: regenerate the iOS xcframework and `android/libs/api.aar` via `run_build_tool_android.sh` and the equivalent iOS pipeline before release. The checked-in artefacts must match the Go module path declared in `go/go.mod`.

## Code style

- **Dart**: `dart format` is enforced. No `print` in production code (`lib/`) — use a passed-in logger or remove. `example/lib/` may use `print` for demonstration.
- **Go**: gofmt-clean, `go vet` clean. Errors propagate; the gomobile boundary in `go/api/` is the only place that swallows errors (returning zero values) so panics don't cross into the host engine.
- **Swift / Kotlin**: follow each platform's standard formatter. Native bridges should be thin — push logic into the Go layer wherever possible.

## Security

- Never log seed phrases, private keys, or unredacted device serials.
- Don't add network calls — this plugin talks to local hardware only.
- For sensitive issues, mail security@dfx.swiss rather than filing a public issue.
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ these, so it is cheaper to catch them locally first:
```sh
# Flutter
dart format --set-exit-if-changed --output=none lib test example/lib example/test
flutter analyze --no-fatal-infos
flutter analyze --fatal-infos
flutter test

# Go (from go/)
Expand Down
Binary file modified android/libs/api-sources.jar
Binary file not shown.
Binary file modified android/libs/api.aar
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.cakewallet.bitbox_flutter.operations

import android.content.Context
import android.os.Handler
import android.os.Looper
import api.Api
import com.cakewallet.bitbox_flutter.BitboxManager
import io.flutter.plugin.common.MethodCall
Expand All @@ -12,7 +14,30 @@ class InitBitBoxOperation(manager: BitboxManager) : UsbMethodCallOperation(manag
methodCall: MethodCall,
result: MethodChannel.Result
) {
Api.initDevice()
result.success(true)
// Api.initDevice() drives the Noise XX handshake and then blocks until the
// user confirms the pairing on the device. The channel hash (pairing code) is
// already set on the shared device object before that blocking wait, so the
// host can display it immediately.
//
// The "bitbox_usb" MethodChannel runs on a serial background task queue. If we
// called Api.initDevice() directly here, it would occupy that single queue for
// the whole blocking wait, so the concurrent getChannelHash poll could not run
// until init returned — i.e. only AFTER the device confirmation. That made the
// pairing code appear in the app only after the user confirmed on the BitBox,
// instead of simultaneously (Android-only; iOS dispatches init to a background
// queue and is unaffected).
//
// Running the blocking call on its own thread frees the serial queue, so
// getChannelHash is serviced while init is in flight and the code shows on app
// and device at the same time. All other device operations stay on the serial
// queue, preserving the single-cipher nonce ordering.
Thread {
val success = try {
Api.initDevice()
} catch (e: Throwable) {
false
}
Handler(Looper.getMainLooper()).post { result.success(success) }
}.apply { name = "bitbox-init" }.start()
}
}
4 changes: 4 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Demo app uses print() for runtime console feedback during the BitBox flow
// walkthrough. The plugin itself (lib/) must stay print-free.
// ignore_for_file: avoid_print

import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
Expand Down
10 changes: 5 additions & 5 deletions ios/Api.xcframework/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,32 @@
<key>BinaryPath</key>
<string>Api.framework/Api</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>Api.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>Api.framework/Api</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>Api.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
Expand Down
Binary file modified ios/Api.xcframework/ios-arm64/Api.framework/Api
Binary file not shown.
2 changes: 1 addition & 1 deletion ios/Api.xcframework/ios-arm64/Api.framework/Headers/Api.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

// Objective-C API for talking to the following Go packages
//
// github.com/konstantinullrich/bitbox_flutter/api
// github.com/DFXswiss/bitbox_flutter/api
//
// File is generated by gomobile bind. Do not edit.
#ifndef __Api_FRAMEWORK_H__
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Objective-C API for talking to github.com/konstantinullrich/bitbox_flutter/api Go package.
// gobind -lang=objc github.com/konstantinullrich/bitbox_flutter/api
// Objective-C API for talking to github.com/DFXswiss/bitbox_flutter/api Go package.
// gobind -lang=objc github.com/DFXswiss/bitbox_flutter/api
//
// File is generated by gobind. Do not edit.

Expand Down
4 changes: 2 additions & 2 deletions ios/Api.xcframework/ios-arm64/Api.framework/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<key>MinimumOSVersion</key>
<string>100.0</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1778239382</string>
<string>0.0.1779133764</string>
<key>CFBundleVersion</key>
<string>0.0.1778239382</string>
<string>0.0.1779133764</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
</dict>
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

// Objective-C API for talking to the following Go packages
//
// github.com/konstantinullrich/bitbox_flutter/api
// github.com/DFXswiss/bitbox_flutter/api
//
// File is generated by gomobile bind. Do not edit.
#ifndef __Api_FRAMEWORK_H__
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Objective-C API for talking to github.com/konstantinullrich/bitbox_flutter/api Go package.
// gobind -lang=objc github.com/konstantinullrich/bitbox_flutter/api
// Objective-C API for talking to github.com/DFXswiss/bitbox_flutter/api Go package.
// gobind -lang=objc github.com/DFXswiss/bitbox_flutter/api
//
// File is generated by gobind. Do not edit.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<key>MinimumOSVersion</key>
<string>100.0</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1778239382</string>
<string>0.0.1779133765</string>
<key>CFBundleVersion</key>
<string>0.0.1778239382</string>
<string>0.0.1779133765</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
</dict>
Expand Down
127 changes: 38 additions & 89 deletions lib/testing/bitbox_testkit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,66 +180,6 @@ class SimulatedBitboxPlatform extends BitboxUsbPlatform {
bool _isOpen = false;
bool _channelHashVerified;

static SimulatedBitboxPlatform install({
List<BitboxDevice>? devices,
Duration defaultDelay = Duration.zero,
bool requireOpen = true,
bool channelHashVerified = false,
bool startScanResult = true,
bool permissionResult = true,
bool openResult = true,
bool initResult = true,
bool channelHashVerifyResult = true,
bool supportsETHResult = true,
bool supportsERC20Result = true,
bool supportsLTCResult = true,
String? channelHash,
Uint8List? masterFingerprint,
String? btcXPub,
String? btcPsbt,
Uint8List? btcMessageSignature,
String? ethAddress,
Uint8List? ethTransactionSignature,
Uint8List? ethEip1559Signature,
Uint8List? ethRlpSignature,
Uint8List? ethMessageSignature,
Uint8List? ethTypedMessageSignature,
Map<String, Duration>? delays,
Map<String, Object>? errors,
Map<String, SimulatedBitboxBehavior>? behaviors,
}) {
final platform = SimulatedBitboxPlatform(
devices: devices,
defaultDelay: defaultDelay,
requireOpen: requireOpen,
channelHashVerified: channelHashVerified,
startScanResult: startScanResult,
permissionResult: permissionResult,
openResult: openResult,
initResult: initResult,
channelHashVerifyResult: channelHashVerifyResult,
supportsETHResult: supportsETHResult,
supportsERC20Result: supportsERC20Result,
supportsLTCResult: supportsLTCResult,
channelHash: channelHash,
masterFingerprint: masterFingerprint,
btcXPub: btcXPub,
btcPsbt: btcPsbt,
btcMessageSignature: btcMessageSignature,
ethAddress: ethAddress,
ethTransactionSignature: ethTransactionSignature,
ethEip1559Signature: ethEip1559Signature,
ethRlpSignature: ethRlpSignature,
ethMessageSignature: ethMessageSignature,
ethTypedMessageSignature: ethTypedMessageSignature,
delays: delays,
errors: errors,
behaviors: behaviors,
);
BitboxUsbPlatform.instance = platform;
return platform;
}

bool get isOpen => _isOpen;

bool get channelHashVerified => _channelHashVerified;
Expand Down Expand Up @@ -566,6 +506,12 @@ class SimulatedBitboxPlatform extends BitboxUsbPlatform {
static Uint8List _copy(Uint8List bytes) => Uint8List.fromList(bytes);
}

/// Constructs a [SimulatedBitboxPlatform] with the given overrides and
/// installs it as `BitboxUsbPlatform.instance`. Returns the platform so
/// tests can drive `setDelay` / `throwOn` / `when` and inspect `calls`.
///
/// Save and restore `BitboxUsbPlatform.instance` in `setUp` / `tearDown`
/// when a suite needs isolation between tests.
SimulatedBitboxPlatform installSimulatedBitboxPlatform({
List<BitboxDevice>? devices,
Duration defaultDelay = Duration.zero,
Expand Down Expand Up @@ -593,32 +539,35 @@ SimulatedBitboxPlatform installSimulatedBitboxPlatform({
Map<String, Duration>? delays,
Map<String, Object>? errors,
Map<String, SimulatedBitboxBehavior>? behaviors,
}) =>
SimulatedBitboxPlatform.install(
devices: devices,
defaultDelay: defaultDelay,
requireOpen: requireOpen,
channelHashVerified: channelHashVerified,
startScanResult: startScanResult,
permissionResult: permissionResult,
openResult: openResult,
initResult: initResult,
channelHashVerifyResult: channelHashVerifyResult,
supportsETHResult: supportsETHResult,
supportsERC20Result: supportsERC20Result,
supportsLTCResult: supportsLTCResult,
channelHash: channelHash,
masterFingerprint: masterFingerprint,
btcXPub: btcXPub,
btcPsbt: btcPsbt,
btcMessageSignature: btcMessageSignature,
ethAddress: ethAddress,
ethTransactionSignature: ethTransactionSignature,
ethEip1559Signature: ethEip1559Signature,
ethRlpSignature: ethRlpSignature,
ethMessageSignature: ethMessageSignature,
ethTypedMessageSignature: ethTypedMessageSignature,
delays: delays,
errors: errors,
behaviors: behaviors,
);
}) {
final platform = SimulatedBitboxPlatform(
devices: devices,
defaultDelay: defaultDelay,
requireOpen: requireOpen,
channelHashVerified: channelHashVerified,
startScanResult: startScanResult,
permissionResult: permissionResult,
openResult: openResult,
initResult: initResult,
channelHashVerifyResult: channelHashVerifyResult,
supportsETHResult: supportsETHResult,
supportsERC20Result: supportsERC20Result,
supportsLTCResult: supportsLTCResult,
channelHash: channelHash,
masterFingerprint: masterFingerprint,
btcXPub: btcXPub,
btcPsbt: btcPsbt,
btcMessageSignature: btcMessageSignature,
ethAddress: ethAddress,
ethTransactionSignature: ethTransactionSignature,
ethEip1559Signature: ethEip1559Signature,
ethRlpSignature: ethRlpSignature,
ethMessageSignature: ethMessageSignature,
ethTypedMessageSignature: ethTypedMessageSignature,
delays: delays,
errors: errors,
behaviors: behaviors,
);
BitboxUsbPlatform.instance = platform;
return platform;
}
Loading
Loading