Skip to content

Commit 7116e00

Browse files
authored
Merge pull request #939 from synonymdev/feat/trezor-hardware-support-fixes
chore: improve trezor dashboard
2 parents f1b93c8 + 9505e25 commit 7116e00

18 files changed

Lines changed: 923 additions & 291 deletions

app/src/main/java/to/bitkit/data/TrezorStore.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import android.content.Context
44
import androidx.datastore.core.DataStore
55
import androidx.datastore.dataStore
66
import dagger.hilt.android.qualifiers.ApplicationContext
7+
import kotlinx.coroutines.CoroutineDispatcher
78
import kotlinx.coroutines.flow.Flow
89
import kotlinx.coroutines.flow.first
10+
import kotlinx.coroutines.withContext
911
import kotlinx.serialization.Serializable
1012
import to.bitkit.data.serializers.TrezorDataSerializer
13+
import to.bitkit.di.IoDispatcher
1114
import to.bitkit.repositories.KnownDevice
1215
import javax.inject.Inject
1316
import javax.inject.Singleton
@@ -20,20 +23,24 @@ private val Context.trezorDataStore: DataStore<TrezorData> by dataStore(
2023
@Singleton
2124
class TrezorStore @Inject constructor(
2225
@ApplicationContext private val context: Context,
26+
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
2327
) {
2428
private val store = context.trezorDataStore
2529

2630
val data: Flow<TrezorData> = store.data
2731

28-
suspend fun loadKnownDevices(): List<KnownDevice> =
32+
suspend fun loadKnownDevices(): List<KnownDevice> = withContext(ioDispatcher) {
2933
store.data.first().knownDevices
34+
}
3035

31-
suspend fun saveKnownDevices(devices: List<KnownDevice>) {
36+
suspend fun saveKnownDevices(devices: List<KnownDevice>) = withContext(ioDispatcher) {
3237
store.updateData { it.copy(knownDevices = devices) }
38+
Unit
3339
}
3440

35-
suspend fun reset() {
41+
suspend fun reset() = withContext(ioDispatcher) {
3642
store.updateData { TrezorData() }
43+
Unit
3744
}
3845
}
3946

app/src/main/java/to/bitkit/data/serializers/TrezorDataSerializer.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import java.io.InputStream
99
import java.io.OutputStream
1010

1111
object TrezorDataSerializer : Serializer<TrezorData> {
12+
private const val TAG = "TrezorDataSerializer"
13+
1214
override val defaultValue: TrezorData = TrezorData()
1315

1416
override suspend fun readFrom(input: InputStream): TrezorData {
1517
return try {
1618
json.decodeFromString(input.readBytes().decodeToString())
1719
} catch (e: SerializationException) {
18-
Logger.error("Failed to deserialize: $e")
20+
Logger.error("Deserialize Trezor data failed", e, context = TAG)
1921
defaultValue
2022
}
2123
}

app/src/main/java/to/bitkit/repositories/TrezorRepo.kt

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package to.bitkit.repositories
22

33
import android.content.Context
4+
import androidx.compose.runtime.Immutable
45
import androidx.compose.runtime.Stable
56
import com.synonym.bitkitcore.AccountInfoResult
67
import com.synonym.bitkitcore.AccountType
@@ -21,6 +22,9 @@ import com.synonym.bitkitcore.TrezorSignedTx
2122
import com.synonym.bitkitcore.TrezorTransportType
2223
import com.synonym.bitkitcore.WalletParams
2324
import dagger.hilt.android.qualifiers.ApplicationContext
25+
import kotlinx.collections.immutable.ImmutableList
26+
import kotlinx.collections.immutable.persistentListOf
27+
import kotlinx.collections.immutable.toImmutableList
2428
import kotlinx.coroutines.CoroutineDispatcher
2529
import kotlinx.coroutines.CoroutineScope
2630
import kotlinx.coroutines.flow.MutableStateFlow
@@ -29,6 +33,7 @@ import kotlinx.coroutines.flow.launchIn
2933
import kotlinx.coroutines.flow.onEach
3034
import kotlinx.coroutines.flow.update
3135
import kotlinx.coroutines.withContext
36+
import kotlinx.serialization.SerialName
3237
import kotlinx.serialization.Serializable
3338
import to.bitkit.data.TrezorStore
3439
import 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
611626
data 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+
}
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package to.bitkit.services
22

3+
import kotlinx.collections.immutable.ImmutableList
4+
import kotlinx.collections.immutable.persistentListOf
5+
import kotlinx.collections.immutable.toImmutableList
36
import kotlinx.coroutines.flow.MutableStateFlow
47
import kotlinx.coroutines.flow.StateFlow
58
import kotlinx.coroutines.flow.asStateFlow
@@ -10,8 +13,8 @@ import java.util.Locale
1013

1114
object TrezorDebugLog {
1215
private const val MAX_LINES = 300
13-
private val _lines = MutableStateFlow<List<String>>(emptyList())
14-
val lines: StateFlow<List<String>> = _lines.asStateFlow()
16+
private val _lines = MutableStateFlow<ImmutableList<String>>(persistentListOf())
17+
val lines: StateFlow<ImmutableList<String>> = _lines.asStateFlow()
1518

1619
private val fmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
1720

@@ -20,11 +23,15 @@ object TrezorDebugLog {
2023
val line = "$ts [$tag] $msg"
2124
_lines.update { current ->
2225
val updated = current + line
23-
if (updated.size > MAX_LINES) updated.takeLast(MAX_LINES) else updated
26+
if (updated.size > MAX_LINES) {
27+
updated.takeLast(MAX_LINES).toImmutableList()
28+
} else {
29+
updated.toImmutableList()
30+
}
2431
}
2532
}
2633

2734
fun clear() {
28-
_lines.update { emptyList() }
35+
_lines.update { persistentListOf() }
2936
}
3037
}

0 commit comments

Comments
 (0)