diff --git a/Cargo.lock b/Cargo.lock index a6a0b6f..a448210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,7 +475,7 @@ dependencies = [ [[package]] name = "bitkitcore" -version = "0.1.60" +version = "0.1.62" dependencies = [ "android_logger", "async-trait", @@ -515,6 +515,7 @@ dependencies = [ "uniffi", "url", "uuid", + "zeroize", ] [[package]] @@ -4688,9 +4689,9 @@ dependencies = [ [[package]] name = "trezor-connect-rs" -version = "0.2.8" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1d7bbb66e791e3bf063373b40c6a7911c4130f4fb14298ecb5bfae5040ed84" +checksum = "e2243996b0d2a6f5dc6526a31922b3398fc1b1a56623ed24318257138e38f69b" dependencies = [ "aes-gcm", "async-trait", @@ -4715,6 +4716,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tracing", + "unicode-normalization", "uuid", "x25519-dalek", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 936214a..4ea0b7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitkitcore" -version = "0.1.60" +version = "0.1.62" edition = "2021" [lib] @@ -25,6 +25,7 @@ lnurl-rs = "0.9.0" openssl = { version = "0.10", features = ["vendored"] } rand = "0.8.5" once_cell = "1.20.2" +zeroize = "1.8" rusqlite = { version = "0.32.1", features = ["bundled", "unlock_notify"] } rust-blocktank-client = { version = "0.0.16", features = ["rustls-tls"] } reqwest = { version = "0.12.12", features = ["json", "rustls-tls", "rustls-tls-native-roots"], default-features = false } @@ -46,11 +47,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.2", 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.2", 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..69c46a7 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.62" +let checksum = "6cf443e6614fe9c1d875809c89eeb291f3cfa7c6d1cd7e80b0281407ee0b834c" 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..2facdd5 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.62 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..12de88d 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..d74ff9f 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..b208967 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..2c70e2b 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..1ad708e 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 @@ -1770,7 +1770,7 @@ internal object IntegrityCheckingUniffiLib : Library { if (uniffi_bitkitcore_checksum_func_trezor_clear_credentials() != 41940.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (uniffi_bitkitcore_checksum_func_trezor_connect() != 6551.toShort()) { + if (uniffi_bitkitcore_checksum_func_trezor_connect() != 49232.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } if (uniffi_bitkitcore_checksum_func_trezor_disconnect() != 48780.toShort()) { @@ -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() != 33994.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } } @@ -3057,6 +3057,7 @@ internal object UniffiLib : Library { @JvmStatic external fun uniffi_bitkitcore_fn_func_trezor_connect( `deviceId`: RustBufferByValue, + `selection`: RustBufferByValue, ): Long @JvmStatic external fun uniffi_bitkitcore_fn_func_trezor_disconnect( @@ -4413,12 +4414,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 +4526,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 device is asking for the passphrase to be + * entered on the Trezor itself — return `PassphraseResponse::OnDevice`. * - * 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 + * `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + * `OnDevice` (defer entry to the Trezor), or `Cancel`. */ - 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 +4618,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) } @@ -7128,6 +7124,7 @@ public object FfiConverterTypeTrezorFeatures: 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), + ) + 4 -> PassphraseResponse.OnDevice + 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`) + ) + } + is PassphraseResponse.OnDevice -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + } + + 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 + } + is PassphraseResponse.OnDevice -> { + buf.putInt(4) + 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 +9759,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 +9830,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 +9923,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 } @@ -9949,6 +10025,63 @@ public object FfiConverterTypeTxDirection: FfiConverterRustBuffer { +public object FfiConverterTypeWalletSelection : FfiConverterRustBuffer{ + override fun read(buf: ByteBuffer): WalletSelection { + return when(buf.getInt()) { + 1 -> WalletSelection.Standard + 2 -> WalletSelection.Hidden( + FfiConverterString.read(buf), + ) + 3 -> WalletSelection.OnDevice + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: WalletSelection): ULong = when(value) { + is WalletSelection.Standard -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is WalletSelection.Hidden -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`passphrase`) + ) + } + is WalletSelection.OnDevice -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + } + + override fun write(value: WalletSelection, buf: ByteBuffer) { + when(value) { + is WalletSelection.Standard -> { + buf.putInt(1) + Unit + } + is WalletSelection.Hidden -> { + buf.putInt(2) + FfiConverterString.write(value.`passphrase`, buf) + Unit + } + is WalletSelection.OnDevice -> { + buf.putInt(3) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + + + + + public object FfiConverterTypeWordCount: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): WordCount = try { WordCount.entries[buf.getInt() - 1] @@ -13683,12 +13816,18 @@ public suspend fun `trezorClearCredentials`(`deviceId`: kotlin.String) { * * For Bluetooth devices, this will use stored credentials if available, * or trigger pairing if needed. + * + * `selection` chooses which wallet to open. On THP devices (Safe 5/7) it is + * bound to the session at creation, so it must be supplied on every connect; + * there is no separate "set passphrase" step and nothing is cached between + * calls. Reconnect with a different `selection` to switch wallets. */ @Throws(TrezorException::class, kotlin.coroutines.cancellation.CancellationException::class) -public suspend fun `trezorConnect`(`deviceId`: kotlin.String): TrezorFeatures { +public suspend fun `trezorConnect`(`deviceId`: kotlin.String, `selection`: WalletSelection): TrezorFeatures { return uniffiRustCallAsync( UniffiLib.uniffi_bitkitcore_fn_func_trezor_connect( FfiConverterString.lower(`deviceId`), + FfiConverterTypeWalletSelection.lower(`selection`), ), { future, callback, continuation -> UniffiLib.ffi_bitkitcore_rust_future_poll_rust_buffer(future, callback, continuation) }, { future, continuation -> UniffiLib.ffi_bitkitcore_rust_future_complete_rust_buffer(future, continuation) }, 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..8de8781 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 device is asking for the passphrase to be + * entered on the Trezor itself — return `PassphraseResponse::OnDevice`. * - * 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 + * `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + * `OnDevice` (defer entry to the Trezor), or `Cancel`. */ - public fun `onPassphraseRequest`(`onDevice`: kotlin.Boolean): kotlin.String + public fun `onPassphraseRequest`(`onDevice`: kotlin.Boolean): PassphraseResponse public companion object } @@ -1815,7 +1810,12 @@ public data class TrezorFeatures ( /** * Whether the device needs backup */ - val `needsBackup`: kotlin.Boolean? + val `needsBackup`: kotlin.Boolean?, + /** + * Whether the device can accept passphrase entry on the device itself + * (`Capability_PassphraseEntry`). When false/None, use host entry only. + */ + val `passphraseEntryCapable`: kotlin.Boolean? ) { public companion object } @@ -3380,6 +3380,45 @@ 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 passphrase entered on the host. + */@kotlinx.serialization.Serializable + public data class Hidden( + val `value`: kotlin.String, + ) : PassphraseResponse() { + } + + /** + * Enter the passphrase on the Trezor device itself instead of on the host. + */ + @kotlinx.serialization.Serializable + public data object OnDevice : PassphraseResponse() + + +} + + + + + + @kotlinx.serialization.Serializable public enum class PaymentState { @@ -3738,6 +3777,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 */ @@ -3908,6 +3956,47 @@ public enum class TxDirection { +/** + * Which wallet a connection should open. + * + * Passed to `trezor_connect` and consumed at connect time — the passphrase is + * a one-shot input, not retained anywhere afterwards. On THP devices (Safe + * 5/7) it is bound to the session at `ThpCreateNewSession`; on legacy devices + * the mid-operation `PassphraseRequest` is answered from the UI callback + * instead (see [`TrezorUiCallback`]). + */ +@kotlinx.serialization.Serializable +public sealed class WalletSelection { + + /** + * The standard wallet — no passphrase. + */ + @kotlinx.serialization.Serializable + public data object Standard : WalletSelection() + + + /** + * A hidden wallet whose passphrase is entered on the host. + */@kotlinx.serialization.Serializable + public data class Hidden( + val `passphrase`: kotlin.String, + ) : WalletSelection() { + } + + /** + * A hidden wallet whose passphrase is entered on the Trezor itself. + */ + @kotlinx.serialization.Serializable + public data object OnDevice : WalletSelection() + + +} + + + + + + @kotlinx.serialization.Serializable public enum class WordCount { diff --git a/bindings/ios/BitkitCore.xcframework.zip b/bindings/ios/BitkitCore.xcframework.zip index 1fd45b7..3c77de6 100644 Binary files a/bindings/ios/BitkitCore.xcframework.zip and b/bindings/ios/BitkitCore.xcframework.zip differ diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h index 1d40072..a057f92 100644 --- a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h +++ b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h @@ -979,7 +979,7 @@ uint64_t uniffi_bitkitcore_fn_func_trezor_clear_credentials(RustBuffer device_id #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_CONNECT #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_CONNECT -uint64_t uniffi_bitkitcore_fn_func_trezor_connect(RustBuffer device_id +uint64_t uniffi_bitkitcore_fn_func_trezor_connect(RustBuffer device_id, RustBuffer selection ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_DISCONNECT diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a index 3bd1161..ab9a237 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/Headers/bitkitcoreFFI.h b/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h index 1d40072..a057f92 100644 --- a/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h +++ b/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h @@ -979,7 +979,7 @@ uint64_t uniffi_bitkitcore_fn_func_trezor_clear_credentials(RustBuffer device_id #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_CONNECT #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_CONNECT -uint64_t uniffi_bitkitcore_fn_func_trezor_connect(RustBuffer device_id +uint64_t uniffi_bitkitcore_fn_func_trezor_connect(RustBuffer device_id, RustBuffer selection ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_DISCONNECT diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a b/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a index 105442e..e21f0b8 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..4c56d01 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 device is asking for the passphrase to be + * entered on the Trezor itself — return `PassphraseResponse::OnDevice`. * - * 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 + * `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + * `OnDevice` (defer entry to the Trezor), or `Cancel`. */ - 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 device is asking for the passphrase to be + * entered on the Trezor itself — return `PassphraseResponse::OnDevice`. * - * 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 + * `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + * `OnDevice` (defer entry to the Trezor), or `Cancel`. */ -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, @@ -9856,6 +9846,11 @@ public struct TrezorFeatures { * Whether the device needs backup */ public var needsBackup: Bool? + /** + * Whether the device can accept passphrase entry on the device itself + * (`Capability_PassphraseEntry`). When false/None, use host entry only. + */ + public var passphraseEntryCapable: Bool? // Default memberwise initializers are never public by default, so we // declare one manually. @@ -9892,7 +9887,11 @@ public struct TrezorFeatures { */initialized: Bool?, /** * Whether the device needs backup - */needsBackup: Bool?) { + */needsBackup: Bool?, + /** + * Whether the device can accept passphrase entry on the device itself + * (`Capability_PassphraseEntry`). When false/None, use host entry only. + */passphraseEntryCapable: Bool?) { self.vendor = vendor self.model = model self.label = label @@ -9904,6 +9903,7 @@ public struct TrezorFeatures { self.passphraseProtection = passphraseProtection self.initialized = initialized self.needsBackup = needsBackup + self.passphraseEntryCapable = passphraseEntryCapable } } @@ -9947,6 +9947,9 @@ extension TrezorFeatures: Equatable, Hashable { if lhs.needsBackup != rhs.needsBackup { return false } + if lhs.passphraseEntryCapable != rhs.passphraseEntryCapable { + return false + } return true } @@ -9962,6 +9965,7 @@ extension TrezorFeatures: Equatable, Hashable { hasher.combine(passphraseProtection) hasher.combine(initialized) hasher.combine(needsBackup) + hasher.combine(passphraseEntryCapable) } } @@ -9986,7 +9990,8 @@ public struct FfiConverterTypeTrezorFeatures: FfiConverterRustBuffer { pinProtection: FfiConverterOptionBool.read(from: &buf), passphraseProtection: FfiConverterOptionBool.read(from: &buf), initialized: FfiConverterOptionBool.read(from: &buf), - needsBackup: FfiConverterOptionBool.read(from: &buf) + needsBackup: FfiConverterOptionBool.read(from: &buf), + passphraseEntryCapable: FfiConverterOptionBool.read(from: &buf) ) } @@ -10002,6 +10007,7 @@ public struct FfiConverterTypeTrezorFeatures: FfiConverterRustBuffer { FfiConverterOptionBool.write(value.passphraseProtection, into: &buf) FfiConverterOptionBool.write(value.initialized, into: &buf) FfiConverterOptionBool.write(value.needsBackup, into: &buf) + FfiConverterOptionBool.write(value.passphraseEntryCapable, into: &buf) } } @@ -15547,6 +15553,107 @@ 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 passphrase entered on the host. + */ + case hidden(value: String + ) + /** + * Enter the passphrase on the Trezor device itself instead of on the host. + */ + case onDevice +} + + +#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) + ) + + case 4: return .onDevice + + 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) + + + case .onDevice: + writeInt(&buf, Int32(4)) + + } + } +} + + +#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 +16509,10 @@ public enum TrezorError: Swift.Error { * Passphrase is required */ case PassphraseRequired + /** + * Passphrase entry cancelled + */ + case PassphraseCancelled /** * Action cancelled by user on device */ @@ -16473,20 +16584,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 +16661,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) } @@ -16913,6 +17029,106 @@ extension TxDirection: Codable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +/** + * Which wallet a connection should open. + * + * Passed to `trezor_connect` and consumed at connect time — the passphrase is + * a one-shot input, not retained anywhere afterwards. On THP devices (Safe + * 5/7) it is bound to the session at `ThpCreateNewSession`; on legacy devices + * the mid-operation `PassphraseRequest` is answered from the UI callback + * instead (see [`TrezorUiCallback`]). + */ + +public enum WalletSelection { + + /** + * The standard wallet — no passphrase. + */ + case standard + /** + * A hidden wallet whose passphrase is entered on the host. + */ + case hidden(passphrase: String + ) + /** + * A hidden wallet whose passphrase is entered on the Trezor itself. + */ + case onDevice +} + + +#if compiler(>=6) +extension WalletSelection: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWalletSelection: FfiConverterRustBuffer { + typealias SwiftType = WalletSelection + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WalletSelection { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .standard + + case 2: return .hidden(passphrase: try FfiConverterString.read(from: &buf) + ) + + case 3: return .onDevice + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: WalletSelection, into buf: inout [UInt8]) { + switch value { + + + case .standard: + writeInt(&buf, Int32(1)) + + + case let .hidden(passphrase): + writeInt(&buf, Int32(2)) + FfiConverterString.write(passphrase, into: &buf) + + + case .onDevice: + writeInt(&buf, Int32(3)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWalletSelection_lift(_ buf: RustBuffer) throws -> WalletSelection { + return try FfiConverterTypeWalletSelection.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWalletSelection_lower(_ value: WalletSelection) -> RustBuffer { + return FfiConverterTypeWalletSelection.lower(value) +} + + +extension WalletSelection: Equatable, Hashable {} + +extension WalletSelection: Codable {} + + + + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -20154,12 +20370,17 @@ public func trezorClearCredentials(deviceId: String)async throws { * * For Bluetooth devices, this will use stored credentials if available, * or trigger pairing if needed. + * + * `selection` chooses which wallet to open. On THP devices (Safe 5/7) it is + * bound to the session at creation, so it must be supplied on every connect; + * there is no separate "set passphrase" step and nothing is cached between + * calls. Reconnect with a different `selection` to switch wallets. */ -public func trezorConnect(deviceId: String)async throws -> TrezorFeatures { +public func trezorConnect(deviceId: String, selection: WalletSelection)async throws -> TrezorFeatures { return try await uniffiRustCallAsync( rustFutureFunc: { - uniffi_bitkitcore_fn_func_trezor_connect(FfiConverterString.lower(deviceId) + uniffi_bitkitcore_fn_func_trezor_connect(FfiConverterString.lower(deviceId),FfiConverterTypeWalletSelection_lower(selection) ) }, pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, @@ -20956,7 +21177,7 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_func_trezor_clear_credentials() != 41940) { return InitializationResult.apiChecksumMismatch } - if (uniffi_bitkitcore_checksum_func_trezor_connect() != 6551) { + if (uniffi_bitkitcore_checksum_func_trezor_connect() != 49232) { return InitializationResult.apiChecksumMismatch } if (uniffi_bitkitcore_checksum_func_trezor_disconnect() != 48780) { @@ -21106,7 +21327,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() != 33994) { return InitializationResult.apiChecksumMismatch } diff --git a/bindings/ios/bitkitcoreFFI.h b/bindings/ios/bitkitcoreFFI.h index 1d40072..a057f92 100644 --- a/bindings/ios/bitkitcoreFFI.h +++ b/bindings/ios/bitkitcoreFFI.h @@ -979,7 +979,7 @@ uint64_t uniffi_bitkitcore_fn_func_trezor_clear_credentials(RustBuffer device_id #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_CONNECT #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_CONNECT -uint64_t uniffi_bitkitcore_fn_func_trezor_connect(RustBuffer device_id +uint64_t uniffi_bitkitcore_fn_func_trezor_connect(RustBuffer device_id, RustBuffer selection ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_TREZOR_DISCONNECT diff --git a/bindings/python/bitkitcore/bitkitcore.py b/bindings/python/bitkitcore/bitkitcore.py index 50e10fe..c1cd0e7 100644 --- a/bindings/python/bitkitcore/bitkitcore.py +++ b/bindings/python/bitkitcore/bitkitcore.py @@ -661,7 +661,7 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_bitkitcore_checksum_func_trezor_clear_credentials() != 41940: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_bitkitcore_checksum_func_trezor_connect() != 6551: + if lib.uniffi_bitkitcore_checksum_func_trezor_connect() != 49232: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_bitkitcore_checksum_func_trezor_disconnect() != 48780: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") @@ -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() != 33994: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") # A ctypes library to expose the extern-C FFI definitions. @@ -1585,6 +1585,7 @@ class _UniffiVTableCallbackInterfaceTrezorUiCallback(ctypes.Structure): _UniffiLib.uniffi_bitkitcore_fn_func_trezor_clear_credentials.restype = ctypes.c_uint64 _UniffiLib.uniffi_bitkitcore_fn_func_trezor_connect.argtypes = ( _UniffiRustBuffer, + _UniffiRustBuffer, ) _UniffiLib.uniffi_bitkitcore_fn_func_trezor_connect.restype = ctypes.c_uint64 _UniffiLib.uniffi_bitkitcore_fn_func_trezor_disconnect.argtypes = ( @@ -7936,7 +7937,13 @@ class TrezorFeatures: Whether the device needs backup """ - def __init__(self, *, vendor: "typing.Optional[str]", model: "typing.Optional[str]", label: "typing.Optional[str]", device_id: "typing.Optional[str]", major_version: "typing.Optional[int]", minor_version: "typing.Optional[int]", patch_version: "typing.Optional[int]", pin_protection: "typing.Optional[bool]", passphrase_protection: "typing.Optional[bool]", initialized: "typing.Optional[bool]", needs_backup: "typing.Optional[bool]"): + passphrase_entry_capable: "typing.Optional[bool]" + """ + Whether the device can accept passphrase entry on the device itself + (`Capability_PassphraseEntry`). When false/None, use host entry only. + """ + + def __init__(self, *, vendor: "typing.Optional[str]", model: "typing.Optional[str]", label: "typing.Optional[str]", device_id: "typing.Optional[str]", major_version: "typing.Optional[int]", minor_version: "typing.Optional[int]", patch_version: "typing.Optional[int]", pin_protection: "typing.Optional[bool]", passphrase_protection: "typing.Optional[bool]", initialized: "typing.Optional[bool]", needs_backup: "typing.Optional[bool]", passphrase_entry_capable: "typing.Optional[bool]"): self.vendor = vendor self.model = model self.label = label @@ -7948,9 +7955,10 @@ def __init__(self, *, vendor: "typing.Optional[str]", model: "typing.Optional[st self.passphrase_protection = passphrase_protection self.initialized = initialized self.needs_backup = needs_backup + self.passphrase_entry_capable = passphrase_entry_capable def __str__(self): - return "TrezorFeatures(vendor={}, model={}, label={}, device_id={}, major_version={}, minor_version={}, patch_version={}, pin_protection={}, passphrase_protection={}, initialized={}, needs_backup={})".format(self.vendor, self.model, self.label, self.device_id, self.major_version, self.minor_version, self.patch_version, self.pin_protection, self.passphrase_protection, self.initialized, self.needs_backup) + return "TrezorFeatures(vendor={}, model={}, label={}, device_id={}, major_version={}, minor_version={}, patch_version={}, pin_protection={}, passphrase_protection={}, initialized={}, needs_backup={}, passphrase_entry_capable={})".format(self.vendor, self.model, self.label, self.device_id, self.major_version, self.minor_version, self.patch_version, self.pin_protection, self.passphrase_protection, self.initialized, self.needs_backup, self.passphrase_entry_capable) def __eq__(self, other): if self.vendor != other.vendor: @@ -7975,6 +7983,8 @@ def __eq__(self, other): return False if self.needs_backup != other.needs_backup: return False + if self.passphrase_entry_capable != other.passphrase_entry_capable: + return False return True class _UniffiConverterTypeTrezorFeatures(_UniffiConverterRustBuffer): @@ -7992,6 +8002,7 @@ def read(buf): passphrase_protection=_UniffiConverterOptionalBool.read(buf), initialized=_UniffiConverterOptionalBool.read(buf), needs_backup=_UniffiConverterOptionalBool.read(buf), + passphrase_entry_capable=_UniffiConverterOptionalBool.read(buf), ) @staticmethod @@ -8007,6 +8018,7 @@ def check_lower(value): _UniffiConverterOptionalBool.check_lower(value.passphrase_protection) _UniffiConverterOptionalBool.check_lower(value.initialized) _UniffiConverterOptionalBool.check_lower(value.needs_backup) + _UniffiConverterOptionalBool.check_lower(value.passphrase_entry_capable) @staticmethod def write(value, buf): @@ -8021,6 +8033,7 @@ def write(value, buf): _UniffiConverterOptionalBool.write(value.passphrase_protection, buf) _UniffiConverterOptionalBool.write(value.initialized, buf) _UniffiConverterOptionalBool.write(value.needs_backup, buf) + _UniffiConverterOptionalBool.write(value.passphrase_entry_capable, buf) class TrezorGetAddressParams: @@ -12427,6 +12440,165 @@ 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 passphrase entered on the host. + """ + + 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 + + class ON_DEVICE: + """ + Enter the passphrase on the Trezor device itself instead of on the host. + """ + + + def __init__(self,): + pass + + def __str__(self): + return "PassphraseResponse.ON_DEVICE()".format() + + def __eq__(self, other): + if not other.is_ON_DEVICE(): + 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) + def is_ON_DEVICE(self) -> bool: + return isinstance(self, PassphraseResponse.ON_DEVICE) + def is_on_device(self) -> bool: + return isinstance(self, PassphraseResponse.ON_DEVICE) + + +# 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 +PassphraseResponse.ON_DEVICE = type("PassphraseResponse.ON_DEVICE", (PassphraseResponse.ON_DEVICE, 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), + ) + if variant == 4: + return PassphraseResponse.ON_DEVICE( + ) + 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 + if value.is_ON_DEVICE(): + 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) + if value.is_ON_DEVICE(): + buf.write_i32(4) + + + + + + + class PaymentState(enum.Enum): PENDING = 0 @@ -13457,6 +13629,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 +13787,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 +13847,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 +13898,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) @@ -13945,6 +14135,146 @@ def write(value, buf): +class WalletSelection: + """ + Which wallet a connection should open. + + Passed to `trezor_connect` and consumed at connect time — the passphrase is + a one-shot input, not retained anywhere afterwards. On THP devices (Safe + 5/7) it is bound to the session at `ThpCreateNewSession`; on legacy devices + the mid-operation `PassphraseRequest` is answered from the UI callback + instead (see [`TrezorUiCallback`]). + """ + + def __init__(self): + raise RuntimeError("WalletSelection cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class STANDARD: + """ + The standard wallet — no passphrase. + """ + + + def __init__(self,): + pass + + def __str__(self): + return "WalletSelection.STANDARD()".format() + + def __eq__(self, other): + if not other.is_STANDARD(): + return False + return True + + class HIDDEN: + """ + A hidden wallet whose passphrase is entered on the host. + """ + + passphrase: "str" + + def __init__(self,passphrase: "str"): + self.passphrase = passphrase + + def __str__(self): + return "WalletSelection.HIDDEN(passphrase={})".format(self.passphrase) + + def __eq__(self, other): + if not other.is_HIDDEN(): + return False + if self.passphrase != other.passphrase: + return False + return True + + class ON_DEVICE: + """ + A hidden wallet whose passphrase is entered on the Trezor itself. + """ + + + def __init__(self,): + pass + + def __str__(self): + return "WalletSelection.ON_DEVICE()".format() + + def __eq__(self, other): + if not other.is_ON_DEVICE(): + 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_STANDARD(self) -> bool: + return isinstance(self, WalletSelection.STANDARD) + def is_standard(self) -> bool: + return isinstance(self, WalletSelection.STANDARD) + def is_HIDDEN(self) -> bool: + return isinstance(self, WalletSelection.HIDDEN) + def is_hidden(self) -> bool: + return isinstance(self, WalletSelection.HIDDEN) + def is_ON_DEVICE(self) -> bool: + return isinstance(self, WalletSelection.ON_DEVICE) + def is_on_device(self) -> bool: + return isinstance(self, WalletSelection.ON_DEVICE) + + +# 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. +WalletSelection.STANDARD = type("WalletSelection.STANDARD", (WalletSelection.STANDARD, WalletSelection,), {}) # type: ignore +WalletSelection.HIDDEN = type("WalletSelection.HIDDEN", (WalletSelection.HIDDEN, WalletSelection,), {}) # type: ignore +WalletSelection.ON_DEVICE = type("WalletSelection.ON_DEVICE", (WalletSelection.ON_DEVICE, WalletSelection,), {}) # type: ignore + + + + +class _UniffiConverterTypeWalletSelection(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return WalletSelection.STANDARD( + ) + if variant == 2: + return WalletSelection.HIDDEN( + _UniffiConverterString.read(buf), + ) + if variant == 3: + return WalletSelection.ON_DEVICE( + ) + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value.is_STANDARD(): + return + if value.is_HIDDEN(): + _UniffiConverterString.check_lower(value.passphrase) + return + if value.is_ON_DEVICE(): + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value.is_STANDARD(): + buf.write_i32(1) + if value.is_HIDDEN(): + buf.write_i32(2) + _UniffiConverterString.write(value.passphrase, buf) + if value.is_ON_DEVICE(): + buf.write_i32(3) + + + + + + + class WordCount(enum.Enum): WORDS12 = 12 """ @@ -16983,12 +17313,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 +17328,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 device is asking for the passphrase to be + entered on the Trezor itself — return `PassphraseResponse::OnDevice`. - 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 + `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + `OnDevice` (defer entry to the Trezor), or `Cancel`. """ raise NotImplementedError @@ -17023,12 +17348,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 +17363,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 device is asking for the passphrase to be + entered on the Trezor itself — return `PassphraseResponse::OnDevice`. - 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 + `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + `OnDevice` (defer entry to the Trezor), or `Cancel`. """ raise NotImplementedError @@ -17059,12 +17379,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 +17421,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 device is asking for the passphrase to be + entered on the Trezor itself — return `PassphraseResponse::OnDevice`. - 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 + `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + `OnDevice` (defer entry to the Trezor), or `Cancel`. """ _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 +17484,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, @@ -18857,20 +19172,28 @@ async def trezor_clear_credentials(device_id: "str") -> None: _UniffiConverterTypeTrezorError, ) -async def trezor_connect(device_id: "str") -> "TrezorFeatures": +async def trezor_connect(device_id: "str",selection: "WalletSelection") -> "TrezorFeatures": """ Connect to a Trezor device by its ID. For Bluetooth devices, this will use stored credentials if available, or trigger pairing if needed. + + `selection` chooses which wallet to open. On THP devices (Safe 5/7) it is + bound to the session at creation, so it must be supplied on every connect; + there is no separate "set passphrase" step and nothing is cached between + calls. Reconnect with a different `selection` to switch wallets. """ _UniffiConverterString.check_lower(device_id) + _UniffiConverterTypeWalletSelection.check_lower(selection) + return await _uniffi_rust_call_async( _UniffiLib.uniffi_bitkitcore_fn_func_trezor_connect( - _UniffiConverterString.lower(device_id)), + _UniffiConverterString.lower(device_id), + _UniffiConverterTypeWalletSelection.lower(selection)), _UniffiLib.ffi_bitkitcore_rust_future_poll_rust_buffer, _UniffiLib.ffi_bitkitcore_rust_future_complete_rust_buffer, _UniffiLib.ffi_bitkitcore_rust_future_free_rust_buffer, @@ -19454,6 +19777,7 @@ def wipe_all_transaction_details() -> None: "ManualRefundStateEnum", "Network", "NetworkType", + "PassphraseResponse", "PaymentState", "PaymentType", "PubkyAuthKind", @@ -19466,6 +19790,7 @@ def wipe_all_transaction_details() -> None: "TrezorScriptType", "TrezorTransportType", "TxDirection", + "WalletSelection", "WordCount", "AccountAddresses", "AccountInfoResult", diff --git a/bindings/python/bitkitcore/libbitkitcore.dylib b/bindings/python/bitkitcore/libbitkitcore.dylib index e62b2cd..72447a1 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..71dcd49 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, WalletSelection, }; use crate::modules::trezor::{ TrezorAddressResponse, TrezorCoinType, TrezorDeviceInfo, TrezorError, TrezorFeatures, @@ -2178,10 +2179,18 @@ pub async fn trezor_list_devices() -> Result, TrezorError> /// /// For Bluetooth devices, this will use stored credentials if available, /// or trigger pairing if needed. +/// +/// `selection` chooses which wallet to open. On THP devices (Safe 5/7) it is +/// bound to the session at creation, so it must be supplied on every connect; +/// there is no separate "set passphrase" step and nothing is cached between +/// calls. Reconnect with a different `selection` to switch wallets. #[uniffi::export] -pub async fn trezor_connect(device_id: String) -> Result { +pub async fn trezor_connect( + device_id: String, + selection: WalletSelection, +) -> Result { let rt = ensure_runtime(); - rt.spawn(async move { get_trezor_manager().connect(&device_id).await }) + rt.spawn(async move { get_trezor_manager().connect(&device_id, selection).await }) .await .unwrap_or_else(|e| { Err(TrezorError::IoError { diff --git a/src/modules/trezor/callbacks.rs b/src/modules/trezor/callbacks.rs index c3315b8..b0fa1d4 100644 --- a/src/modules/trezor/callbacks.rs +++ b/src/modules/trezor/callbacks.rs @@ -172,16 +172,34 @@ 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 passphrase entered on the host. + Hidden { value: String }, + /// Enter the passphrase on the Trezor device itself instead of on the host. + OnDevice, +} + /// 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 +210,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 device is asking for the passphrase to be + /// entered on the Trezor itself — return `PassphraseResponse::OnDevice`. /// - /// 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 + /// `Standard` (no passphrase), `Hidden { value }` (host-entered passphrase), + /// `OnDevice` (defer entry to the Trezor), or `Cancel`. + fn on_passphrase_request(&self, on_device: bool) -> PassphraseResponse; } // ============================================================================ @@ -241,6 +260,53 @@ pub fn get_ui_callback() -> Option<&'static Arc> { UI_CALLBACK.get() } +/// Which wallet a connection should open. +/// +/// Passed to `trezor_connect` and consumed at connect time — the passphrase is +/// a one-shot input, not retained anywhere afterwards. On THP devices (Safe +/// 5/7) it is bound to the session at `ThpCreateNewSession`; on legacy devices +/// the mid-operation `PassphraseRequest` is answered from the UI callback +/// instead (see [`TrezorUiCallback`]). +#[derive(Debug, Clone, uniffi::Enum)] +pub enum WalletSelection { + /// The standard wallet — no passphrase. + Standard, + /// A hidden wallet whose passphrase is entered on the host. + Hidden { passphrase: String }, + /// A hidden wallet whose passphrase is entered on the Trezor itself. + OnDevice, +} + +impl Default for WalletSelection { + fn default() -> Self { + WalletSelection::Standard + } +} + +// Only the callback (mobile) transport binds the passphrase at connect time; +// the desktop path supplies it via the UI callback instead. +#[cfg(any(target_os = "android", target_os = "ios"))] +use zeroize::Zeroizing; + +#[cfg(any(target_os = "android", target_os = "ios"))] +impl WalletSelection { + /// Lower the selection to the `(passphrase, on_device)` pair the THP + /// transport binds at session creation. `OnDevice` carries no host + /// passphrase: the transport omits the field so the Trezor prompts on its + /// own screen (firmware rejects an empty passphrase combined with + /// `on_device = true`). + /// + /// The passphrase is moved into a `Zeroizing` so this crate's copy + /// is wiped from memory on drop instead of lingering in a freed allocation. + pub(crate) fn into_session_passphrase(self) -> (Zeroizing, bool) { + match self { + WalletSelection::Standard => (Zeroizing::new(String::new()), false), + WalletSelection::Hidden { passphrase } => (Zeroizing::new(passphrase), false), + WalletSelection::OnDevice => (Zeroizing::new(String::new()), true), + } + } +} + // ============================================================================ // BLE initialization // ============================================================================ diff --git a/src/modules/trezor/errors.rs b/src/modules/trezor/errors.rs index b6336da..c388cb5 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, @@ -164,6 +168,9 @@ impl From for TrezorError { ), } } + _ => TrezorError::ProtocolError { + error_details: protocol_err.to_string(), + }, }, // Device errors @@ -174,6 +181,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(), }, @@ -201,6 +209,16 @@ impl From for TrezorError { TcDeviceError::InvalidInput(msg) => TrezorError::DeviceError { error_details: format!("Invalid input: {}", msg), }, + // Wrong passphrase for a remembered wallet (static-session-id + // mismatch). Only produced if the caller opts into + // ConnectedDevice::verify_session_state, which bitkit-core does + // not currently call — mapped here to keep the match exhaustive. + TcDeviceError::InvalidState => TrezorError::DeviceError { + error_details: "Passphrase is incorrect (device state mismatch)".to_string(), + }, + _ => TrezorError::DeviceError { + error_details: device_err.to_string(), + }, }, // THP (Trezor Host Protocol) errors @@ -234,6 +252,9 @@ impl From for TrezorError { ThpError::SessionError(msg) => TrezorError::SessionError { error_details: format!("THP session error: {}", msg), }, + _ => TrezorError::ConnectionError { + error_details: format!("THP error: {}", thp_err), + }, }, // Session errors @@ -250,6 +271,9 @@ impl From for TrezorError { TcSessionError::Expired => TrezorError::SessionError { error_details: "Session expired".to_string(), }, + _ => TrezorError::SessionError { + error_details: session_err.to_string(), + }, }, // Bitcoin errors @@ -273,6 +297,15 @@ impl From for TrezorError { expected, actual ), }, + _ => TrezorError::DeviceError { + error_details: bitcoin_err.to_string(), + }, + }, + + // The upstream error enums are `#[non_exhaustive]`, so any future + // variant we don't explicitly map falls through to a generic error. + _ => TrezorError::DeviceError { + error_details: err.to_string(), }, } } diff --git a/src/modules/trezor/implementation.rs b/src/modules/trezor/implementation.rs index e078d0f..5d062b4 100644 --- a/src/modules/trezor/implementation.rs +++ b/src/modules/trezor/implementation.rs @@ -11,7 +11,7 @@ use crate::modules::trezor::{ TrezorAddressResponse, TrezorCoinType, TrezorDeviceInfo, TrezorError, TrezorFeatures, TrezorGetAddressParams, TrezorGetPublicKeyParams, TrezorPublicKeyResponse, TrezorSignMessageParams, TrezorSignTxParams, TrezorSignedMessageResponse, TrezorSignedTx, - TrezorTransportType, TrezorVerifyMessageParams, + TrezorTransportType, TrezorVerifyMessageParams, WalletSelection, }; // Desktop: use full trezor-connect-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) } } @@ -539,11 +539,18 @@ impl TrezorManager { } } - /// Connect to a device by its ID. + /// Connect to a device by its ID, opening the wallet given by `selection`. /// - /// On mobile: Opens device connection via native callback. - /// On desktop: Uses trezor-connect-rs library. - pub async fn connect(&self, device_id: &str) -> Result { + /// On mobile: Opens device connection via native callback. For THP devices + /// the `selection` passphrase is bound to the session created during + /// `acquire()`. + /// On desktop: Uses trezor-connect-rs library; the passphrase is supplied + /// via the UI callback, so `selection` is not consumed on that path. + pub async fn connect( + &self, + device_id: &str, + selection: WalletSelection, + ) -> Result { if !*self.initialized.lock().await { return Err(TrezorError::NotInitialized); } @@ -569,8 +576,15 @@ impl TrezorManager { let adapter = Arc::new(CallbackAdapter { callback: callback.clone(), }); - let mut transport = - CallbackTransport::new(adapter).with_app_identity("Bitkit", "Bitkit"); + // Bind the selected wallet's passphrase to the THP session that + // acquire() creates. On THP devices the passphrase is set at session + // creation, not via a mid-operation prompt. We hold our copy in + // `Zeroizing` so it is wiped on drop (including error paths); the + // transport re-wraps the value it receives in `Zeroizing` as well. + let (session_passphrase, session_on_device) = selection.into_session_passphrase(); + let mut transport = CallbackTransport::new(adapter) + .with_app_identity("Bitkit", "Bitkit") + .with_session_passphrase(session_passphrase.to_string(), session_on_device); transport.init().await.map_err(TrezorError::from)?; // Acquire a session (this triggers THP handshake for BLE) @@ -638,6 +652,11 @@ impl TrezorManager { // Desktop: Use trezor-connect-rs #[cfg(not(any(target_os = "android", target_os = "ios")))] { + // Desktop binds the passphrase through the UI callback + // (on_passphrase_request), not at connect time, so the selection is + // not consumed on this path. + let _ = selection; + // Find the device in the cached list let device = { let device_list = self.device_list.lock().await; diff --git a/src/modules/trezor/tests.rs b/src/modules/trezor/tests.rs index f3deb44..a3801b7 100644 --- a/src/modules/trezor/tests.rs +++ b/src/modules/trezor/tests.rs @@ -651,4 +651,132 @@ 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_on_device_maps_to_upstream_on_device() { + // On-device entry: the app defers passphrase entry to the Trezor, which + // must reach the device as the `OnDevice` variant so the library acks + // with `on_device = true` instead of sending a host passphrase. + let mock = MockUiCallback::new("", PassphraseResponse::OnDevice); + let adapter = adapter_with(mock); + assert_eq!( + adapter.on_passphrase_request(true), + trezor_connect_rs::PassphraseResponse::OnDevice + ); + } + + #[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)); + } } diff --git a/src/modules/trezor/types.rs b/src/modules/trezor/types.rs index 1b86f47..4a9102c 100644 --- a/src/modules/trezor/types.rs +++ b/src/modules/trezor/types.rs @@ -76,8 +76,15 @@ pub struct TrezorFeatures { pub initialized: Option, /// Whether the device needs backup pub needs_backup: Option, + /// Whether the device can accept passphrase entry on the device itself + /// (`Capability_PassphraseEntry`). When false/None, use host entry only. + pub passphrase_entry_capable: Option, } +/// `Capability_PassphraseEntry` value from the Trezor management proto — the +/// device is capable of passphrase entry directly on its own screen. +const CAPABILITY_PASSPHRASE_ENTRY: u32 = 17; + impl From for TrezorFeatures { fn from(f: trezor_connect_rs::device::Features) -> Self { Self { @@ -92,6 +99,7 @@ impl From for TrezorFeatures { passphrase_protection: f.passphrase_protection, initialized: f.initialized, needs_backup: f.needs_backup, + passphrase_entry_capable: Some(f.capabilities.contains(&CAPABILITY_PASSPHRASE_ENTRY)), } } }