Skip to content

Commit dd291ac

Browse files
authored
Merge pull request #976 from synonymdev/feat/onchain-event-watcher
feat: add trezor onchain event watcher
2 parents e5f8ad5 + 53cda0f commit dd291ac

10 files changed

Lines changed: 904 additions & 4 deletions

File tree

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.synonym.bitkitcore.CoinSelection
99
import com.synonym.bitkitcore.ComposeOutput
1010
import com.synonym.bitkitcore.ComposeParams
1111
import com.synonym.bitkitcore.ComposeResult
12+
import com.synonym.bitkitcore.EventListener
1213
import com.synonym.bitkitcore.SingleAddressInfoResult
1314
import com.synonym.bitkitcore.TransactionHistoryResult
1415
import com.synonym.bitkitcore.TrezorAddressResponse
@@ -22,18 +23,25 @@ import com.synonym.bitkitcore.TrezorSignedTx
2223
import com.synonym.bitkitcore.TrezorTransportType
2324
import com.synonym.bitkitcore.WalletParams
2425
import com.synonym.bitkitcore.WalletSelection
26+
import com.synonym.bitkitcore.WatcherEvent
27+
import com.synonym.bitkitcore.WatcherParams
2528
import dagger.hilt.android.qualifiers.ApplicationContext
2629
import kotlinx.collections.immutable.ImmutableList
2730
import kotlinx.collections.immutable.persistentListOf
2831
import kotlinx.collections.immutable.toImmutableList
2932
import kotlinx.coroutines.CoroutineDispatcher
3033
import kotlinx.coroutines.CoroutineScope
34+
import kotlinx.coroutines.SupervisorJob
3135
import kotlinx.coroutines.delay
36+
import kotlinx.coroutines.flow.MutableSharedFlow
3237
import kotlinx.coroutines.flow.MutableStateFlow
38+
import kotlinx.coroutines.flow.SharedFlow
39+
import kotlinx.coroutines.flow.asSharedFlow
3340
import kotlinx.coroutines.flow.asStateFlow
3441
import kotlinx.coroutines.flow.launchIn
3542
import kotlinx.coroutines.flow.onEach
3643
import kotlinx.coroutines.flow.update
44+
import kotlinx.coroutines.launch
3745
import kotlinx.coroutines.withContext
3846
import kotlinx.serialization.SerialName
3947
import kotlinx.serialization.Serializable
@@ -65,6 +73,7 @@ class TrezorRepo @Inject constructor(
6573
) {
6674
companion object {
6775
private const val TAG = "TrezorRepo"
76+
private const val WATCHER_TAG = "WATCHER"
6877
private const val DEFAULT_ADDRESS_PATH = "m/84'/0'/0'/0/0"
6978
private const val DEFAULT_ACCOUNT_PATH = "m/84'/0'/0'"
7079
private const val WALLET_MODE_RECONNECT_DELAY_MS = 1_000L
@@ -73,6 +82,18 @@ class TrezorRepo @Inject constructor(
7382
private val _state = MutableStateFlow(TrezorState())
7483
val state = _state.asStateFlow()
7584

85+
private val watcherCleanupScope = CoroutineScope(SupervisorJob() + ioDispatcher)
86+
87+
private val _watcherEvents = MutableSharedFlow<Pair<String, WatcherEvent>>(extraBufferCapacity = 64)
88+
val watcherEvents: SharedFlow<Pair<String, WatcherEvent>> = _watcherEvents.asSharedFlow()
89+
90+
private val eventBridge: EventListener = object : EventListener {
91+
override fun onEvent(watcherId: String, event: WatcherEvent) {
92+
TrezorDebugLog.log(WATCHER_TAG, "[$watcherId] ${event::class.simpleName}")
93+
_watcherEvents.tryEmit(watcherId to event)
94+
}
95+
}
96+
7697
/**
7798
* Flow indicating when a pairing code needs to be entered.
7899
* UI should show a dialog when this emits true.
@@ -551,6 +572,56 @@ class TrezorRepo @Inject constructor(
551572
}
552573
}
553574

575+
suspend fun startWatcher(
576+
watcherId: String,
577+
extendedKey: String,
578+
network: BitkitCoreNetwork,
579+
gapLimit: UInt = 20u,
580+
accountType: AccountType? = null,
581+
): Result<Unit> = withContext(ioDispatcher) {
582+
runCatching {
583+
val params = WatcherParams(
584+
watcherId = watcherId,
585+
extendedKey = extendedKey,
586+
electrumUrl = electrumUrlForNetwork(network),
587+
network = network,
588+
accountType = accountType,
589+
gapLimit = gapLimit,
590+
)
591+
trezorService.startWatcher(params, eventBridge)
592+
TrezorDebugLog.log(WATCHER_TAG, "Started watcher '$watcherId' for '${extendedKey.take(12)}...'")
593+
Logger.info("Started watcher '$watcherId'", context = TAG)
594+
}.onFailure {
595+
Logger.error("Start watcher failed", it, context = TAG)
596+
_state.update { s -> s.copy(error = it.message) }
597+
}
598+
}
599+
600+
suspend fun stopWatcher(watcherId: String): Result<Unit> = withContext(ioDispatcher) {
601+
runCatching {
602+
trezorService.stopWatcher(watcherId)
603+
TrezorDebugLog.log(WATCHER_TAG, "Stopped watcher '$watcherId'")
604+
Logger.info("Stopped watcher '$watcherId'", context = TAG)
605+
}.onFailure {
606+
Logger.error("Stop watcher failed", it, context = TAG)
607+
_state.update { s -> s.copy(error = it.message) }
608+
}
609+
}
610+
611+
fun stopWatcherOnCleared(watcherId: String) {
612+
watcherCleanupScope.launch { stopWatcher(watcherId) }
613+
}
614+
615+
suspend fun stopAllWatchers(): Result<Unit> = withContext(ioDispatcher) {
616+
runCatching {
617+
trezorService.stopAllWatchers()
618+
TrezorDebugLog.log(WATCHER_TAG, "Stopped all watchers")
619+
}.onFailure {
620+
Logger.error("Stop all watchers failed", it, context = TAG)
621+
_state.update { s -> s.copy(error = it.message) }
622+
}
623+
}
624+
554625
fun clearError() {
555626
_state.update { it.copy(error = null) }
556627
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.synonym.bitkitcore.AccountInfoResult
44
import com.synonym.bitkitcore.AccountType
55
import com.synonym.bitkitcore.ComposeParams
66
import com.synonym.bitkitcore.ComposeResult
7+
import com.synonym.bitkitcore.EventListener
78
import com.synonym.bitkitcore.SingleAddressInfoResult
89
import com.synonym.bitkitcore.TransactionHistoryResult
910
import com.synonym.bitkitcore.TrezorAddressResponse
@@ -19,11 +20,15 @@ import com.synonym.bitkitcore.TrezorSignedMessageResponse
1920
import com.synonym.bitkitcore.TrezorSignedTx
2021
import com.synonym.bitkitcore.TrezorVerifyMessageParams
2122
import com.synonym.bitkitcore.WalletSelection
23+
import com.synonym.bitkitcore.WatcherParams
2224
import com.synonym.bitkitcore.onchainBroadcastRawTx
2325
import com.synonym.bitkitcore.onchainComposeTransaction
2426
import com.synonym.bitkitcore.onchainGetAccountInfo
2527
import com.synonym.bitkitcore.onchainGetAddressInfo
2628
import com.synonym.bitkitcore.onchainGetTransactionHistory
29+
import com.synonym.bitkitcore.onchainStartWatcher
30+
import com.synonym.bitkitcore.onchainStopAllWatchers
31+
import com.synonym.bitkitcore.onchainStopWatcher
2732
import com.synonym.bitkitcore.trezorClearCredentials
2833
import com.synonym.bitkitcore.trezorConnect
2934
import com.synonym.bitkitcore.trezorDisconnect
@@ -266,4 +271,22 @@ class TrezorService @Inject constructor(
266271
)
267272
}
268273
}
274+
275+
suspend fun startWatcher(params: WatcherParams, listener: EventListener) {
276+
ServiceQueue.CORE.background {
277+
onchainStartWatcher(params = params, listener = listener)
278+
}
279+
}
280+
281+
suspend fun stopWatcher(watcherId: String) {
282+
ServiceQueue.CORE.background {
283+
onchainStopWatcher(watcherId = watcherId)
284+
}
285+
}
286+
287+
suspend fun stopAllWatchers() {
288+
ServiceQueue.CORE.background {
289+
onchainStopAllWatchers()
290+
}
291+
}
269292
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package to.bitkit.ui.screens.trezor
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.FlowRow
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.unit.dp
10+
import com.synonym.bitkitcore.AccountType
11+
import to.bitkit.ui.components.Caption
12+
import to.bitkit.ui.components.TagButton
13+
import to.bitkit.ui.components.VerticalSpacer
14+
import to.bitkit.ui.theme.Colors
15+
16+
internal fun AccountType?.accountTypeLabel(): String = when (this) {
17+
null -> "Auto"
18+
AccountType.LEGACY -> "Legacy"
19+
AccountType.WRAPPED_SEGWIT -> "Wrapped"
20+
AccountType.NATIVE_SEGWIT -> "Native"
21+
AccountType.TAPROOT -> "Taproot"
22+
}
23+
24+
@Composable
25+
internal fun AccountTypeSelectorRow(
26+
selectedAccountType: AccountType?,
27+
onAccountTypeChange: (AccountType?) -> Unit,
28+
) {
29+
Column {
30+
Caption("Account type (Auto = detect from key prefix)", color = Colors.White50)
31+
VerticalSpacer(8.dp)
32+
FlowRow(
33+
horizontalArrangement = Arrangement.spacedBy(8.dp),
34+
verticalArrangement = Arrangement.spacedBy(8.dp),
35+
modifier = Modifier.fillMaxWidth()
36+
) {
37+
val options = listOf(null) + AccountType.entries
38+
options.forEach { type ->
39+
TagButton(
40+
text = type.accountTypeLabel(),
41+
onClick = { onAccountTypeChange(type) },
42+
isSelected = type == selectedAccountType,
43+
)
44+
}
45+
}
46+
}
47+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.compose.ui.res.painterResource
1919
import androidx.compose.ui.tooling.preview.Preview
2020
import androidx.compose.ui.unit.dp
2121
import com.synonym.bitkitcore.AccountInfoResult
22+
import com.synonym.bitkitcore.AccountType
2223
import com.synonym.bitkitcore.AccountUtxo
2324
import com.synonym.bitkitcore.CoinSelection
2425
import com.synonym.bitkitcore.SingleAddressInfoResult
@@ -40,6 +41,7 @@ internal fun BalanceLookupSection(
4041
uiState: TrezorUiState,
4142
isDeviceConnected: Boolean,
4243
onInputChange: (String) -> Unit,
44+
onAccountTypeChange: (AccountType?) -> Unit,
4345
onLookup: () -> Unit,
4446
onSendAddressChange: (String) -> Unit,
4547
onSendAmountChange: (String) -> Unit,
@@ -74,6 +76,13 @@ internal fun BalanceLookupSection(
7476
modifier = Modifier.fillMaxWidth(),
7577
)
7678

79+
VerticalSpacer(8.dp)
80+
81+
AccountTypeSelectorRow(
82+
selectedAccountType = uiState.lookupSelectedAccountType,
83+
onAccountTypeChange = onAccountTypeChange,
84+
)
85+
7786
VerticalSpacer(16.dp)
7887

7988
PrimaryButton(
@@ -264,6 +273,7 @@ private fun PreviewBalanceLookupEmpty() {
264273
uiState = TrezorUiState(),
265274
isDeviceConnected = false,
266275
onInputChange = {},
276+
onAccountTypeChange = {},
267277
onLookup = {},
268278
onSendAddressChange = {},
269279
onSendAmountChange = {},
@@ -287,6 +297,7 @@ private fun PreviewBalanceLookupWithAccountInfo() {
287297
uiState = TrezorPreviewData.uiStateWithAccountInfo,
288298
isDeviceConnected = true,
289299
onInputChange = {},
300+
onAccountTypeChange = {},
290301
onLookup = {},
291302
onSendAddressChange = {},
292303
onSendAmountChange = {},
@@ -310,6 +321,7 @@ private fun PreviewBalanceLookupWithAddressInfo() {
310321
uiState = TrezorPreviewData.uiStateWithAddressInfo,
311322
isDeviceConnected = false,
312323
onInputChange = {},
324+
onAccountTypeChange = {},
313325
onLookup = {},
314326
onSendAddressChange = {},
315327
onSendAmountChange = {},
@@ -338,6 +350,7 @@ private fun PreviewBalanceLookupLoading() {
338350
),
339351
isDeviceConnected = false,
340352
onInputChange = {},
353+
onAccountTypeChange = {},
341354
onLookup = {},
342355
onSendAddressChange = {},
343356
onSendAmountChange = {},

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
1414
import androidx.compose.ui.res.painterResource
1515
import androidx.compose.ui.tooling.preview.Preview
1616
import androidx.compose.ui.unit.dp
17+
import com.synonym.bitkitcore.AccountType
1718
import com.synonym.bitkitcore.HistoryTransaction
1819
import com.synonym.bitkitcore.TransactionHistoryResult
1920
import com.synonym.bitkitcore.TxDirection
@@ -36,6 +37,7 @@ import java.util.Locale
3637
internal fun TransactionHistorySection(
3738
uiState: TrezorUiState,
3839
onInputChange: (String) -> Unit,
40+
onAccountTypeChange: (AccountType?) -> Unit,
3941
onLookup: () -> Unit,
4042
) {
4143
Column {
@@ -60,6 +62,13 @@ internal fun TransactionHistorySection(
6062
modifier = Modifier.fillMaxWidth(),
6163
)
6264

65+
VerticalSpacer(8.dp)
66+
67+
AccountTypeSelectorRow(
68+
selectedAccountType = uiState.txHistorySelectedAccountType,
69+
onAccountTypeChange = onAccountTypeChange,
70+
)
71+
6372
VerticalSpacer(16.dp)
6473

6574
PrimaryButton(
@@ -165,6 +174,7 @@ private fun PreviewTransactionHistoryEmpty() {
165174
TransactionHistorySection(
166175
uiState = TrezorUiState(),
167176
onInputChange = {},
177+
onAccountTypeChange = {},
168178
onLookup = {},
169179
)
170180
}
@@ -182,6 +192,7 @@ private fun PreviewTransactionHistoryLoading() {
182192
),
183193
),
184194
onInputChange = {},
195+
onAccountTypeChange = {},
185196
onLookup = {},
186197
)
187198
}
@@ -194,6 +205,7 @@ private fun PreviewTransactionHistoryWithResult() {
194205
TransactionHistorySection(
195206
uiState = TrezorPreviewData.uiStateWithTxHistory,
196207
onInputChange = {},
208+
onAccountTypeChange = {},
197209
onLookup = {},
198210
)
199211
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import com.synonym.bitkitcore.TrezorSignedTx
1717
import com.synonym.bitkitcore.TrezorTransportType
1818
import com.synonym.bitkitcore.TxDirection
1919
import com.synonym.bitkitcore.WalletBalance
20+
import kotlinx.collections.immutable.persistentListOf
21+
import kotlinx.collections.immutable.toImmutableList
2022
import to.bitkit.repositories.ConnectedTrezorDevice
2123
import to.bitkit.repositories.KnownDevice
2224
import to.bitkit.repositories.KnownDeviceTransportType
@@ -305,6 +307,24 @@ internal object TrezorPreviewData {
305307
),
306308
)
307309

310+
val uiStateWithActiveWatcher = TrezorUiState(
311+
network = TrezorNetworkState(selectedNetwork = BitkitCoreNetwork.REGTEST),
312+
watcher = TrezorWatcherState(
313+
extendedKey = SAMPLE_XPUB,
314+
activeWatcherId = "watcher-abc-123",
315+
connectionStatus = WatcherConnectionStatus.CONNECTED,
316+
balance = sampleWalletBalance,
317+
transactions = sampleHistoryTransactions.toImmutableList(),
318+
transactionCount = 2u,
319+
blockHeight = 850_000u,
320+
accountType = AccountType.NATIVE_SEGWIT,
321+
events = persistentListOf(
322+
"Watcher started: watcher-abc-123",
323+
"TX update: 2 txs, balance=155000 sats",
324+
),
325+
),
326+
)
327+
308328
val uiStateBroadcast = TrezorUiState(
309329
network = TrezorNetworkState(selectedNetwork = BitkitCoreNetwork.REGTEST),
310330
send = TrezorSendState(

0 commit comments

Comments
 (0)