@@ -21,12 +21,14 @@ import com.synonym.bitkitcore.TrezorSignedMessageResponse
2121import com.synonym.bitkitcore.TrezorSignedTx
2222import com.synonym.bitkitcore.TrezorTransportType
2323import com.synonym.bitkitcore.WalletParams
24+ import com.synonym.bitkitcore.WalletSelection
2425import dagger.hilt.android.qualifiers.ApplicationContext
2526import kotlinx.collections.immutable.ImmutableList
2627import kotlinx.collections.immutable.persistentListOf
2728import kotlinx.collections.immutable.toImmutableList
2829import kotlinx.coroutines.CoroutineDispatcher
2930import kotlinx.coroutines.CoroutineScope
31+ import kotlinx.coroutines.delay
3032import kotlinx.coroutines.flow.MutableStateFlow
3133import kotlinx.coroutines.flow.asStateFlow
3234import kotlinx.coroutines.flow.launchIn
@@ -42,6 +44,8 @@ import to.bitkit.models.toCoreNetwork
4244import to.bitkit.services.TrezorDebugLog
4345import to.bitkit.services.TrezorService
4446import to.bitkit.services.TrezorTransport
47+ import to.bitkit.services.TrezorUiHandler
48+ import to.bitkit.services.TrezorWalletMode
4549import to.bitkit.utils.AppError
4650import to.bitkit.utils.Logger
4751import java.io.File
@@ -55,13 +59,15 @@ class TrezorRepo @Inject constructor(
5559 @ApplicationContext private val context : Context ,
5660 private val trezorService : TrezorService ,
5761 private val trezorTransport : TrezorTransport ,
62+ private val trezorUiHandler : TrezorUiHandler ,
5863 private val trezorStore : TrezorStore ,
5964 @IoDispatcher private val ioDispatcher : CoroutineDispatcher ,
6065) {
6166 companion object {
6267 private const val TAG = " TrezorRepo"
6368 private const val DEFAULT_ADDRESS_PATH = " m/84'/0'/0'/0/0"
6469 private const val DEFAULT_ACCOUNT_PATH = " m/84'/0'/0'"
70+ private const val WALLET_MODE_RECONNECT_DELAY_MS = 1_000L
6571 }
6672
6773 private val _state = MutableStateFlow (TrezorState ())
@@ -87,6 +93,64 @@ class TrezorRepo @Inject constructor(
8793 trezorTransport.cancelPairingCode()
8894 }
8995
96+ val needsPinEntry = trezorUiHandler.needsPinEntry
97+
98+ fun submitPin (pin : String ) {
99+ trezorUiHandler.submitPin(pin)
100+ }
101+
102+ fun cancelPin () {
103+ trezorUiHandler.cancelPin()
104+ }
105+
106+ val walletMode = trezorUiHandler.walletMode
107+
108+ /* *
109+ * Reset to the standard wallet and clear any selected passphrase, without
110+ * reconnecting. Call this when the user explicitly picks a device from a
111+ * list ([connect]/[connectKnownDevice]) so a passphrase or on-device
112+ * selection left over from a previously connected device isn't silently
113+ * applied to the newly selected one.
114+ *
115+ * Silent reconnects ([autoReconnect]/[ensureConnected]) deliberately skip
116+ * this, so a dropped link reopens the same hidden wallet the user was using.
117+ */
118+ fun resetWalletSelection () {
119+ trezorUiHandler.setWalletMode(TrezorWalletMode .STANDARD )
120+ }
121+
122+ /* *
123+ * Switch between the standard wallet and a passphrase (hidden) wallet.
124+ *
125+ * The Trezor caches the passphrase for the whole session, so switching
126+ * requires a fresh session: this sets the desired mode, then disconnects
127+ * and reconnects. The new mode takes effect on the next wallet operation.
128+ */
129+ suspend fun setWalletMode (
130+ mode : TrezorWalletMode ,
131+ passphrase : String = "",
132+ ): Result <TrezorFeatures > = withContext(ioDispatcher) {
133+ runCatching {
134+ val deviceId = _state .value.connectedDeviceId
135+ ? : throw AppError (" No connected Trezor" )
136+ TrezorDebugLog .log(" WALLET_MODE" , " Switching to $mode , resetting session for $deviceId " )
137+ // Reset the session via disconnect/reconnect. disconnect() resets the
138+ // UI handler's wallet mode to standard, so set the desired mode AFTER
139+ // the disconnect and right before reconnecting.
140+ runCatching { disconnect() }
141+ // Reconnect by id WITHOUT a scan: scan() clears the discovered-device
142+ // cache and a scan right after a disconnect usually finds nothing,
143+ // whereas the cached handle (and direct address resolution) still work.
144+ delay(WALLET_MODE_RECONNECT_DELAY_MS )
145+ // Record the selection on the handler: THP reads it via
146+ // currentSelection() to bind the passphrase at session creation,
147+ // while non-THP devices re-request it mid-operation and are answered
148+ // from the same value. connect() then derives the wallet from it.
149+ trezorUiHandler.setWalletMode(mode, passphrase)
150+ connect(deviceId).getOrThrow()
151+ }
152+ }
153+
90154 suspend fun initialize (walletIndex : Int = 0): Result <Unit > = withContext(ioDispatcher) {
91155 runCatching {
92156 val credentialPath = " ${Env .bitkitCoreStoragePath(walletIndex)} /trezor-credentials.json"
@@ -131,7 +195,7 @@ class TrezorRepo @Inject constructor(
131195 runCatching {
132196 _state .update { it.copy(isConnecting = true , error = null ) }
133197 TrezorDebugLog .log(" CONNECT" , " connect() called for deviceId=$deviceId " )
134- val features = connectWithThpRetry(deviceId)
198+ val features = connectWithThpRetry(deviceId, trezorUiHandler.currentSelection() )
135199 TrezorDebugLog .log(" CONNECT" , " connect() succeeded: label=${features.label} , model=${features.model} " )
136200 val deviceInfo = _state .value.nearbyDevices.find { it.id == deviceId }
137201 ? : _state .value.knownDevices.find { it.id == deviceId }?.let { known ->
@@ -319,6 +383,14 @@ class TrezorRepo @Inject constructor(
319383 suspend fun disconnect (): Result <Unit > = withContext(ioDispatcher) {
320384 TrezorDebugLog .log(" DISCONNECT" , " disconnect() called, connectedDeviceId=${_state .value.connectedDeviceId} " )
321385 val result = runCatching { trezorService.disconnect() }
386+ // Mirror the core: trezorService.disconnect() resets the session
387+ // passphrase to the standard wallet, so reset the UI handler's wallet
388+ // mode too. This keeps the THP path, the legacy PassphraseRequest
389+ // callback, and the displayed mode consistent on the next (re)connect —
390+ // a hidden wallet must be re-selected explicitly after an explicit
391+ // disconnect. (A transient external disconnect does not call this and so
392+ // retains the selection, matching the core's behaviour.)
393+ trezorUiHandler.setWalletMode(TrezorWalletMode .STANDARD )
322394 _state .update {
323395 it.copy(connected = null , lastAddress = null , lastPublicKey = null )
324396 }
@@ -428,20 +500,13 @@ class TrezorRepo @Inject constructor(
428500 " RECONNECT" ,
429501 " Scan found ${scannedDevices.size} devices: ${scannedDevices.map { it.id }} " ,
430502 )
431- val exactMatch = scannedDevices.find { it.id == deviceId }
432- val knownIds = _state .value.knownDevices.map { it.id }.toSet()
433- val usbDevice = scannedDevices.find {
434- it.transportType == TrezorTransportType .USB && it.id in knownIds
435- }
436- val device = if (exactMatch?.transportType == TrezorTransportType .BLUETOOTH && usbDevice != null ) {
437- TrezorDebugLog .log(" RECONNECT" , " Preferring USB over BLE" )
438- usbDevice
439- } else {
440- exactMatch ? : throw AppError (" Device not found nearby — is it powered on?" )
441- }
503+ // Honor the transport the user selected — connect to exactly the
504+ // entry they tapped instead of overriding Bluetooth with USB.
505+ val device = scannedDevices.find { it.id == deviceId }
506+ ? : throw AppError (" Device not found nearby — is it powered on?" )
442507 TrezorDebugLog .log(" RECONNECT" , " Found matching device: id=${device.id} , name=${device.name} " )
443508 TrezorDebugLog .log(" RECONNECT" , " Calling connectWithThpRetry..." )
444- val features = connectWithThpRetry(device.id)
509+ val features = connectWithThpRetry(device.id, trezorUiHandler.currentSelection() )
445510 TrezorDebugLog .log(" RECONNECT" , " Connected! label=${features.label} , model=${features.model} " )
446511 addOrUpdateKnownDevice(device, features)
447512 _state .update {
@@ -461,6 +526,9 @@ class TrezorRepo @Inject constructor(
461526 TrezorDebugLog .log(" FORGET" , " forgetDevice called for: $deviceId " )
462527 val disconnectResult = if (_state .value.connectedDeviceId == deviceId) {
463528 runCatching { trezorService.disconnect() }.also {
529+ // Clear any cached host passphrase so it can't be reused
530+ // against a different device on a later connect.
531+ trezorUiHandler.setWalletMode(TrezorWalletMode .STANDARD )
464532 _state .update { it.copy(connected = null ) }
465533 }
466534 } else {
@@ -541,7 +609,7 @@ class TrezorRepo @Inject constructor(
541609 val devices = trezorService.scan()
542610 val device = devices.find { it.id == deviceId }
543611 ? : throw AppError (" Device not found during reconnect" )
544- val features = connectWithThpRetry(device.id)
612+ val features = connectWithThpRetry(device.id, trezorUiHandler.currentSelection() )
545613 _state .update { it.copy(connected = ConnectedTrezorDevice (id = deviceId, features = features)) }
546614 }
547615
@@ -555,11 +623,14 @@ class TrezorRepo @Inject constructor(
555623 }
556624 }
557625
558- private suspend fun connectWithThpRetry (deviceId : String ): TrezorFeatures {
626+ private suspend fun connectWithThpRetry (
627+ deviceId : String ,
628+ selection : WalletSelection ,
629+ ): TrezorFeatures {
559630 TrezorDebugLog .log(" THPRetry" , " First connect attempt for: $deviceId " )
560631 logCredentialFileState(deviceId, " BEFORE 1st attempt" )
561632 return runCatching {
562- trezorService.connect(deviceId)
633+ trezorService.connect(deviceId, selection )
563634 }.onSuccess {
564635 logCredentialFileState(deviceId, " AFTER 1st attempt (success)" )
565636 TrezorDebugLog .log(" THPRetry" , " First attempt succeeded" )
@@ -573,7 +644,7 @@ class TrezorRepo @Inject constructor(
573644 TrezorDebugLog .log(" THPRetry" , " Error is retryable, attempting second connect..." )
574645 Logger .warn(" Connection failed for $deviceId , retrying" , e, context = TAG )
575646 logCredentialFileState(deviceId, " BEFORE 2nd attempt" )
576- val result = trezorService.connect(deviceId)
647+ val result = trezorService.connect(deviceId, selection )
577648 logCredentialFileState(deviceId, " AFTER 2nd attempt (success)" )
578649 TrezorDebugLog .log(" THPRetry" , " Second attempt succeeded" )
579650 result
@@ -591,6 +662,11 @@ class TrezorRepo @Inject constructor(
591662
592663 private fun isRetryableError (e : Throwable ): Boolean {
593664 val msg = e.message?.lowercase() ? : return false
665+ // A rejected session (wrong passphrase, or the user cancelling on-device
666+ // passphrase entry) is a definitive failure, not a transient THP/transport
667+ // hiccup. Retrying it just re-prompts the device and risks wedging the
668+ // connection, so don't treat ThpCreateNewSession rejections as retryable.
669+ if (" rejected" in msg) return false
594670 return " thp" in msg || " session" in msg || " timeout" in msg || " disconnect" in msg
595671 }
596672}
0 commit comments