diff --git a/CHANGELOG.md b/CHANGELOG.md
index c570f90..c3024b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+## 0.0.10
+
+* Expose `BitboxManager.getDeviceStatus()`, returning the SDK's cached firmware
+ status string (`uninitialized` / `seeded` / `initialized`). It reads the locally
+ cached status without a device round-trip, so the host app can tell an unseeded
+ device (no wallet set up yet) apart from a transient empty address read after
+ pairing instead of failing both the same way.
+
+## 0.0.9
+
+* Android: force 16 KB ELF page alignment on the gomobile-built native libs
+ (`-extldflags=-Wl,-z,max-page-size=16384`). `libgojni.so` was the only 4 KB-aligned
+ library in the bundle; Android 15+ devices using 16 KB memory pages (and Google
+ Play) require ≥ 16 KB alignment.
+
## 0.0.8
* Android: run the blocking `initBitBox` (Noise pairing handshake) off the serial
diff --git a/android/libs/api-sources.jar b/android/libs/api-sources.jar
index 9068770..64cfb40 100644
Binary files a/android/libs/api-sources.jar and b/android/libs/api-sources.jar differ
diff --git a/android/libs/api.aar b/android/libs/api.aar
index df1441b..ebc6578 100644
Binary files a/android/libs/api.aar and b/android/libs/api.aar differ
diff --git a/android/src/main/kotlin/com/cakewallet/bitbox_flutter/BitboxFlutterPlugin.kt b/android/src/main/kotlin/com/cakewallet/bitbox_flutter/BitboxFlutterPlugin.kt
index 122c372..dadd984 100644
--- a/android/src/main/kotlin/com/cakewallet/bitbox_flutter/BitboxFlutterPlugin.kt
+++ b/android/src/main/kotlin/com/cakewallet/bitbox_flutter/BitboxFlutterPlugin.kt
@@ -13,6 +13,7 @@ import com.cakewallet.bitbox_flutter.operations.ETHSignMessageOperation
import com.cakewallet.bitbox_flutter.operations.ETHSignTransactionOperation
import com.cakewallet.bitbox_flutter.operations.ETHSignTypedMessageOperation
import com.cakewallet.bitbox_flutter.operations.GetChannelHashOperation
+import com.cakewallet.bitbox_flutter.operations.GetDeviceStatusOperation
import com.cakewallet.bitbox_flutter.operations.GetDevicesOperation
import com.cakewallet.bitbox_flutter.operations.ETHGetAddressOperation
import com.cakewallet.bitbox_flutter.operations.ETHSignRLPTransactionOperation
@@ -53,6 +54,7 @@ class BitboxFlutterPlugin : FlutterPlugin, MethodCallHandler {
registry.registerMethodCall("initBitBox", InitBitBoxOperation(bitboxManager))
registry.registerMethodCall("getChannelHash", GetChannelHashOperation(bitboxManager))
registry.registerMethodCall("channelHashVerify", ChannelHashVerifyOperation(bitboxManager))
+ registry.registerMethodCall("getDeviceStatus", GetDeviceStatusOperation(bitboxManager))
registry.registerMethodCall("getMasterFingerprint", GetMasterFingerprintOperation(bitboxManager))
registry.registerMethodCall("supportsETH", SupportsETHOperation(bitboxManager))
registry.registerMethodCall("supportsERC20", SupportsERC20Operation(bitboxManager))
diff --git a/android/src/main/kotlin/com/cakewallet/bitbox_flutter/operations/GetDeviceStatusOperation.kt b/android/src/main/kotlin/com/cakewallet/bitbox_flutter/operations/GetDeviceStatusOperation.kt
new file mode 100644
index 0000000..99b2d2e
--- /dev/null
+++ b/android/src/main/kotlin/com/cakewallet/bitbox_flutter/operations/GetDeviceStatusOperation.kt
@@ -0,0 +1,20 @@
+package com.cakewallet.bitbox_flutter.operations
+
+import android.content.Context
+import api.Api
+import com.cakewallet.bitbox_flutter.BitboxManager
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+
+class GetDeviceStatusOperation(manager: BitboxManager) : UsbMethodCallOperation(manager.usbManager) {
+ override fun onMethodCall(
+ context: Context,
+ methodCall: MethodCall,
+ result: MethodChannel.Result
+ ) {
+ // Api.deviceStatus() reads the SDK's cached firmware status — no device
+ // round-trip — so it is safe to run on the serial queue like getChannelHash.
+ val status = Api.deviceStatus()
+ result.success(status)
+ }
+}
diff --git a/go/api/api.go b/go/api/api.go
index ce07e66..e9aeb5b 100644
--- a/go/api/api.go
+++ b/go/api/api.go
@@ -189,6 +189,23 @@ func InitDevice() (success bool) {
return true
}
+// DeviceStatus returns the firmware status of the paired device as a string
+// (e.g. "uninitialized", "seeded", "initialized"). It reads the cached status
+// the SDK maintains, so it does not perform a device round-trip and cannot
+// block. An empty string is returned when there is no device. Callers use this
+// after pairing to tell an unseeded device (no wallet set up yet) apart from a
+// transient empty address read.
+//
+//export DeviceStatus
+func DeviceStatus() (status string) {
+ defer recoverPanic("DeviceStatus")
+
+ if bitbox == nil {
+ return ""
+ }
+ return string(bitbox.Status())
+}
+
//export SupportsETH
func SupportsETH(chainId int) (supported bool) {
defer recoverPanic("SupportsETH")
diff --git a/go/api/bitbox_device.go b/go/api/bitbox_device.go
index 63e83ee..44ad11a 100644
--- a/go/api/bitbox_device.go
+++ b/go/api/bitbox_device.go
@@ -12,6 +12,7 @@ type bitboxDevice interface {
Init() error
ChannelHash() (string, bool)
ChannelHashVerify(ok bool)
+ Status() firmware.Status
DeviceInfo() (*firmware.DeviceInfo, error)
RootFingerprint() ([]byte, error)
SupportsETH(chainID uint64) bool
diff --git a/go/api/fake_bitbox_test.go b/go/api/fake_bitbox_test.go
index 3a24f47..0c1fe84 100644
--- a/go/api/fake_bitbox_test.go
+++ b/go/api/fake_bitbox_test.go
@@ -21,6 +21,8 @@ type fakeBitboxDevice struct {
channelHashOk bool
channelHashVerified *bool
+ status firmware.Status
+
deviceInfo *firmware.DeviceInfo
deviceInfoErr error
rootFingerprint []byte
@@ -67,6 +69,11 @@ func (f *fakeBitboxDevice) ChannelHashVerify(ok bool) {
f.channelHashVerified = &ok
}
+func (f *fakeBitboxDevice) Status() firmware.Status {
+ f.calls = append(f.calls, "Status")
+ return f.status
+}
+
func (f *fakeBitboxDevice) DeviceInfo() (*firmware.DeviceInfo, error) {
f.calls = append(f.calls, "DeviceInfo")
return f.deviceInfo, f.deviceInfoErr
@@ -155,6 +162,7 @@ func TestFakeBitboxHarnessSimulatesPairingAndCapabilities(t *testing.T) {
channelHash: "PAIR-CODE",
channelHashOk: true,
channelHashVerified: &verified,
+ status: firmware.StatusInitialized,
deviceInfo: &firmware.DeviceInfo{Name: "Simulated BitBox"},
rootFingerprint: []byte{0x01, 0x02, 0x03, 0x04},
supportsETH: true,
@@ -180,6 +188,9 @@ func TestFakeBitboxHarnessSimulatesPairingAndCapabilities(t *testing.T) {
if got := DeviceInfo().Name; got != "Simulated BitBox" {
t.Fatalf("expected simulated device info, got %q", got)
}
+ if got := DeviceStatus(); got != string(firmware.StatusInitialized) {
+ t.Fatalf("expected simulated device status, got %q", got)
+ }
if got := GetMasterFingerprint(); !reflect.DeepEqual(got, []byte{0x01, 0x02, 0x03, 0x04}) {
t.Fatalf("expected simulated root fingerprint, got %x", got)
}
@@ -297,6 +308,9 @@ func TestExportedAPIsReturnZeroValuesWithoutDeviceInsteadOfCrashing(t *testing.T
if got := DeviceInfo(); got.Name != "" {
t.Fatalf("expected zero device info without device, got %+v", got)
}
+ if got := DeviceStatus(); got != "" {
+ t.Fatalf("expected empty device status without device, got %q", got)
+ }
if got := ETHGetAddress(1, keypath, int(messages.ETHPubRequest_ADDRESS), false, nil); got != "" {
t.Fatalf("expected empty ETH address without device, got %q", got)
}
diff --git a/ios/Api.xcframework/Info.plist b/ios/Api.xcframework/Info.plist
index 9d3fc23..1a9d8ff 100644
--- a/ios/Api.xcframework/Info.plist
+++ b/ios/Api.xcframework/Info.plist
@@ -8,32 +8,32 @@
BinaryPath
Api.framework/Api
LibraryIdentifier
- ios-arm64
+ ios-arm64_x86_64-simulator
LibraryPath
Api.framework
SupportedArchitectures
arm64
+ x86_64
SupportedPlatform
ios
+ SupportedPlatformVariant
+ simulator
BinaryPath
Api.framework/Api
LibraryIdentifier
- ios-arm64_x86_64-simulator
+ ios-arm64
LibraryPath
Api.framework
SupportedArchitectures
arm64
- x86_64
SupportedPlatform
ios
- SupportedPlatformVariant
- simulator
CFBundlePackageType
diff --git a/ios/Api.xcframework/ios-arm64/Api.framework/Api b/ios/Api.xcframework/ios-arm64/Api.framework/Api
index d6db685..4b4f4d0 100644
Binary files a/ios/Api.xcframework/ios-arm64/Api.framework/Api and b/ios/Api.xcframework/ios-arm64/Api.framework/Api differ
diff --git a/ios/Api.xcframework/ios-arm64/Api.framework/Headers/Api.objc.h b/ios/Api.xcframework/ios-arm64/Api.framework/Headers/Api.objc.h
index d090f8b..093e3aa 100644
--- a/ios/Api.xcframework/ios-arm64/Api.framework/Headers/Api.objc.h
+++ b/ios/Api.xcframework/ios-arm64/Api.framework/Headers/Api.objc.h
@@ -45,6 +45,16 @@ FOUNDATION_EXPORT void ApiChannelHashVerify(BOOL ok);
// skipped function DeviceInfo with unsupported parameter or return types
+/**
+ * DeviceStatus returns the firmware status of the paired device as a string
+(e.g. "uninitialized", "seeded", "initialized"). It reads the cached status
+the SDK maintains, so it does not perform a device round-trip and cannot
+block. An empty string is returned when there is no device. Callers use this
+after pairing to tell an unseeded device (no wallet set up yet) apart from a
+transient empty address read.
+ */
+FOUNDATION_EXPORT NSString* _Nonnull ApiDeviceStatus(void);
+
FOUNDATION_EXPORT NSString* _Nonnull ApiETHGetAddress(long chainId, NSString* _Nullable keypath, long outputType, BOOL display, NSData* _Nullable contractAddress);
FOUNDATION_EXPORT NSData* _Nullable ApiETHSignEIP1559(long chainId, NSString* _Nullable keypath, long nonce, NSString* _Nullable maxPriorityFeePerGas, NSString* _Nullable maxFeePerGas, long gasLimit, NSData* _Nullable recipient, NSString* _Nullable value, NSData* _Nullable data, long recipientAddressCase);
diff --git a/ios/Api.xcframework/ios-arm64/Api.framework/Info.plist b/ios/Api.xcframework/ios-arm64/Api.framework/Info.plist
index 83959d6..08aa4e4 100644
--- a/ios/Api.xcframework/ios-arm64/Api.framework/Info.plist
+++ b/ios/Api.xcframework/ios-arm64/Api.framework/Info.plist
@@ -9,9 +9,9 @@
MinimumOSVersion
100.0
CFBundleShortVersionString
- 0.0.1779133764
+ 0.0.1781025390
CFBundleVersion
- 0.0.1779133764
+ 0.0.1781025390
CFBundlePackageType
FMWK
diff --git a/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Api b/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Api
index 0914e5a..04dddb2 100644
Binary files a/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Api and b/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Api differ
diff --git a/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Headers/Api.objc.h b/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Headers/Api.objc.h
index d090f8b..093e3aa 100644
--- a/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Headers/Api.objc.h
+++ b/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Headers/Api.objc.h
@@ -45,6 +45,16 @@ FOUNDATION_EXPORT void ApiChannelHashVerify(BOOL ok);
// skipped function DeviceInfo with unsupported parameter or return types
+/**
+ * DeviceStatus returns the firmware status of the paired device as a string
+(e.g. "uninitialized", "seeded", "initialized"). It reads the cached status
+the SDK maintains, so it does not perform a device round-trip and cannot
+block. An empty string is returned when there is no device. Callers use this
+after pairing to tell an unseeded device (no wallet set up yet) apart from a
+transient empty address read.
+ */
+FOUNDATION_EXPORT NSString* _Nonnull ApiDeviceStatus(void);
+
FOUNDATION_EXPORT NSString* _Nonnull ApiETHGetAddress(long chainId, NSString* _Nullable keypath, long outputType, BOOL display, NSData* _Nullable contractAddress);
FOUNDATION_EXPORT NSData* _Nullable ApiETHSignEIP1559(long chainId, NSString* _Nullable keypath, long nonce, NSString* _Nullable maxPriorityFeePerGas, NSString* _Nullable maxFeePerGas, long gasLimit, NSData* _Nullable recipient, NSString* _Nullable value, NSData* _Nullable data, long recipientAddressCase);
diff --git a/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Info.plist b/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Info.plist
index fd4fa65..08aa4e4 100644
--- a/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Info.plist
+++ b/ios/Api.xcframework/ios-arm64_x86_64-simulator/Api.framework/Info.plist
@@ -9,9 +9,9 @@
MinimumOSVersion
100.0
CFBundleShortVersionString
- 0.0.1779133765
+ 0.0.1781025390
CFBundleVersion
- 0.0.1779133765
+ 0.0.1781025390
CFBundlePackageType
FMWK
diff --git a/ios/Classes/BitboxFlutterPlugin.swift b/ios/Classes/BitboxFlutterPlugin.swift
index 432dae3..2234c3c 100644
--- a/ios/Classes/BitboxFlutterPlugin.swift
+++ b/ios/Classes/BitboxFlutterPlugin.swift
@@ -32,6 +32,8 @@ public class BitboxFlutterPlugin: NSObject, FlutterPlugin {
getChannelHash(result: result)
case "channelHashVerify":
channelHashVerify(result: result)
+ case "getDeviceStatus":
+ getDeviceStatus(result: result)
case "supportsETH":
supportsETH(call: call, result: result)
case "supportsERC20":
@@ -188,6 +190,12 @@ public class BitboxFlutterPlugin: NSObject, FlutterPlugin {
}
}
+ private func getDeviceStatus(result: @escaping FlutterResult) {
+ // ApiDeviceStatus() reads the SDK's cached firmware status — no device
+ // round-trip — so it returns immediately without a background dispatch.
+ result(ApiDeviceStatus())
+ }
+
// MARK: - Feature Support
private func supportsETH(call: FlutterMethodCall, result: @escaping FlutterResult) {
diff --git a/lib/bitbox_manager.dart b/lib/bitbox_manager.dart
index a213ba1..abdff55 100644
--- a/lib/bitbox_manager.dart
+++ b/lib/bitbox_manager.dart
@@ -36,6 +36,9 @@ class BitboxManager {
Future channelHashVerify() =>
BitboxUsbPlatform.instance.channelHashVerify();
+ Future getDeviceStatus() =>
+ BitboxUsbPlatform.instance.getDeviceStatus();
+
Future supportsLTC() => BitboxUsbPlatform.instance.supportsLTC();
Future supportsETH(int chainId) =>
diff --git a/lib/testing/bitbox_testkit.dart b/lib/testing/bitbox_testkit.dart
index 9846b16..20b2824 100644
--- a/lib/testing/bitbox_testkit.dart
+++ b/lib/testing/bitbox_testkit.dart
@@ -17,6 +17,7 @@ final class SimulatedBitboxMethod {
static const getMasterFingerprint = 'getMasterFingerprint';
static const getChannelHash = 'getChannelHash';
static const channelHashVerify = 'channelHashVerify';
+ static const getDeviceStatus = 'getDeviceStatus';
static const supportsETH = 'supportsETH';
static const supportsERC20 = 'supportsERC20';
static const supportsLTC = 'supportsLTC';
@@ -66,6 +67,7 @@ class SimulatedBitboxPlatform extends BitboxUsbPlatform {
this.openResult = true,
this.initResult = true,
this.channelHashVerifyResult = true,
+ this.deviceStatus = 'initialized',
this.supportsETHResult = true,
this.supportsERC20Result = true,
this.supportsLTCResult = true,
@@ -156,6 +158,7 @@ class SimulatedBitboxPlatform extends BitboxUsbPlatform {
final bool openResult;
final bool initResult;
final bool channelHashVerifyResult;
+ final String deviceStatus;
final bool supportsETHResult;
final bool supportsERC20Result;
final bool supportsLTCResult;
@@ -276,6 +279,13 @@ class SimulatedBitboxPlatform extends BitboxUsbPlatform {
return result;
}
+ @override
+ Future getDeviceStatus() => _run(
+ SimulatedBitboxMethod.getDeviceStatus,
+ const {},
+ deviceStatus,
+ );
+
@override
Future supportsETH(int chainId) => _run(
SimulatedBitboxMethod.supportsETH,
@@ -522,6 +532,7 @@ SimulatedBitboxPlatform installSimulatedBitboxPlatform({
bool openResult = true,
bool initResult = true,
bool channelHashVerifyResult = true,
+ String deviceStatus = 'initialized',
bool supportsETHResult = true,
bool supportsERC20Result = true,
bool supportsLTCResult = true,
@@ -550,6 +561,7 @@ SimulatedBitboxPlatform installSimulatedBitboxPlatform({
openResult: openResult,
initResult: initResult,
channelHashVerifyResult: channelHashVerifyResult,
+ deviceStatus: deviceStatus,
supportsETHResult: supportsETHResult,
supportsERC20Result: supportsERC20Result,
supportsLTCResult: supportsLTCResult,
diff --git a/lib/usb/bitbox_usb_method_channel.dart b/lib/usb/bitbox_usb_method_channel.dart
index bb21d1f..dd5a8a9 100644
--- a/lib/usb/bitbox_usb_method_channel.dart
+++ b/lib/usb/bitbox_usb_method_channel.dart
@@ -75,6 +75,13 @@ class MethodChannelBitboxUsb extends BitboxUsbPlatform {
return result ?? '';
}
+ @override
+ Future getDeviceStatus() async {
+ final result = await methodChannel.invokeMethod('getDeviceStatus');
+
+ return result ?? '';
+ }
+
@override
Future getMasterFingerprint() async {
final result =
diff --git a/lib/usb/bitbox_usb_platform_interface.dart b/lib/usb/bitbox_usb_platform_interface.dart
index 10f0321..80f7bf9 100644
--- a/lib/usb/bitbox_usb_platform_interface.dart
+++ b/lib/usb/bitbox_usb_platform_interface.dart
@@ -43,6 +43,8 @@ abstract class BitboxUsbPlatform extends PlatformInterface {
Future channelHashVerify();
+ Future getDeviceStatus();
+
Future supportsETH(int chainId);
Future supportsERC20(String contractAddress);
diff --git a/test/bitbox_testkit_test.dart b/test/bitbox_testkit_test.dart
index 7088823..7f6c47e 100644
--- a/test/bitbox_testkit_test.dart
+++ b/test/bitbox_testkit_test.dart
@@ -227,4 +227,23 @@ void main() {
throwsA(isA()),
);
});
+
+ test('reports the simulated device status', () async {
+ installSimulatedBitboxPlatform();
+ final manager = BitboxManager();
+ await manager.connect((await manager.devices).single);
+
+ expect(await manager.getDeviceStatus(), 'initialized');
+ });
+
+ test('reports an unseeded device status', () async {
+ final platform = installSimulatedBitboxPlatform(
+ deviceStatus: 'uninitialized',
+ );
+ final manager = BitboxManager();
+ await manager.connect((await manager.devices).single);
+
+ expect(await manager.getDeviceStatus(), 'uninitialized');
+ expect(platform.count(SimulatedBitboxMethod.getDeviceStatus), 1);
+ });
}