diff --git a/Cargo.lock b/Cargo.lock index a6a0b6f..7e4ea18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,7 +475,7 @@ dependencies = [ [[package]] name = "bitkitcore" -version = "0.1.60" +version = "0.1.61" dependencies = [ "android_logger", "async-trait", @@ -4688,9 +4688,9 @@ dependencies = [ [[package]] name = "trezor-connect-rs" -version = "0.2.8" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1d7bbb66e791e3bf063373b40c6a7911c4130f4fb14298ecb5bfae5040ed84" +checksum = "31fadba4f104a143d98ca38bf426086b7321b40abc3e83356fc600e0d77eeb8c" dependencies = [ "aes-gcm", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 936214a..3246573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitkitcore" -version = "0.1.60" +version = "0.1.61" edition = "2021" [lib] @@ -46,11 +46,11 @@ btleplug = "0.11" # Trezor connect library - non-iOS platforms get USB + Bluetooth [target.'cfg(not(target_os = "ios"))'.dependencies] -trezor-connect-rs = { version = "0.2.8", features = ["psbt"] } +trezor-connect-rs = { version = "0.3.0", features = ["psbt"] } # iOS: Bluetooth only (libusb has no iOS backend, so no USB support) [target.'cfg(target_os = "ios")'.dependencies] -trezor-connect-rs = { version = "0.2.8", default-features = false, features = ["bluetooth", "psbt"] } +trezor-connect-rs = { version = "0.3.0", default-features = false, features = ["bluetooth", "psbt"] } # JNI for Android (must match btleplug's jni version) [target.'cfg(target_os = "android")'.dependencies] diff --git a/Package.swift b/Package.swift index d4d94e2..3cd00af 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.1.60" -let checksum = "2491068759664686c279f33f7c9612c568b559cacb5174d20262f8b65fb731ec" +let tag = "v0.1.61" +let checksum = "7b0fc28e3a9faa031cf39c72e9bac231de64408d5bdd7403c8129ab9e42ed474" let url = "https://github.com/synonymdev/bitkit-core/releases/download/\(tag)/BitkitCore.xcframework.zip" let package = Package( diff --git a/bindings/android/gradle.properties b/bindings/android/gradle.properties index 1507630..3e7f13b 100644 --- a/bindings/android/gradle.properties +++ b/bindings/android/gradle.properties @@ -3,4 +3,4 @@ android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official group=com.synonym -version=0.1.60 +version=0.1.61 diff --git a/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so index 6eb7bd8..ccd8bf4 100755 Binary files a/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/arm64-v8a/libpubky_app_specs-fc4495b5409b2d39.so b/bindings/android/lib/src/main/jniLibs/arm64-v8a/libpubky_app_specs-fc4495b5409b2d39.so deleted file mode 100755 index a14e63e..0000000 Binary files a/bindings/android/lib/src/main/jniLibs/arm64-v8a/libpubky_app_specs-fc4495b5409b2d39.so and /dev/null differ diff --git a/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so index b956088..621377c 100755 Binary files a/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libpubky_app_specs-c99f298ca8acda0f.so b/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libpubky_app_specs-c99f298ca8acda0f.so deleted file mode 100755 index 034e56d..0000000 Binary files a/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libpubky_app_specs-c99f298ca8acda0f.so and /dev/null differ diff --git a/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so index 38723ce..b5d1b25 100755 Binary files a/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/x86/libpubky_app_specs-43984173235c5d3b.so b/bindings/android/lib/src/main/jniLibs/x86/libpubky_app_specs-43984173235c5d3b.so deleted file mode 100755 index 12fdb3c..0000000 Binary files a/bindings/android/lib/src/main/jniLibs/x86/libpubky_app_specs-43984173235c5d3b.so and /dev/null differ diff --git a/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so index 1c8056f..472c9b3 100755 Binary files a/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/x86_64/libpubky_app_specs-de79160b8ee95e02.so b/bindings/android/lib/src/main/jniLibs/x86_64/libpubky_app_specs-de79160b8ee95e02.so deleted file mode 100755 index 4b4cfad..0000000 Binary files a/bindings/android/lib/src/main/jniLibs/x86_64/libpubky_app_specs-de79160b8ee95e02.so and /dev/null differ diff --git a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt index 096a300..0d0df83 100644 --- a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt +++ b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt @@ -1920,7 +1920,7 @@ internal object IntegrityCheckingUniffiLib : Library { if (uniffi_bitkitcore_checksum_method_trezoruicallback_on_pin_request() != 50474.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (uniffi_bitkitcore_checksum_method_trezoruicallback_on_passphrase_request() != 63487.toShort()) { + if (uniffi_bitkitcore_checksum_method_trezoruicallback_on_passphrase_request() != 37914.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } } @@ -4413,12 +4413,6 @@ internal object uniffiCallbackInterfaceTrezorTransportCallback { * * The native layer (iOS/Android) should implement this to show PIN/passphrase * input UI when the device requests it during operations like signing. - * - * Methods return `String`: - * - Empty string (`""`) = cancel the request - * - Non-empty string = the user's input (PIN or passphrase) - * - * This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. */ public open class TrezorUiCallbackImpl: Disposable, TrezorUiCallback { @@ -4531,14 +4525,15 @@ public open class TrezorUiCallbackImpl: Disposable, TrezorUiCallback { /** * Called when the device requests a passphrase. * - * If `on_device` is true, the user should enter on the Trezor itself — - * return any non-empty string (e.g., "ok") to acknowledge. + * If `on_device` is true, the user should enter the passphrase on the + * Trezor itself — return `PassphraseResponse::Standard` (or + * `Hidden { value: "ok" }`) to acknowledge. * - * If `on_device` is false, show a passphrase input UI and return the value. - * Return empty string to cancel. + * If `on_device` is false, show a passphrase input UI and return the + * matching `PassphraseResponse` variant. */ - public override fun `onPassphraseRequest`(`onDevice`: kotlin.Boolean): kotlin.String { - return FfiConverterString.lift(callWithPointer { + public override fun `onPassphraseRequest`(`onDevice`: kotlin.Boolean): PassphraseResponse { + return FfiConverterTypePassphraseResponse.lift(callWithPointer { uniffiRustCall { uniffiRustCallStatus -> UniffiLib.uniffi_bitkitcore_fn_method_trezoruicallback_on_passphrase_request( it, @@ -4622,8 +4617,8 @@ internal object uniffiCallbackInterfaceTrezorUiCallback { FfiConverterBoolean.lift(`onDevice`), ) } - val writeReturn = { uniffiResultValue: kotlin.String -> - uniffiOutReturn.setValue(FfiConverterString.lower(uniffiResultValue)) + val writeReturn = { uniffiResultValue: PassphraseResponse -> + uniffiOutReturn.setValue(FfiConverterTypePassphraseResponse.lower(uniffiResultValue)) } uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) } @@ -9211,6 +9206,63 @@ public object FfiConverterTypeNetworkType: FfiConverterRustBuffer { +public object FfiConverterTypePassphraseResponse : FfiConverterRustBuffer{ + override fun read(buf: ByteBuffer): PassphraseResponse { + return when(buf.getInt()) { + 1 -> PassphraseResponse.Cancel + 2 -> PassphraseResponse.Standard + 3 -> PassphraseResponse.Hidden( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: PassphraseResponse): ULong = when(value) { + is PassphraseResponse.Cancel -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is PassphraseResponse.Standard -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is PassphraseResponse.Hidden -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`value`) + ) + } + } + + override fun write(value: PassphraseResponse, buf: ByteBuffer) { + when(value) { + is PassphraseResponse.Cancel -> { + buf.putInt(1) + Unit + } + is PassphraseResponse.Standard -> { + buf.putInt(2) + Unit + } + is PassphraseResponse.Hidden -> { + buf.putInt(3) + FfiConverterString.write(value.`value`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + + + + + public object FfiConverterTypePaymentState: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): PaymentState = try { PaymentState.entries[buf.getInt() - 1] @@ -9692,20 +9744,21 @@ public object FfiConverterTypeTrezorError : FfiConverterRustBuffer TrezorException.PinCancelled() 10 -> TrezorException.InvalidPin() 11 -> TrezorException.PassphraseRequired() - 12 -> TrezorException.UserCancelled() - 13 -> TrezorException.Timeout() - 14 -> TrezorException.InvalidPath( + 12 -> TrezorException.PassphraseCancelled() + 13 -> TrezorException.UserCancelled() + 14 -> TrezorException.Timeout() + 15 -> TrezorException.InvalidPath( FfiConverterString.read(buf), ) - 15 -> TrezorException.DeviceException( + 16 -> TrezorException.DeviceException( FfiConverterString.read(buf), ) - 16 -> TrezorException.NotInitialized() - 17 -> TrezorException.NotConnected() - 18 -> TrezorException.SessionException( + 17 -> TrezorException.NotInitialized() + 18 -> TrezorException.NotConnected() + 19 -> TrezorException.SessionException( FfiConverterString.read(buf), ) - 19 -> TrezorException.IoException( + 20 -> TrezorException.IoException( FfiConverterString.read(buf), ) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") @@ -9762,6 +9815,10 @@ public object FfiConverterTypeTrezorError : FfiConverterRustBuffer ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) is TrezorException.UserCancelled -> ( // Add the size for the Int that specifies the variant plus the size needed for all fields 4UL @@ -9851,39 +9908,43 @@ public object FfiConverterTypeTrezorError : FfiConverterRustBuffer { + is TrezorException.PassphraseCancelled -> { buf.putInt(12) Unit } - is TrezorException.Timeout -> { + is TrezorException.UserCancelled -> { buf.putInt(13) Unit } - is TrezorException.InvalidPath -> { + is TrezorException.Timeout -> { buf.putInt(14) + Unit + } + is TrezorException.InvalidPath -> { + buf.putInt(15) FfiConverterString.write(value.`errorDetails`, buf) Unit } is TrezorException.DeviceException -> { - buf.putInt(15) + buf.putInt(16) FfiConverterString.write(value.`errorDetails`, buf) Unit } is TrezorException.NotInitialized -> { - buf.putInt(16) + buf.putInt(17) Unit } is TrezorException.NotConnected -> { - buf.putInt(17) + buf.putInt(18) Unit } is TrezorException.SessionException -> { - buf.putInt(18) + buf.putInt(19) FfiConverterString.write(value.`errorDetails`, buf) Unit } is TrezorException.IoException -> { - buf.putInt(19) + buf.putInt(20) FfiConverterString.write(value.`errorDetails`, buf) Unit } diff --git a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt index 6c40c88..30e105c 100644 --- a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt +++ b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt @@ -248,12 +248,6 @@ public interface TrezorTransportCallback { * * The native layer (iOS/Android) should implement this to show PIN/passphrase * input UI when the device requests it during operations like signing. - * - * Methods return `String`: - * - Empty string (`""`) = cancel the request - * - Non-empty string = the user's input (PIN or passphrase) - * - * This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. */ public interface TrezorUiCallback { @@ -268,13 +262,14 @@ public interface TrezorUiCallback { /** * Called when the device requests a passphrase. * - * If `on_device` is true, the user should enter on the Trezor itself — - * return any non-empty string (e.g., "ok") to acknowledge. + * If `on_device` is true, the user should enter the passphrase on the + * Trezor itself — return `PassphraseResponse::Standard` (or + * `Hidden { value: "ok" }`) to acknowledge. * - * If `on_device` is false, show a passphrase input UI and return the value. - * Return empty string to cancel. + * If `on_device` is false, show a passphrase input UI and return the + * matching `PassphraseResponse` variant. */ - public fun `onPassphraseRequest`(`onDevice`: kotlin.Boolean): kotlin.String + public fun `onPassphraseRequest`(`onDevice`: kotlin.Boolean): PassphraseResponse public companion object } @@ -3380,6 +3375,38 @@ public enum class NetworkType { +@kotlinx.serialization.Serializable +public sealed class PassphraseResponse { + + /** + * User cancelled — aborts the pending operation. + */ + @kotlinx.serialization.Serializable + public data object Cancel : PassphraseResponse() + + + /** + * Standard wallet — no passphrase, equivalent to `Some("")` on the device. + */ + @kotlinx.serialization.Serializable + public data object Standard : PassphraseResponse() + + + /** + * Hidden wallet — derived from the supplied passphrase. + */@kotlinx.serialization.Serializable + public data class Hidden( + val `value`: kotlin.String, + ) : PassphraseResponse() { + } + +} + + + + + + @kotlinx.serialization.Serializable public enum class PaymentState { @@ -3738,6 +3765,15 @@ public sealed class TrezorException: kotlin.Exception() { get() = "" } + /** + * Passphrase entry cancelled + */ + public class PassphraseCancelled( + ) : TrezorException() { + override val message: String + get() = "" + } + /** * Action cancelled by user on device */ diff --git a/bindings/ios/BitkitCore.xcframework.zip b/bindings/ios/BitkitCore.xcframework.zip index 1fd45b7..f55ec00 100644 Binary files a/bindings/ios/BitkitCore.xcframework.zip and b/bindings/ios/BitkitCore.xcframework.zip differ diff --git a/bindings/ios/BitkitCore.xcframework/Info.plist b/bindings/ios/BitkitCore.xcframework/Info.plist index 478a88f..b7357e0 100644 --- a/bindings/ios/BitkitCore.xcframework/Info.plist +++ b/bindings/ios/BitkitCore.xcframework/Info.plist @@ -10,7 +10,7 @@ HeadersPath Headers LibraryIdentifier - ios-arm64 + ios-arm64-simulator LibraryPath libbitkitcore.a SupportedArchitectures @@ -19,6 +19,8 @@ SupportedPlatform ios + SupportedPlatformVariant + simulator BinaryPath @@ -26,7 +28,7 @@ HeadersPath Headers LibraryIdentifier - ios-arm64-simulator + ios-arm64 LibraryPath libbitkitcore.a SupportedArchitectures @@ -35,8 +37,6 @@ SupportedPlatform ios - SupportedPlatformVariant - simulator CFBundlePackageType diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a index 3bd1161..89dd9df 100644 Binary files a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a and b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a differ diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a b/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a index 105442e..9fa5db5 100644 Binary files a/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a and b/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a differ diff --git a/bindings/ios/bitkitcore.swift b/bindings/ios/bitkitcore.swift index d2c9d09..8ba9b3c 100644 --- a/bindings/ios/bitkitcore.swift +++ b/bindings/ios/bitkitcore.swift @@ -1312,12 +1312,6 @@ public func FfiConverterTypeTrezorTransportCallback_lower(_ value: TrezorTranspo * * The native layer (iOS/Android) should implement this to show PIN/passphrase * input UI when the device requests it during operations like signing. - * - * Methods return `String`: - * - Empty string (`""`) = cancel the request - * - Non-empty string = the user's input (PIN or passphrase) - * - * This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. */ public protocol TrezorUiCallback: AnyObject, Sendable { @@ -1332,13 +1326,14 @@ public protocol TrezorUiCallback: AnyObject, Sendable { /** * Called when the device requests a passphrase. * - * If `on_device` is true, the user should enter on the Trezor itself — - * return any non-empty string (e.g., "ok") to acknowledge. + * If `on_device` is true, the user should enter the passphrase on the + * Trezor itself — return `PassphraseResponse::Standard` (or + * `Hidden { value: "ok" }`) to acknowledge. * - * If `on_device` is false, show a passphrase input UI and return the value. - * Return empty string to cancel. + * If `on_device` is false, show a passphrase input UI and return the + * matching `PassphraseResponse` variant. */ - func onPassphraseRequest(onDevice: Bool) -> String + func onPassphraseRequest(onDevice: Bool) -> PassphraseResponse } /** @@ -1346,12 +1341,6 @@ public protocol TrezorUiCallback: AnyObject, Sendable { * * The native layer (iOS/Android) should implement this to show PIN/passphrase * input UI when the device requests it during operations like signing. - * - * Methods return `String`: - * - Empty string (`""`) = cancel the request - * - Non-empty string = the user's input (PIN or passphrase) - * - * This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. */ open class TrezorUiCallbackImpl: TrezorUiCallback, @unchecked Sendable { fileprivate let pointer: UnsafeMutableRawPointer! @@ -1421,14 +1410,15 @@ open func onPinRequest() -> String { /** * Called when the device requests a passphrase. * - * If `on_device` is true, the user should enter on the Trezor itself — - * return any non-empty string (e.g., "ok") to acknowledge. + * If `on_device` is true, the user should enter the passphrase on the + * Trezor itself — return `PassphraseResponse::Standard` (or + * `Hidden { value: "ok" }`) to acknowledge. * - * If `on_device` is false, show a passphrase input UI and return the value. - * Return empty string to cancel. + * If `on_device` is false, show a passphrase input UI and return the + * matching `PassphraseResponse` variant. */ -open func onPassphraseRequest(onDevice: Bool) -> String { - return try! FfiConverterString.lift(try! rustCall() { +open func onPassphraseRequest(onDevice: Bool) -> PassphraseResponse { + return try! FfiConverterTypePassphraseResponse_lift(try! rustCall() { uniffi_bitkitcore_fn_method_trezoruicallback_on_passphrase_request(self.uniffiClonePointer(), FfiConverterBool.lower(onDevice),$0 ) @@ -1477,7 +1467,7 @@ fileprivate struct UniffiCallbackInterfaceTrezorUiCallback { uniffiCallStatus: UnsafeMutablePointer ) in let makeCall = { - () throws -> String in + () throws -> PassphraseResponse in guard let uniffiObj = try? FfiConverterTypeTrezorUiCallback.handleMap.get(handle: uniffiHandle) else { throw UniffiInternalError.unexpectedStaleHandle } @@ -1487,7 +1477,7 @@ fileprivate struct UniffiCallbackInterfaceTrezorUiCallback { } - let writeReturn = { uniffiOutReturn.pointee = FfiConverterString.lower($0) } + let writeReturn = { uniffiOutReturn.pointee = FfiConverterTypePassphraseResponse_lower($0) } uniffiTraitInterfaceCall( callStatus: uniffiCallStatus, makeCall: makeCall, @@ -15547,6 +15537,97 @@ extension NetworkType: Codable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum PassphraseResponse { + + /** + * User cancelled — aborts the pending operation. + */ + case cancel + /** + * Standard wallet — no passphrase, equivalent to `Some("")` on the device. + */ + case standard + /** + * Hidden wallet — derived from the supplied passphrase. + */ + case hidden(value: String + ) +} + + +#if compiler(>=6) +extension PassphraseResponse: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypePassphraseResponse: FfiConverterRustBuffer { + typealias SwiftType = PassphraseResponse + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PassphraseResponse { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .cancel + + case 2: return .standard + + case 3: return .hidden(value: try FfiConverterString.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: PassphraseResponse, into buf: inout [UInt8]) { + switch value { + + + case .cancel: + writeInt(&buf, Int32(1)) + + + case .standard: + writeInt(&buf, Int32(2)) + + + case let .hidden(value): + writeInt(&buf, Int32(3)) + FfiConverterString.write(value, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePassphraseResponse_lift(_ buf: RustBuffer) throws -> PassphraseResponse { + return try FfiConverterTypePassphraseResponse.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePassphraseResponse_lower(_ value: PassphraseResponse) -> RustBuffer { + return FfiConverterTypePassphraseResponse.lower(value) +} + + +extension PassphraseResponse: Equatable, Hashable {} + +extension PassphraseResponse: Codable {} + + + + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -16402,6 +16483,10 @@ public enum TrezorError: Swift.Error { * Passphrase is required */ case PassphraseRequired + /** + * Passphrase entry cancelled + */ + case PassphraseCancelled /** * Action cancelled by user on device */ @@ -16473,20 +16558,21 @@ public struct FfiConverterTypeTrezorError: FfiConverterRustBuffer { case 9: return .PinCancelled case 10: return .InvalidPin case 11: return .PassphraseRequired - case 12: return .UserCancelled - case 13: return .Timeout - case 14: return .InvalidPath( + case 12: return .PassphraseCancelled + case 13: return .UserCancelled + case 14: return .Timeout + case 15: return .InvalidPath( errorDetails: try FfiConverterString.read(from: &buf) ) - case 15: return .DeviceError( + case 16: return .DeviceError( errorDetails: try FfiConverterString.read(from: &buf) ) - case 16: return .NotInitialized - case 17: return .NotConnected - case 18: return .SessionError( + case 17: return .NotInitialized + case 18: return .NotConnected + case 19: return .SessionError( errorDetails: try FfiConverterString.read(from: &buf) ) - case 19: return .IoError( + case 20: return .IoError( errorDetails: try FfiConverterString.read(from: &buf) ) @@ -16549,39 +16635,43 @@ public struct FfiConverterTypeTrezorError: FfiConverterRustBuffer { writeInt(&buf, Int32(11)) - case .UserCancelled: + case .PassphraseCancelled: writeInt(&buf, Int32(12)) - case .Timeout: + case .UserCancelled: writeInt(&buf, Int32(13)) - case let .InvalidPath(errorDetails): + case .Timeout: writeInt(&buf, Int32(14)) + + + case let .InvalidPath(errorDetails): + writeInt(&buf, Int32(15)) FfiConverterString.write(errorDetails, into: &buf) case let .DeviceError(errorDetails): - writeInt(&buf, Int32(15)) + writeInt(&buf, Int32(16)) FfiConverterString.write(errorDetails, into: &buf) case .NotInitialized: - writeInt(&buf, Int32(16)) + writeInt(&buf, Int32(17)) case .NotConnected: - writeInt(&buf, Int32(17)) + writeInt(&buf, Int32(18)) case let .SessionError(errorDetails): - writeInt(&buf, Int32(18)) + writeInt(&buf, Int32(19)) FfiConverterString.write(errorDetails, into: &buf) case let .IoError(errorDetails): - writeInt(&buf, Int32(19)) + writeInt(&buf, Int32(20)) FfiConverterString.write(errorDetails, into: &buf) } @@ -21106,7 +21196,7 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_method_trezoruicallback_on_pin_request() != 50474) { return InitializationResult.apiChecksumMismatch } - if (uniffi_bitkitcore_checksum_method_trezoruicallback_on_passphrase_request() != 63487) { + if (uniffi_bitkitcore_checksum_method_trezoruicallback_on_passphrase_request() != 37914) { return InitializationResult.apiChecksumMismatch } diff --git a/bindings/python/bitkitcore/bitkitcore.py b/bindings/python/bitkitcore/bitkitcore.py index 50e10fe..bcbfde0 100644 --- a/bindings/python/bitkitcore/bitkitcore.py +++ b/bindings/python/bitkitcore/bitkitcore.py @@ -761,7 +761,7 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_bitkitcore_checksum_method_trezoruicallback_on_pin_request() != 50474: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_bitkitcore_checksum_method_trezoruicallback_on_passphrase_request() != 63487: + if lib.uniffi_bitkitcore_checksum_method_trezoruicallback_on_passphrase_request() != 37914: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") # A ctypes library to expose the extern-C FFI definitions. @@ -12427,6 +12427,136 @@ def write(value, buf): +class PassphraseResponse: + def __init__(self): + raise RuntimeError("PassphraseResponse cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class CANCEL: + """ + User cancelled — aborts the pending operation. + """ + + + def __init__(self,): + pass + + def __str__(self): + return "PassphraseResponse.CANCEL()".format() + + def __eq__(self, other): + if not other.is_CANCEL(): + return False + return True + + class STANDARD: + """ + Standard wallet — no passphrase, equivalent to `Some("")` on the device. + """ + + + def __init__(self,): + pass + + def __str__(self): + return "PassphraseResponse.STANDARD()".format() + + def __eq__(self, other): + if not other.is_STANDARD(): + return False + return True + + class HIDDEN: + """ + Hidden wallet — derived from the supplied passphrase. + """ + + value: "str" + + def __init__(self,value: "str"): + self.value = value + + def __str__(self): + return "PassphraseResponse.HIDDEN(value={})".format(self.value) + + def __eq__(self, other): + if not other.is_HIDDEN(): + return False + if self.value != other.value: + return False + return True + + + + # For each variant, we have `is_NAME` and `is_name` methods for easily checking + # whether an instance is that variant. + def is_CANCEL(self) -> bool: + return isinstance(self, PassphraseResponse.CANCEL) + def is_cancel(self) -> bool: + return isinstance(self, PassphraseResponse.CANCEL) + def is_STANDARD(self) -> bool: + return isinstance(self, PassphraseResponse.STANDARD) + def is_standard(self) -> bool: + return isinstance(self, PassphraseResponse.STANDARD) + def is_HIDDEN(self) -> bool: + return isinstance(self, PassphraseResponse.HIDDEN) + def is_hidden(self) -> bool: + return isinstance(self, PassphraseResponse.HIDDEN) + + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +PassphraseResponse.CANCEL = type("PassphraseResponse.CANCEL", (PassphraseResponse.CANCEL, PassphraseResponse,), {}) # type: ignore +PassphraseResponse.STANDARD = type("PassphraseResponse.STANDARD", (PassphraseResponse.STANDARD, PassphraseResponse,), {}) # type: ignore +PassphraseResponse.HIDDEN = type("PassphraseResponse.HIDDEN", (PassphraseResponse.HIDDEN, PassphraseResponse,), {}) # type: ignore + + + + +class _UniffiConverterTypePassphraseResponse(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return PassphraseResponse.CANCEL( + ) + if variant == 2: + return PassphraseResponse.STANDARD( + ) + if variant == 3: + return PassphraseResponse.HIDDEN( + _UniffiConverterString.read(buf), + ) + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value.is_CANCEL(): + return + if value.is_STANDARD(): + return + if value.is_HIDDEN(): + _UniffiConverterString.check_lower(value.value) + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value.is_CANCEL(): + buf.write_i32(1) + if value.is_STANDARD(): + buf.write_i32(2) + if value.is_HIDDEN(): + buf.write_i32(3) + _UniffiConverterString.write(value.value, buf) + + + + + + + class PaymentState(enum.Enum): PENDING = 0 @@ -13457,6 +13587,17 @@ def __init__(self): def __repr__(self): return "TrezorError.PassphraseRequired({})".format(str(self)) _UniffiTempTrezorError.PassphraseRequired = PassphraseRequired # type: ignore + class PassphraseCancelled(_UniffiTempTrezorError): + """ + Passphrase entry cancelled + """ + + def __init__(self): + pass + + def __repr__(self): + return "TrezorError.PassphraseCancelled({})".format(str(self)) + _UniffiTempTrezorError.PassphraseCancelled = PassphraseCancelled # type: ignore class UserCancelled(_UniffiTempTrezorError): """ Action cancelled by user on device @@ -13604,30 +13745,33 @@ def read(buf): return TrezorError.PassphraseRequired( ) if variant == 12: - return TrezorError.UserCancelled( + return TrezorError.PassphraseCancelled( ) if variant == 13: - return TrezorError.Timeout( + return TrezorError.UserCancelled( ) if variant == 14: + return TrezorError.Timeout( + ) + if variant == 15: return TrezorError.InvalidPath( _UniffiConverterString.read(buf), ) - if variant == 15: + if variant == 16: return TrezorError.DeviceError( _UniffiConverterString.read(buf), ) - if variant == 16: + if variant == 17: return TrezorError.NotInitialized( ) - if variant == 17: + if variant == 18: return TrezorError.NotConnected( ) - if variant == 18: + if variant == 19: return TrezorError.SessionError( _UniffiConverterString.read(buf), ) - if variant == 19: + if variant == 20: return TrezorError.IoError( _UniffiConverterString.read(buf), ) @@ -13661,6 +13805,8 @@ def check_lower(value): return if isinstance(value, TrezorError.PassphraseRequired): return + if isinstance(value, TrezorError.PassphraseCancelled): + return if isinstance(value, TrezorError.UserCancelled): return if isinstance(value, TrezorError.Timeout): @@ -13710,25 +13856,27 @@ def write(value, buf): buf.write_i32(10) if isinstance(value, TrezorError.PassphraseRequired): buf.write_i32(11) - if isinstance(value, TrezorError.UserCancelled): + if isinstance(value, TrezorError.PassphraseCancelled): buf.write_i32(12) - if isinstance(value, TrezorError.Timeout): + if isinstance(value, TrezorError.UserCancelled): buf.write_i32(13) - if isinstance(value, TrezorError.InvalidPath): + if isinstance(value, TrezorError.Timeout): buf.write_i32(14) + if isinstance(value, TrezorError.InvalidPath): + buf.write_i32(15) _UniffiConverterString.write(value.error_details, buf) if isinstance(value, TrezorError.DeviceError): - buf.write_i32(15) + buf.write_i32(16) _UniffiConverterString.write(value.error_details, buf) if isinstance(value, TrezorError.NotInitialized): - buf.write_i32(16) - if isinstance(value, TrezorError.NotConnected): buf.write_i32(17) - if isinstance(value, TrezorError.SessionError): + if isinstance(value, TrezorError.NotConnected): buf.write_i32(18) + if isinstance(value, TrezorError.SessionError): + buf.write_i32(19) _UniffiConverterString.write(value.error_details, buf) if isinstance(value, TrezorError.IoError): - buf.write_i32(19) + buf.write_i32(20) _UniffiConverterString.write(value.error_details, buf) @@ -16983,12 +17131,6 @@ class TrezorUiCallbackProtocol(typing.Protocol): The native layer (iOS/Android) should implement this to show PIN/passphrase input UI when the device requests it during operations like signing. - - Methods return `String`: - - Empty string (`""`) = cancel the request - - Non-empty string = the user's input (PIN or passphrase) - - This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. """ def on_pin_request(self, ): @@ -17004,11 +17146,12 @@ def on_passphrase_request(self, on_device: "bool"): """ Called when the device requests a passphrase. - If `on_device` is true, the user should enter on the Trezor itself — - return any non-empty string (e.g., "ok") to acknowledge. + If `on_device` is true, the user should enter the passphrase on the + Trezor itself — return `PassphraseResponse::Standard` (or + `Hidden { value: "ok" }`) to acknowledge. - If `on_device` is false, show a passphrase input UI and return the value. - Return empty string to cancel. + If `on_device` is false, show a passphrase input UI and return the + matching `PassphraseResponse` variant. """ raise NotImplementedError @@ -17023,12 +17166,6 @@ class TrezorUiCallback(): The native layer (iOS/Android) should implement this to show PIN/passphrase input UI when the device requests it during operations like signing. - - Methods return `String`: - - Empty string (`""`) = cancel the request - - Non-empty string = the user's input (PIN or passphrase) - - This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. """ def on_pin_request(self, ): @@ -17044,11 +17181,12 @@ def on_passphrase_request(self, on_device: "bool"): """ Called when the device requests a passphrase. - If `on_device` is true, the user should enter on the Trezor itself — - return any non-empty string (e.g., "ok") to acknowledge. + If `on_device` is true, the user should enter the passphrase on the + Trezor itself — return `PassphraseResponse::Standard` (or + `Hidden { value: "ok" }`) to acknowledge. - If `on_device` is false, show a passphrase input UI and return the value. - Return empty string to cancel. + If `on_device` is false, show a passphrase input UI and return the + matching `PassphraseResponse` variant. """ raise NotImplementedError @@ -17059,12 +17197,6 @@ class TrezorUiCallbackImpl(): The native layer (iOS/Android) should implement this to show PIN/passphrase input UI when the device requests it during operations like signing. - - Methods return `String`: - - Empty string (`""`) = cancel the request - - Non-empty string = the user's input (PIN or passphrase) - - This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. """ _pointer: ctypes.c_void_p @@ -17107,20 +17239,21 @@ def on_pin_request(self, ) -> "str": - def on_passphrase_request(self, on_device: "bool") -> "str": + def on_passphrase_request(self, on_device: "bool") -> "PassphraseResponse": """ Called when the device requests a passphrase. - If `on_device` is true, the user should enter on the Trezor itself — - return any non-empty string (e.g., "ok") to acknowledge. + If `on_device` is true, the user should enter the passphrase on the + Trezor itself — return `PassphraseResponse::Standard` (or + `Hidden { value: "ok" }`) to acknowledge. - If `on_device` is false, show a passphrase input UI and return the value. - Return empty string to cancel. + If `on_device` is false, show a passphrase input UI and return the + matching `PassphraseResponse` variant. """ _UniffiConverterBool.check_lower(on_device) - return _UniffiConverterString.lift( + return _UniffiConverterTypePassphraseResponse.lift( _uniffi_rust_call(_UniffiLib.uniffi_bitkitcore_fn_method_trezoruicallback_on_passphrase_request,self._uniffi_clone_pointer(), _UniffiConverterBool.lower(on_device)) ) @@ -17169,7 +17302,7 @@ def make_call(): def write_return_value(v): - uniffi_out_return[0] = _UniffiConverterString.lower(v) + uniffi_out_return[0] = _UniffiConverterTypePassphraseResponse.lower(v) _uniffi_trait_interface_call( uniffi_call_status_ptr.contents, make_call, @@ -19454,6 +19587,7 @@ def wipe_all_transaction_details() -> None: "ManualRefundStateEnum", "Network", "NetworkType", + "PassphraseResponse", "PaymentState", "PaymentType", "PubkyAuthKind", diff --git a/bindings/python/bitkitcore/libbitkitcore.dylib b/bindings/python/bitkitcore/libbitkitcore.dylib index e62b2cd..7905a75 100755 Binary files a/bindings/python/bitkitcore/libbitkitcore.dylib and b/bindings/python/bitkitcore/libbitkitcore.dylib differ diff --git a/src/lib.rs b/src/lib.rs index 18a6998..3697508 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,9 @@ use crate::modules::pubky::{PubkyAuthDetails, PubkyAuthKind, PubkyError, PubkyPr use crate::modules::trezor::account_type_to_script_type; pub use crate::modules::trezor::{ get_transport_callback, trezor_is_ble_available, trezor_set_transport_callback, - trezor_set_ui_callback, NativeDeviceInfo, TrezorCallMessageResult, TrezorTransportCallback, - TrezorTransportReadResult, TrezorTransportWriteResult, TrezorUiCallback, + trezor_set_ui_callback, NativeDeviceInfo, PassphraseResponse, TrezorCallMessageResult, + TrezorTransportCallback, TrezorTransportReadResult, TrezorTransportWriteResult, + TrezorUiCallback, }; use crate::modules::trezor::{ TrezorAddressResponse, TrezorCoinType, TrezorDeviceInfo, TrezorError, TrezorFeatures, diff --git a/src/modules/trezor/callbacks.rs b/src/modules/trezor/callbacks.rs index c3315b8..eef4898 100644 --- a/src/modules/trezor/callbacks.rs +++ b/src/modules/trezor/callbacks.rs @@ -172,16 +172,32 @@ pub trait TrezorTransportCallback: Send + Sync { // UI callback trait // ============================================================================ +/// Re-export the upstream `PassphraseResponse` and attach UniFFI scaffolding +/// to it via `#[uniffi::remote(Enum)]`. trezor-connect-rs intentionally does +/// not depend on uniffi, so we add the bindings metadata externally here. +/// The variant list below is parsed by the macro but not redefined as a type +/// — `PassphraseResponse` in scope resolves to the upstream enum. +/// +/// NOTE: the variant list below must match `trezor_connect_rs::PassphraseResponse` +/// exactly (currently trezor-connect-rs 0.3.x). If a future bump reshapes the +/// upstream enum, update these variants in lockstep — the adapter tests in +/// `tests.rs` (`test_passphrase_adapter_*`) guard the variant-for-variant mapping. +pub use trezor_connect_rs::PassphraseResponse; + +#[uniffi::remote(Enum)] +pub enum PassphraseResponse { + /// User cancelled — aborts the pending operation. + Cancel, + /// Standard wallet — no passphrase, equivalent to `Some("")` on the device. + Standard, + /// Hidden wallet — derived from the supplied passphrase. + Hidden { value: String }, +} + /// Callback interface for handling PIN and passphrase requests from the Trezor device. /// /// The native layer (iOS/Android) should implement this to show PIN/passphrase /// input UI when the device requests it during operations like signing. -/// -/// Methods return `String`: -/// - Empty string (`""`) = cancel the request -/// - Non-empty string = the user's input (PIN or passphrase) -/// -/// This matches the existing `get_pairing_code` pattern used in `TrezorTransportCallback`. #[uniffi::export(with_foreign)] pub trait TrezorUiCallback: Send + Sync { /// Called when the device requests a PIN. @@ -192,12 +208,13 @@ pub trait TrezorUiCallback: Send + Sync { /// Called when the device requests a passphrase. /// - /// If `on_device` is true, the user should enter on the Trezor itself — - /// return any non-empty string (e.g., "ok") to acknowledge. + /// If `on_device` is true, the user should enter the passphrase on the + /// Trezor itself — return `PassphraseResponse::Standard` (or + /// `Hidden { value: "ok" }`) to acknowledge. /// - /// If `on_device` is false, show a passphrase input UI and return the value. - /// Return empty string to cancel. - fn on_passphrase_request(&self, on_device: bool) -> String; + /// If `on_device` is false, show a passphrase input UI and return the + /// matching `PassphraseResponse` variant. + fn on_passphrase_request(&self, on_device: bool) -> PassphraseResponse; } // ============================================================================ diff --git a/src/modules/trezor/errors.rs b/src/modules/trezor/errors.rs index b6336da..bfe05c6 100644 --- a/src/modules/trezor/errors.rs +++ b/src/modules/trezor/errors.rs @@ -48,6 +48,10 @@ pub enum TrezorError { #[error("Passphrase is required")] PassphraseRequired, + /// Passphrase entry cancelled + #[error("Passphrase entry cancelled")] + PassphraseCancelled, + /// Action cancelled by user on device #[error("Action cancelled by user")] UserCancelled, @@ -174,6 +178,7 @@ impl From for TrezorError { TcDeviceError::InvalidPin => TrezorError::InvalidPin, TcDeviceError::PinCancelled => TrezorError::PinCancelled, TcDeviceError::PassphraseRequired => TrezorError::PassphraseRequired, + TcDeviceError::PassphraseCancelled => TrezorError::PassphraseCancelled, TcDeviceError::NotInitialized => TrezorError::DeviceError { error_details: "Device is not initialized".to_string(), }, diff --git a/src/modules/trezor/implementation.rs b/src/modules/trezor/implementation.rs index e078d0f..1f227c3 100644 --- a/src/modules/trezor/implementation.rs +++ b/src/modules/trezor/implementation.rs @@ -310,12 +310,17 @@ impl TransportCallback for CallbackAdapter { } } -/// Adapter bridging bitkit-core's `TrezorUiCallback` (String-based, UniFFI compatible) -/// to trezor-connect-rs's `TrezorUiCallback` (Option-based). +/// Adapter bridging bitkit-core's UniFFI-exported `TrezorUiCallback` to +/// trezor-connect-rs's `TrezorUiCallback`. /// -/// Conversion: empty string → `None` (cancel), non-empty → `Some(value)` (user input). -struct UiCallbackAdapter { - callback: Arc, +/// PIN: empty string → `None` (cancel), non-empty → `Some(value)`. (No +/// upstream enum exists for PIN; we keep the legacy `String → Option` encoding.) +/// +/// Passphrase: a pure pass-through. Both traits return the same upstream +/// `PassphraseResponse` type — bitkit-core attaches UniFFI scaffolding to it +/// via `#[uniffi::remote(Enum)]` (see `callbacks.rs`). +pub(super) struct UiCallbackAdapter { + pub(super) callback: Arc, } impl trezor_connect_rs::TrezorUiCallback for UiCallbackAdapter { @@ -328,13 +333,8 @@ impl trezor_connect_rs::TrezorUiCallback for UiCallbackAdapter { } } - fn on_passphrase_request(&self, on_device: bool) -> Option { - let result = self.callback.on_passphrase_request(on_device); - if result.is_empty() { - None - } else { - Some(result) - } + fn on_passphrase_request(&self, on_device: bool) -> trezor_connect_rs::PassphraseResponse { + self.callback.on_passphrase_request(on_device) } } diff --git a/src/modules/trezor/tests.rs b/src/modules/trezor/tests.rs index f3deb44..5e74a7a 100644 --- a/src/modules/trezor/tests.rs +++ b/src/modules/trezor/tests.rs @@ -651,4 +651,119 @@ mod tests { assert_eq!(result, expected); } } + + // ======================================================================== + // UI Callback Adapter Tests + // ======================================================================== + // + // These tests lock down the bridge between bitkit-core's UniFFI-friendly + // `TrezorUiCallback` and trezor-connect-rs's `TrezorUiCallback`. The + // passphrase variants are variant-for-variant identical across the two + // crates; the adapter just retypes them so UniFFI's foreign-callback + // requirements (which trezor-connect-rs intentionally doesn't depend on) + // can stay isolated to bitkit-core. + + use crate::modules::trezor::implementation::UiCallbackAdapter; + use crate::modules::trezor::PassphraseResponse; + use std::sync::{Arc, Mutex}; + use trezor_connect_rs::TrezorUiCallback as TcUiCallback; + + /// Test double that returns canned responses and records call args. + struct MockUiCallback { + pin_response: String, + passphrase_response: Mutex>, + last_passphrase_on_device: Mutex>, + } + + impl MockUiCallback { + fn new(pin: &str, passphrase: PassphraseResponse) -> Arc { + Arc::new(Self { + pin_response: pin.to_string(), + passphrase_response: Mutex::new(Some(passphrase)), + last_passphrase_on_device: Mutex::new(None), + }) + } + } + + impl crate::TrezorUiCallback for MockUiCallback { + fn on_pin_request(&self) -> String { + self.pin_response.clone() + } + + fn on_passphrase_request(&self, on_device: bool) -> PassphraseResponse { + *self.last_passphrase_on_device.lock().unwrap() = Some(on_device); + self.passphrase_response + .lock() + .unwrap() + .take() + .expect("passphrase_response consumed twice") + } + } + + fn adapter_with(mock: Arc) -> UiCallbackAdapter { + UiCallbackAdapter { callback: mock } + } + + #[test] + fn test_pin_adapter_empty_string_is_cancel() { + let mock = MockUiCallback::new("", PassphraseResponse::Cancel); + let adapter = adapter_with(mock); + assert_eq!(adapter.on_pin_request(), None); + } + + #[test] + fn test_pin_adapter_value_passes_through() { + let mock = MockUiCallback::new("123456", PassphraseResponse::Cancel); + let adapter = adapter_with(mock); + assert_eq!(adapter.on_pin_request(), Some("123456".to_string())); + } + + #[test] + fn test_passphrase_adapter_cancel_maps_to_upstream_cancel() { + let mock = MockUiCallback::new("", PassphraseResponse::Cancel); + let adapter = adapter_with(Arc::clone(&mock)); + assert_eq!( + adapter.on_passphrase_request(false), + trezor_connect_rs::PassphraseResponse::Cancel + ); + assert_eq!(*mock.last_passphrase_on_device.lock().unwrap(), Some(false)); + } + + #[test] + fn test_passphrase_adapter_standard_maps_to_upstream_standard() { + // The crucial case: standard wallet must reach the device as the + // `Standard` variant, not `Cancel`, otherwise the device will think + // the user cancelled. + let mock = MockUiCallback::new("", PassphraseResponse::Standard); + let adapter = adapter_with(mock); + assert_eq!( + adapter.on_passphrase_request(false), + trezor_connect_rs::PassphraseResponse::Standard + ); + } + + #[test] + fn test_passphrase_adapter_hidden_passes_value() { + let mock = MockUiCallback::new( + "", + PassphraseResponse::Hidden { + value: "hunter2".to_string(), + }, + ); + let adapter = adapter_with(mock); + assert_eq!( + adapter.on_passphrase_request(false), + trezor_connect_rs::PassphraseResponse::Hidden { + value: "hunter2".to_string() + } + ); + } + + #[test] + fn test_passphrase_adapter_forwards_on_device_flag() { + let mock = MockUiCallback::new("", PassphraseResponse::Standard); + let adapter = adapter_with(Arc::clone(&mock)); + let _ = adapter.on_passphrase_request(true); + assert_eq!(*mock.last_passphrase_on_device.lock().unwrap(), Some(true)); + } }