Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions app/src/main/java/to/bitkit/repositories/TrezorRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.synonym.bitkitcore.CoinSelection
import com.synonym.bitkitcore.ComposeOutput
import com.synonym.bitkitcore.ComposeParams
import com.synonym.bitkitcore.ComposeResult
import com.synonym.bitkitcore.EventListener
import com.synonym.bitkitcore.SingleAddressInfoResult
import com.synonym.bitkitcore.TransactionHistoryResult
import com.synonym.bitkitcore.TrezorAddressResponse
Expand All @@ -22,18 +23,25 @@ import com.synonym.bitkitcore.TrezorSignedTx
import com.synonym.bitkitcore.TrezorTransportType
import com.synonym.bitkitcore.WalletParams
import com.synonym.bitkitcore.WalletSelection
import com.synonym.bitkitcore.WatcherEvent
import com.synonym.bitkitcore.WatcherParams
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -65,6 +73,7 @@ class TrezorRepo @Inject constructor(
) {
companion object {
private const val TAG = "TrezorRepo"
private const val WATCHER_TAG = "WATCHER"
private const val DEFAULT_ADDRESS_PATH = "m/84'/0'/0'/0/0"
private const val DEFAULT_ACCOUNT_PATH = "m/84'/0'/0'"
private const val WALLET_MODE_RECONNECT_DELAY_MS = 1_000L
Expand All @@ -73,6 +82,18 @@ class TrezorRepo @Inject constructor(
private val _state = MutableStateFlow(TrezorState())
val state = _state.asStateFlow()

private val watcherCleanupScope = CoroutineScope(SupervisorJob() + ioDispatcher)

private val _watcherEvents = MutableSharedFlow<Pair<String, WatcherEvent>>(extraBufferCapacity = 64)
val watcherEvents: SharedFlow<Pair<String, WatcherEvent>> = _watcherEvents.asSharedFlow()

private val eventBridge: EventListener = object : EventListener {
override fun onEvent(watcherId: String, event: WatcherEvent) {
TrezorDebugLog.log(WATCHER_TAG, "[$watcherId] ${event::class.simpleName}")
_watcherEvents.tryEmit(watcherId to event)
}
}

/**
* Flow indicating when a pairing code needs to be entered.
* UI should show a dialog when this emits true.
Expand Down Expand Up @@ -551,6 +572,56 @@ class TrezorRepo @Inject constructor(
}
}

suspend fun startWatcher(
watcherId: String,
extendedKey: String,
network: BitkitCoreNetwork,
gapLimit: UInt = 20u,
accountType: AccountType? = null,
): Result<Unit> = withContext(ioDispatcher) {
runCatching {
val params = WatcherParams(
watcherId = watcherId,
extendedKey = extendedKey,
electrumUrl = electrumUrlForNetwork(network),
network = network,
accountType = accountType,
gapLimit = gapLimit,
)
trezorService.startWatcher(params, eventBridge)
TrezorDebugLog.log(WATCHER_TAG, "Started watcher '$watcherId' for '${extendedKey.take(12)}...'")
Logger.info("Started watcher '$watcherId'", context = TAG)
}.onFailure {
Logger.error("Start watcher failed", it, context = TAG)
_state.update { s -> s.copy(error = it.message) }
}
}

suspend fun stopWatcher(watcherId: String): Result<Unit> = withContext(ioDispatcher) {
runCatching {
trezorService.stopWatcher(watcherId)
TrezorDebugLog.log(WATCHER_TAG, "Stopped watcher '$watcherId'")
Logger.info("Stopped watcher '$watcherId'", context = TAG)
}.onFailure {
Logger.error("Stop watcher failed", it, context = TAG)
_state.update { s -> s.copy(error = it.message) }
}
}

fun stopWatcherOnCleared(watcherId: String) {
watcherCleanupScope.launch { stopWatcher(watcherId) }
}

suspend fun stopAllWatchers(): Result<Unit> = withContext(ioDispatcher) {
runCatching {
trezorService.stopAllWatchers()
TrezorDebugLog.log(WATCHER_TAG, "Stopped all watchers")
}.onFailure {
Logger.error("Stop all watchers failed", it, context = TAG)
_state.update { s -> s.copy(error = it.message) }
}
}

fun clearError() {
_state.update { it.copy(error = null) }
}
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/to/bitkit/services/TrezorService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.synonym.bitkitcore.AccountInfoResult
import com.synonym.bitkitcore.AccountType
import com.synonym.bitkitcore.ComposeParams
import com.synonym.bitkitcore.ComposeResult
import com.synonym.bitkitcore.EventListener
import com.synonym.bitkitcore.SingleAddressInfoResult
import com.synonym.bitkitcore.TransactionHistoryResult
import com.synonym.bitkitcore.TrezorAddressResponse
Expand All @@ -19,11 +20,15 @@ import com.synonym.bitkitcore.TrezorSignedMessageResponse
import com.synonym.bitkitcore.TrezorSignedTx
import com.synonym.bitkitcore.TrezorVerifyMessageParams
import com.synonym.bitkitcore.WalletSelection
import com.synonym.bitkitcore.WatcherParams
import com.synonym.bitkitcore.onchainBroadcastRawTx
import com.synonym.bitkitcore.onchainComposeTransaction
import com.synonym.bitkitcore.onchainGetAccountInfo
import com.synonym.bitkitcore.onchainGetAddressInfo
import com.synonym.bitkitcore.onchainGetTransactionHistory
import com.synonym.bitkitcore.onchainStartWatcher
import com.synonym.bitkitcore.onchainStopAllWatchers
import com.synonym.bitkitcore.onchainStopWatcher
import com.synonym.bitkitcore.trezorClearCredentials
import com.synonym.bitkitcore.trezorConnect
import com.synonym.bitkitcore.trezorDisconnect
Expand Down Expand Up @@ -266,4 +271,22 @@ class TrezorService @Inject constructor(
)
}
}

suspend fun startWatcher(params: WatcherParams, listener: EventListener) {
ServiceQueue.CORE.background {
onchainStartWatcher(params = params, listener = listener)
}
}

suspend fun stopWatcher(watcherId: String) {
ServiceQueue.CORE.background {
onchainStopWatcher(watcherId = watcherId)
}
}

suspend fun stopAllWatchers() {
ServiceQueue.CORE.background {
onchainStopAllWatchers()
}
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/to/bitkit/ui/screens/trezor/TrezorPreviewData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.synonym.bitkitcore.TrezorSignedTx
import com.synonym.bitkitcore.TrezorTransportType
import com.synonym.bitkitcore.TxDirection
import com.synonym.bitkitcore.WalletBalance
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import to.bitkit.repositories.ConnectedTrezorDevice
import to.bitkit.repositories.KnownDevice
import to.bitkit.repositories.KnownDeviceTransportType
Expand Down Expand Up @@ -305,6 +307,24 @@ internal object TrezorPreviewData {
),
)

val uiStateWithActiveWatcher = TrezorUiState(
network = TrezorNetworkState(selectedNetwork = BitkitCoreNetwork.REGTEST),
watcher = TrezorWatcherState(
extendedKey = SAMPLE_XPUB,
activeWatcherId = "watcher-abc-123",
connectionStatus = WatcherConnectionStatus.CONNECTED,
balance = sampleWalletBalance,
transactions = sampleHistoryTransactions.toImmutableList(),
transactionCount = 2u,
blockHeight = 850_000u,
accountType = AccountType.NATIVE_SEGWIT,
events = persistentListOf(
"Watcher started: watcher-abc-123",
"TX update: 2 txs, balance=155000 sats",
),
),
)

val uiStateBroadcast = TrezorUiState(
network = TrezorNetworkState(selectedNetwork = BitkitCoreNetwork.REGTEST),
send = TrezorSendState(
Expand Down
30 changes: 28 additions & 2 deletions app/src/main/java/to/bitkit/ui/screens/trezor/TrezorScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.synonym.bitkitcore.AccountType
import com.synonym.bitkitcore.CoinSelection
import kotlinx.collections.immutable.toImmutableList
import to.bitkit.R
Expand Down Expand Up @@ -167,6 +168,12 @@ private fun TrezorScreenContent(
onResetSend = viewModel::resetSendFlow,
onTxHistoryInputChange = viewModel::setTxHistoryInput,
onLookupTxHistory = viewModel::lookupTransactionHistory,
onWatcherExtendedKeyChange = viewModel::setWatcherExtendedKey,
onWatcherGapLimitChange = viewModel::setWatcherGapLimit,
onWatcherAccountTypeChange = viewModel::setWatcherAccountType,
onStartWatcher = viewModel::startWatcher,
onStopWatcher = viewModel::stopWatcher,
onPopulateWatcherFromXpub = viewModel::populateWatcherFromXpub,
permissionsGranted = permissionsState.allPermissionsGranted,
)
}
Expand Down Expand Up @@ -207,6 +214,12 @@ private fun Content(
onResetSend: () -> Unit = {},
onTxHistoryInputChange: (String) -> Unit = {},
onLookupTxHistory: () -> Unit = {},
onWatcherExtendedKeyChange: (String) -> Unit = {},
onWatcherGapLimitChange: (String) -> Unit = {},
onWatcherAccountTypeChange: (AccountType?) -> Unit = {},
onStartWatcher: () -> Unit = {},
onStopWatcher: () -> Unit = {},
onPopulateWatcherFromXpub: () -> Unit = {},
permissionsGranted: Boolean = true,
) {
Column(
Expand Down Expand Up @@ -419,14 +432,27 @@ private fun Content(
onResetSend = onResetSend,
)

// Transaction History (always visible, no device needed)
// Transaction History (one-shot snapshot, no device needed)
VerticalSpacer(32.dp)
TransactionHistorySection(
uiState = uiState,
onInputChange = onTxHistoryInputChange,
onLookup = onLookupTxHistory,
)

// Event Watcher (live subscription, no device needed)
VerticalSpacer(32.dp)
WatcherSection(
uiState = uiState,
trezorState = trezorState,
onExtendedKeyChange = onWatcherExtendedKeyChange,
onGapLimitChange = onWatcherGapLimitChange,
onAccountTypeChange = onWatcherAccountTypeChange,
onStartWatcher = onStartWatcher,
onStopWatcher = onStopWatcher,
onPopulateFromXpub = onPopulateWatcherFromXpub,
)

// Debug Log Window
DebugLogSection()
}
Expand Down Expand Up @@ -668,7 +694,7 @@ private fun StatusRow(trezorState: TrezorState) {
}

@Composable
private fun StatusBadge(text: String, color: Color) {
internal fun StatusBadge(text: String, color: Color) {
Caption(
text = text,
color = color,
Expand Down
Loading
Loading