Skip to content

Commit d8e2d80

Browse files
committed
Merge remote-tracking branch 'origin/feat/trezor-hardware-support' into feat/trezor-hardware-support
2 parents 2bbec26 + 3e02723 commit d8e2d80

8 files changed

Lines changed: 97 additions & 98 deletions

File tree

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

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -506,20 +506,7 @@ class TrezorRepo @Inject constructor(
506506
}.onFailure { Logger.error("Failed to save known devices", it, context = TAG) }
507507
}
508508

509-
private fun electrumUrlForNetwork(network: BitkitCoreNetwork): String = when (network) {
510-
BitkitCoreNetwork.BITCOIN -> "ssl://bitkit.to:9999"
511-
BitkitCoreNetwork.TESTNET -> "ssl://electrum.blockstream.info:60002"
512-
BitkitCoreNetwork.TESTNET4 -> "ssl://electrum.blockstream.info:60002"
513-
BitkitCoreNetwork.REGTEST -> "ssl://electrs.bitkit.stag0.blocktank.to:9999"
514-
BitkitCoreNetwork.SIGNET -> "ssl://electrum.blockstream.info:60002"
515-
}
516-
517-
private fun electrumUrlForNetwork(network: TrezorCoinType): String = when (network) {
518-
TrezorCoinType.BITCOIN -> Env.electrumUrlForNetwork(BitkitCoreNetwork.BITCOIN)
519-
TrezorCoinType.TESTNET -> Env.electrumUrlForNetwork(BitkitCoreNetwork.TESTNET)
520-
TrezorCoinType.REGTEST -> Env.electrumUrlForNetwork(BitkitCoreNetwork.REGTEST)
521-
TrezorCoinType.SIGNET -> Env.electrumUrlForNetwork(BitkitCoreNetwork.SIGNET)
522-
}
509+
private fun electrumUrlForNetwork(network: BitkitCoreNetwork): String = Env.electrumUrlForNetwork(network)
523510

524511
private suspend fun ensureConnected() {
525512
if (trezorService.isConnected()) return

app/src/main/java/to/bitkit/services/TrezorTransport.kt

Lines changed: 88 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ import android.hardware.usb.UsbDeviceConnection
2424
import android.hardware.usb.UsbEndpoint
2525
import android.hardware.usb.UsbInterface
2626
import android.hardware.usb.UsbManager
27+
import android.hardware.usb.UsbRequest
2728
import android.os.Handler
2829
import android.os.Looper
2930
import android.os.ParcelUuid
31+
import androidx.core.content.ContextCompat
32+
import androidx.core.content.edit
3033
import com.synonym.bitkitcore.NativeDeviceInfo
3134
import com.synonym.bitkitcore.TrezorCallMessageResult
3235
import com.synonym.bitkitcore.TrezorTransportCallback
@@ -42,6 +45,7 @@ import to.bitkit.ext.bluetoothManager
4245
import to.bitkit.ext.usbManager
4346
import to.bitkit.utils.Logger
4447
import java.io.File
48+
import java.nio.ByteBuffer
4549
import java.util.UUID
4650
import java.util.concurrent.ConcurrentHashMap
4751
import java.util.concurrent.CountDownLatch
@@ -141,7 +145,7 @@ class TrezorTransport @Inject constructor(
141145
migrated++
142146
}
143147
if (migrated > 0) {
144-
espPrefs.edit().clear().commit()
148+
espPrefs.edit(commit = true) { clear() }
145149
Logger.info("Migrated '$migrated' THP credentials from SharedPreferences to files", context = TAG)
146150
}
147151
} catch (e: Exception) {
@@ -182,36 +186,37 @@ class TrezorTransport @Inject constructor(
182186

183187
// ==================== TrezorTransportCallback Implementation ====================
184188

185-
@Suppress("TooGenericExceptionCaught")
186189
override fun enumerateDevices(): List<NativeDeviceInfo> {
187190
val devices = mutableListOf<NativeDeviceInfo>()
188191

189192
// Enumerate USB devices
190-
try {
191-
val usbDevices = usbManager.deviceList.values
193+
runCatching {
194+
usbManager.deviceList.values
192195
.filter { isTrezorDevice(it) }
193196
.map { device ->
194197
NativeDeviceInfo(
195198
path = device.deviceName,
196199
transportType = "usb",
197-
name = try { device.productName } catch (_: SecurityException) { null },
200+
name = runCatching { device.productName }.getOrNull(),
198201
vendorId = device.vendorId.toUShort(),
199202
productId = device.productId.toUShort(),
200203
)
201204
}
202-
devices.addAll(usbDevices)
203-
Logger.debug("USB enumerate found '${usbDevices.size}' Trezor device(s)", context = TAG)
204-
} catch (e: Exception) {
205-
Logger.error("USB enumerate failed", e, context = TAG)
205+
}.onSuccess {
206+
devices.addAll(it)
207+
Logger.debug("USB enumerate found '${it.size}' Trezor device(s)", context = TAG)
208+
}.onFailure {
209+
Logger.error("USB enumerate failed", it, context = TAG)
206210
}
207211

208212
// Enumerate Bluetooth devices
209-
try {
210-
val bleDevices = enumerateBleDevices()
211-
devices.addAll(bleDevices)
212-
Logger.debug("BLE enumerate found '${bleDevices.size}' Trezor device(s)", context = TAG)
213-
} catch (e: Exception) {
214-
Logger.error("BLE enumerate failed", e, context = TAG)
213+
runCatching {
214+
enumerateBleDevices()
215+
}.onSuccess {
216+
devices.addAll(it)
217+
Logger.debug("BLE enumerate found '${it.size}' Trezor device(s)", context = TAG)
218+
}.onFailure {
219+
Logger.error("BLE enumerate failed", it, context = TAG)
215220
}
216221

217222
Logger.info("Total enumerate found '${devices.size}' Trezor device(s)", context = TAG)
@@ -265,7 +270,7 @@ class TrezorTransport @Inject constructor(
265270
override fun callMessage(
266271
path: String,
267272
messageType: UShort,
268-
data: ByteArray
273+
data: ByteArray,
269274
): TrezorCallMessageResult? {
270275
// For BLE/THP devices, the Rust side now handles THP protocol directly.
271276
// This callback returns null to let Rust use its built-in THP implementation.
@@ -448,16 +453,15 @@ class TrezorTransport @Inject constructor(
448453
}
449454
}
450455

451-
@Suppress("TooGenericExceptionCaught")
452456
fun clearDeviceCredential(deviceId: String) {
453-
try {
457+
runCatching {
454458
val file = credentialFile(deviceId)
455459
TrezorDebugLog.log("CLEAR", "clearDeviceCredential for: $deviceId, exists=${file.exists()}")
456460
file.delete()
457461
Logger.info("Cleared device credential for: '$deviceId'", context = TAG)
458-
} catch (e: Exception) {
459-
TrezorDebugLog.log("CLEAR", "EXCEPTION: ${e.message}")
460-
Logger.error("Failed to clear device credential", e, context = TAG)
462+
}.onFailure {
463+
TrezorDebugLog.log("CLEAR", "EXCEPTION: ${it.message}")
464+
Logger.error("Failed to clear device credential", it, context = TAG)
461465
}
462466
}
463467

@@ -496,10 +500,11 @@ class TrezorTransport @Inject constructor(
496500
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
497501
)
498502

499-
context.registerReceiver(
503+
ContextCompat.registerReceiver(
504+
context,
500505
receiver,
501506
IntentFilter(ACTION_USB_PERMISSION),
502-
Context.RECEIVER_NOT_EXPORTED,
507+
ContextCompat.RECEIVER_NOT_EXPORTED,
503508
)
504509

505510
try {
@@ -529,13 +534,11 @@ class TrezorTransport @Inject constructor(
529534

530535
for (i in 0 until usbInterface.endpointCount) {
531536
val endpoint = usbInterface.getEndpoint(i)
532-
when {
533-
endpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT &&
534-
endpoint.direction == UsbConstants.USB_DIR_IN -> {
537+
when (endpoint.direction) {
538+
UsbConstants.USB_DIR_IN if endpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT -> {
535539
readEndpoint = endpoint
536540
}
537-
endpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT &&
538-
endpoint.direction == UsbConstants.USB_DIR_OUT -> {
541+
UsbConstants.USB_DIR_OUT if endpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT -> {
539542
writeEndpoint = endpoint
540543
}
541544
}
@@ -623,25 +626,39 @@ class TrezorTransport @Inject constructor(
623626
error = "Device not open: $path",
624627
)
625628

626-
val buffer = ByteArray(USB_CHUNK_SIZE)
627-
val bytesRead = openDevice.connection.bulkTransfer(
628-
openDevice.readEndpoint,
629-
buffer,
630-
buffer.size,
631-
READ_TIMEOUT_MS,
632-
)
629+
val buffer = ByteBuffer.allocate(USB_CHUNK_SIZE)
630+
val request = UsbRequest()
631+
try {
632+
if (!request.initialize(openDevice.connection, openDevice.readEndpoint)) {
633+
return TrezorTransportReadResult(
634+
success = false,
635+
data = byteArrayOf(),
636+
error = "Failed to initialize USB read request",
637+
)
638+
}
639+
if (!request.queue(buffer)) {
640+
return TrezorTransportReadResult(
641+
success = false,
642+
data = byteArrayOf(),
643+
error = "Failed to queue USB read request",
644+
)
645+
}
646+
openDevice.connection.requestWait(READ_TIMEOUT_MS.toLong())
647+
?: return TrezorTransportReadResult(
648+
success = false,
649+
data = byteArrayOf(),
650+
error = "USB read timed out",
651+
)
633652

634-
if (bytesRead < 0) {
635-
return TrezorTransportReadResult(
636-
success = false,
637-
data = byteArrayOf(),
638-
error = "Read failed: $bytesRead",
639-
)
653+
buffer.flip()
654+
val bytesRead = buffer.remaining()
655+
val data = ByteArray(bytesRead)
656+
buffer.get(data)
657+
Logger.debug("USB read '$bytesRead' bytes from '$path'", context = TAG)
658+
TrezorTransportReadResult(success = true, data = data, error = "")
659+
} finally {
660+
request.close()
640661
}
641-
642-
val data = buffer.copyOf(bytesRead)
643-
Logger.debug("USB read '$bytesRead' bytes from '$path'", context = TAG)
644-
TrezorTransportReadResult(success = true, data = data, error = "")
645662
} catch (e: Exception) {
646663
Logger.error("USB read failed", e, context = TAG)
647664
TrezorTransportReadResult(success = false, data = byteArrayOf(), error = e.message ?: "Unknown error")
@@ -654,19 +671,29 @@ class TrezorTransport @Inject constructor(
654671
val openDevice = usbConnections[path]
655672
?: return TrezorTransportWriteResult(success = false, error = "Device not open: $path")
656673

657-
val bytesWritten = openDevice.connection.bulkTransfer(
658-
openDevice.writeEndpoint,
659-
data,
660-
data.size,
661-
WRITE_TIMEOUT_MS,
662-
)
674+
val buffer = ByteBuffer.wrap(data)
675+
val request = UsbRequest()
676+
try {
677+
if (!request.initialize(openDevice.connection, openDevice.writeEndpoint)) {
678+
return TrezorTransportWriteResult(
679+
success = false,
680+
error = "Failed to initialize USB write request",
681+
)
682+
}
683+
if (!request.queue(buffer)) {
684+
return TrezorTransportWriteResult(
685+
success = false,
686+
error = "Failed to queue USB write request",
687+
)
688+
}
689+
openDevice.connection.requestWait(WRITE_TIMEOUT_MS.toLong())
690+
?: return TrezorTransportWriteResult(success = false, error = "USB write timed out")
663691

664-
if (bytesWritten < 0) {
665-
return TrezorTransportWriteResult(success = false, error = "Write failed: $bytesWritten")
692+
Logger.debug("USB wrote '${data.size}' bytes to '$path'", context = TAG)
693+
TrezorTransportWriteResult(success = true, error = "")
694+
} finally {
695+
request.close()
666696
}
667-
668-
Logger.debug("USB wrote '$bytesWritten' bytes to '$path'", context = TAG)
669-
TrezorTransportWriteResult(success = true, error = "")
670697
} catch (e: Exception) {
671698
Logger.error("USB write failed", e, context = TAG)
672699
TrezorTransportWriteResult(success = false, error = e.message ?: "Unknown error")
@@ -715,6 +742,7 @@ class TrezorTransport @Inject constructor(
715742
}
716743
}
717744

745+
@SuppressLint("MissingPermission")
718746
private val bleScanCallback = object : ScanCallback() {
719747
override fun onScanResult(callbackType: Int, result: ScanResult) {
720748
val device = result.device
@@ -1074,9 +1102,10 @@ class TrezorTransport @Inject constructor(
10741102
Logger.info("BLE services discovered: '$path'", context = TAG)
10751103
}
10761104

1105+
@Suppress("OVERRIDE_DEPRECATION")
10771106
override fun onCharacteristicChanged(
10781107
gatt: BluetoothGatt,
1079-
characteristic: BluetoothGattCharacteristic
1108+
characteristic: BluetoothGattCharacteristic,
10801109
) {
10811110
val path = "ble:${gatt.device.address}"
10821111
val connection = bleConnections[path] ?: return
@@ -1099,7 +1128,7 @@ class TrezorTransport @Inject constructor(
10991128
override fun onCharacteristicWrite(
11001129
gatt: BluetoothGatt,
11011130
characteristic: BluetoothGattCharacteristic,
1102-
status: Int
1131+
status: Int,
11031132
) {
11041133
val path = "ble:${gatt.device.address}"
11051134
val connection = bleConnections[path] ?: return
@@ -1113,7 +1142,7 @@ class TrezorTransport @Inject constructor(
11131142
override fun onDescriptorWrite(
11141143
gatt: BluetoothGatt,
11151144
descriptor: BluetoothGattDescriptor,
1116-
status: Int
1145+
status: Int,
11171146
) {
11181147
val path = "ble:${gatt.device.address}"
11191148
val connection = bleConnections[path] ?: return

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,9 @@ private fun NavGraphBuilder.settings(
877877
composableWithDefaultTransitions<Routes.DevSettings> {
878878
DevSettingsScreen(navController)
879879
}
880+
composableWithDefaultTransitions<Routes.Trezor> {
881+
TrezorScreen(navController)
882+
}
880883
composableWithDefaultTransitions<Routes.LdkDebug> {
881884
LdkDebugScreen(navController)
882885
}
@@ -1023,9 +1026,6 @@ private fun NavGraphBuilder.advancedSettings(navController: NavHostController) {
10231026
composableWithDefaultTransitions<Routes.NodeInfo> {
10241027
NodeInfoScreen(navController)
10251028
}
1026-
composableWithDefaultTransitions<Routes.Trezor> {
1027-
TrezorScreen(navController)
1028-
}
10291029
}
10301030

10311031
private fun NavGraphBuilder.aboutSettings(navController: NavHostController) {

app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ fun DevSettingsScreen(
5656
SettingsButtonRow("VSS") { navController.navigateTo(Routes.VssDebug) }
5757
SettingsButtonRow("Probing Tool") { navController.navigateTo(Routes.ProbingTool) }
5858

59+
SectionHeader("HARDWARE WALLET")
60+
SettingsButtonRow("Trezor") { navController.navigateTo(Routes.Trezor) }
61+
5962
SectionHeader("LOGS")
6063
SettingsButtonRow("Logs") { navController.navigateTo(Routes.Logs) }
6164
SettingsTextButtonRow(

app/src/main/java/to/bitkit/ui/screens/trezor/BalanceLookupSection.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ internal fun BalanceLookupSection(
8181
onClick = onLookup,
8282
enabled = !uiState.isLookingUp && uiState.lookupInput.isNotBlank(),
8383
size = ButtonSize.Small,
84-
modifier = Modifier.fillMaxWidth(),
8584
)
8685

8786
AnimatedVisibility(visible = uiState.accountInfoResult != null) {

app/src/main/java/to/bitkit/ui/screens/trezor/TrezorViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package to.bitkit.ui.screens.trezor
22

3+
import androidx.compose.runtime.Stable
34
import androidx.lifecycle.ViewModel
45
import androidx.lifecycle.viewModelScope
56
import com.synonym.bitkitcore.AccountInfoResult
@@ -562,6 +563,7 @@ class TrezorViewModel @Inject constructor(
562563
}
563564
}
564565

566+
@Stable
565567
data class TrezorUiState(
566568
val selectedNetwork: BitkitCoreNetwork = Env.network.toCoreNetwork(),
567569
val addressIndex: Int = 0,

0 commit comments

Comments
 (0)