11package to.bitkit.repositories
22
33import android.content.Context
4+ import androidx.compose.runtime.Immutable
45import androidx.compose.runtime.Stable
56import com.synonym.bitkitcore.AccountInfoResult
67import com.synonym.bitkitcore.AccountType
@@ -21,6 +22,9 @@ import com.synonym.bitkitcore.TrezorSignedTx
2122import com.synonym.bitkitcore.TrezorTransportType
2223import com.synonym.bitkitcore.WalletParams
2324import dagger.hilt.android.qualifiers.ApplicationContext
25+ import kotlinx.collections.immutable.ImmutableList
26+ import kotlinx.collections.immutable.persistentListOf
27+ import kotlinx.collections.immutable.toImmutableList
2428import kotlinx.coroutines.CoroutineDispatcher
2529import kotlinx.coroutines.CoroutineScope
2630import kotlinx.coroutines.flow.MutableStateFlow
@@ -29,6 +33,7 @@ import kotlinx.coroutines.flow.launchIn
2933import kotlinx.coroutines.flow.onEach
3034import kotlinx.coroutines.flow.update
3135import kotlinx.coroutines.withContext
36+ import kotlinx.serialization.SerialName
3237import kotlinx.serialization.Serializable
3338import to.bitkit.data.TrezorStore
3439import to.bitkit.di.IoDispatcher
@@ -88,7 +93,7 @@ class TrezorRepo @Inject constructor(
8893 Logger .debug(" Initializing Trezor with credential path: '$credentialPath '" , context = TAG )
8994 trezorService.initialize(credentialPath)
9095 val known = loadKnownDevices()
91- _state .update { it.copy(isInitialized = true , knownDevices = known, error = null ) }
96+ _state .update { it.copy(isInitialized = true , knownDevices = known.toImmutableList() , error = null ) }
9297 }.onFailure { e ->
9398 Logger .error(" Trezor init failed" , e, context = TAG )
9499 _state .update { it.copy(error = e.message) }
@@ -101,7 +106,7 @@ class TrezorRepo @Inject constructor(
101106 val devices = trezorService.scan()
102107 val knownIds = _state .value.knownDevices.map { it.id }.toSet()
103108 val nearby = devices.filter { it.id !in knownIds }
104- _state .update { it.copy(isScanning = false , nearbyDevices = nearby) }
109+ _state .update { it.copy(isScanning = false , nearbyDevices = nearby.toImmutableList() ) }
105110 devices
106111 }.onFailure { e ->
107112 Logger .error(" Trezor scan failed" , e, context = TAG )
@@ -114,7 +119,7 @@ class TrezorRepo @Inject constructor(
114119 val devices = trezorService.listDevices()
115120 val knownIds = _state .value.knownDevices.map { it.id }.toSet()
116121 val nearby = devices.filter { it.id !in knownIds }
117- _state .update { it.copy(nearbyDevices = nearby) }
122+ _state .update { it.copy(nearbyDevices = nearby.toImmutableList() ) }
118123 devices
119124 }.onFailure { e ->
120125 Logger .error(" Trezor listDevices failed" , e, context = TAG )
@@ -132,10 +137,7 @@ class TrezorRepo @Inject constructor(
132137 ? : _state .value.knownDevices.find { it.id == deviceId }?.let { known ->
133138 TrezorDeviceInfo (
134139 id = known.id,
135- transportType = when (known.transportType) {
136- " bluetooth" -> TrezorTransportType .BLUETOOTH
137- else -> TrezorTransportType .USB
138- },
140+ transportType = known.transportType.toCoreTransportType(),
139141 name = known.name,
140142 path = known.path,
141143 label = known.label,
@@ -149,9 +151,8 @@ class TrezorRepo @Inject constructor(
149151 _state .update {
150152 it.copy(
151153 isConnecting = false ,
152- connectedDevice = features,
153- connectedDeviceId = deviceId,
154- nearbyDevices = it.nearbyDevices.filter { d -> d.id != deviceId },
154+ connected = ConnectedTrezorDevice (id = deviceId, features = features),
155+ nearbyDevices = it.nearbyDevices.filter { d -> d.id != deviceId }.toImmutableList(),
155156 )
156157 }
157158 features
@@ -316,12 +317,12 @@ class TrezorRepo @Inject constructor(
316317 }
317318
318319 suspend fun disconnect (): Result <Unit > = withContext(ioDispatcher) {
319- runCatching {
320- TrezorDebugLog .log( " DISCONNECT " , " disconnect() called, connectedDeviceId= ${ _state .value.connectedDeviceId} " )
321- runCatching { trezorService.disconnect() }
322- _state .update {
323- it.copy(connectedDevice = null , connectedDeviceId = null , lastAddress = null , lastPublicKey = null )
324- }
320+ TrezorDebugLog .log( " DISCONNECT " , " disconnect() called, connectedDeviceId= ${ _state .value.connectedDeviceId} " )
321+ val result = runCatching { trezorService. disconnect() }
322+ _state .update {
323+ it.copy(connected = null , lastAddress = null , lastPublicKey = null )
324+ }
325+ result.onSuccess {
325326 TrezorDebugLog .log(" DISCONNECT" , " disconnect() complete (credentials NOT cleared)" )
326327 }.onFailure { e ->
327328 TrezorDebugLog .log(" DISCONNECT" , " FAILED: ${e.message} " )
@@ -386,7 +387,7 @@ class TrezorRepo @Inject constructor(
386387 initialize(walletIndex).getOrThrow()
387388 }
388389 if (trezorService.isConnected()) {
389- _state .value.connectedDevice ? : error (" Connected but no features" )
390+ _state .value.connectedDevice ? : throw AppError (" Connected but no features" )
390391 } else {
391392 val scannedDevices = scan().getOrThrow()
392393 val knownIds = knownDevices.map { it.id }.toSet()
@@ -396,7 +397,7 @@ class TrezorRepo @Inject constructor(
396397 val idMatch = knownDevices.firstNotNullOfOrNull { known ->
397398 scannedDevices.find { it.id == known.id }
398399 }
399- val match = idMatch ? : usbDevice ? : error (" No known device found nearby" )
400+ val match = idMatch ? : usbDevice ? : throw AppError (" No known device found nearby" )
400401 connect(match.id).getOrThrow()
401402 }
402403 }.onSuccess {
@@ -436,15 +437,15 @@ class TrezorRepo @Inject constructor(
436437 TrezorDebugLog .log(" RECONNECT" , " Preferring USB over BLE" )
437438 usbDevice
438439 } else {
439- exactMatch ? : error (" Device not found nearby — is it powered on?" )
440+ exactMatch ? : throw AppError (" Device not found nearby — is it powered on?" )
440441 }
441442 TrezorDebugLog .log(" RECONNECT" , " Found matching device: id=${device.id} , name=${device.name} " )
442443 TrezorDebugLog .log(" RECONNECT" , " Calling connectWithThpRetry..." )
443444 val features = connectWithThpRetry(device.id)
444445 TrezorDebugLog .log(" RECONNECT" , " Connected! label=${features.label} , model=${features.model} " )
445446 addOrUpdateKnownDevice(device, features)
446447 _state .update {
447- it.copy(isConnecting = false , connectedDevice = features, connectedDeviceId = device.id)
448+ it.copy(isConnecting = false , connected = ConnectedTrezorDevice (id = device.id, features = features) )
448449 }
449450 TrezorDebugLog .log(" RECONNECT" , " === connectKnownDevice SUCCESS ===" )
450451 features
@@ -458,16 +459,21 @@ class TrezorRepo @Inject constructor(
458459 suspend fun forgetDevice (deviceId : String ): Result <Unit > = withContext(ioDispatcher) {
459460 runCatching {
460461 TrezorDebugLog .log(" FORGET" , " forgetDevice called for: $deviceId " )
461- if (_state .value.connectedDeviceId == deviceId) {
462- runCatching { trezorService.disconnect() }
463- _state .update { it.copy(connectedDevice = null , connectedDeviceId = null ) }
462+ val disconnectResult = if (_state .value.connectedDeviceId == deviceId) {
463+ runCatching { trezorService.disconnect() }.also {
464+ _state .update { it.copy(connected = null ) }
465+ }
466+ } else {
467+ Result .success(Unit )
464468 }
465469 TrezorDebugLog .log(" FORGET" , " Clearing credentials..." )
466470 trezorTransport.clearDeviceCredential(deviceId)
467- runCatching { trezorService.clearCredentials(deviceId) }
471+ val clearCredentialsResult = runCatching { trezorService.clearCredentials(deviceId) }
468472 val updated = _state .value.knownDevices.filter { it.id != deviceId }
469473 saveKnownDevices(updated)
470- _state .update { it.copy(knownDevices = updated) }
474+ _state .update { it.copy(knownDevices = updated.toImmutableList()) }
475+ disconnectResult.getOrThrow()
476+ clearCredentialsResult.getOrThrow()
471477 TrezorDebugLog .log(" FORGET" , " Device forgotten successfully" )
472478 Logger .info(" Forgot device: '$deviceId '" , context = TAG )
473479 }.onFailure { e ->
@@ -488,7 +494,7 @@ class TrezorRepo @Inject constructor(
488494 if (knownDevice?.id == currentId || path.contains(currentId)) {
489495 Logger .warn(" External disconnect detected for '$currentId '" , context = TAG )
490496 _state .update {
491- it.copy(connectedDevice = null , connectedDeviceId = null , error = " Device disconnected" )
497+ it.copy(connected = null , error = " Device disconnected" )
492498 }
493499 }
494500 }.launchIn(scope)
@@ -500,17 +506,14 @@ class TrezorRepo @Inject constructor(
500506 id = deviceInfo.id,
501507 name = deviceInfo.name,
502508 path = deviceInfo.path,
503- transportType = when (deviceInfo.transportType) {
504- TrezorTransportType .BLUETOOTH -> " bluetooth"
505- TrezorTransportType .USB -> " usb"
506- },
509+ transportType = deviceInfo.transportType.toKnownTransportType(),
507510 label = features.label ? : deviceInfo.label,
508511 model = features.model ? : deviceInfo.model,
509512 lastConnectedAt = System .currentTimeMillis(),
510513 )
511514 val updated = existing.filter { it.id != known.id } + known
512515 saveKnownDevices(updated)
513- _state .update { it.copy(knownDevices = updated) }
516+ _state .update { it.copy(knownDevices = updated.toImmutableList() ) }
514517 }
515518
516519 private suspend fun loadKnownDevices (): List <KnownDevice > = runCatching {
@@ -531,15 +534,15 @@ class TrezorRepo @Inject constructor(
531534 if (trezorService.isConnected()) return
532535 val deviceId = _state .value.connectedDeviceId
533536 ? : _state .value.knownDevices.firstOrNull()?.id
534- ? : error (" No device to reconnect" )
537+ ? : throw AppError (" No device to reconnect" )
535538 if (! _state .value.isInitialized) {
536539 initialize().getOrThrow()
537540 }
538541 val devices = trezorService.scan()
539542 val device = devices.find { it.id == deviceId }
540- ? : error (" Device not found during reconnect" )
543+ ? : throw AppError (" Device not found during reconnect" )
541544 val features = connectWithThpRetry(device.id)
542- _state .update { it.copy(connectedDevice = features, connectedDeviceId = deviceId ) }
545+ _state .update { it.copy(connected = ConnectedTrezorDevice (id = deviceId, features = features) ) }
543546 }
544547
545548 suspend fun clearCredentials (deviceId : String ): Result <Unit > = withContext(ioDispatcher) {
@@ -598,22 +601,53 @@ data class TrezorState(
598601 val isScanning : Boolean = false ,
599602 val isConnecting : Boolean = false ,
600603 val isAutoReconnecting : Boolean = false ,
601- val knownDevices : List <KnownDevice > = emptyList(),
602- val nearbyDevices : List <TrezorDeviceInfo > = emptyList(),
603- val connectedDevice : TrezorFeatures ? = null ,
604- val connectedDeviceId : String? = null ,
604+ val knownDevices : ImmutableList <KnownDevice > = persistentListOf(),
605+ val nearbyDevices : ImmutableList <TrezorDeviceInfo > = persistentListOf(),
606+ val connected : ConnectedTrezorDevice ? = null ,
605607 val lastAddress : TrezorAddressResponse ? = null ,
606608 val lastPublicKey : TrezorPublicKeyResponse ? = null ,
607609 val error : String? = null ,
610+ ) {
611+ val connectedDevice: TrezorFeatures ?
612+ get() = connected?.features
613+
614+ val connectedDeviceId: String?
615+ get() = connected?.id
616+ }
617+
618+ @Stable
619+ data class ConnectedTrezorDevice (
620+ val id : String ,
621+ val features : TrezorFeatures ,
608622)
609623
610624@Serializable
625+ @Immutable
611626data class KnownDevice (
612627 val id : String ,
613628 val name : String? ,
614629 val path : String ,
615- val transportType : String ,
630+ val transportType : KnownDeviceTransportType ,
616631 val label : String? ,
617632 val model : String? ,
618633 val lastConnectedAt : Long ,
619634)
635+
636+ @Serializable
637+ enum class KnownDeviceTransportType {
638+ @SerialName(" bluetooth" )
639+ BLUETOOTH ,
640+
641+ @SerialName(" usb" )
642+ USB ,
643+ }
644+
645+ private fun TrezorTransportType.toKnownTransportType (): KnownDeviceTransportType = when (this ) {
646+ TrezorTransportType .BLUETOOTH -> KnownDeviceTransportType .BLUETOOTH
647+ TrezorTransportType .USB -> KnownDeviceTransportType .USB
648+ }
649+
650+ private fun KnownDeviceTransportType.toCoreTransportType (): TrezorTransportType = when (this ) {
651+ KnownDeviceTransportType .BLUETOOTH -> TrezorTransportType .BLUETOOTH
652+ KnownDeviceTransportType .USB -> TrezorTransportType .USB
653+ }
0 commit comments