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); + }); }