Skip to content

Commit fe8bdce

Browse files
committed
feat: add tx history and balance functionality to Trezor dev view
1 parent c92df2b commit fe8bdce

6 files changed

Lines changed: 340 additions & 0 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.synonym.bitkitcore.ComposeOutput
99
import com.synonym.bitkitcore.ComposeParams
1010
import com.synonym.bitkitcore.ComposeResult
1111
import com.synonym.bitkitcore.SingleAddressInfoResult
12+
import com.synonym.bitkitcore.TransactionHistoryResult
1213
import com.synonym.bitkitcore.TrezorAddressResponse
1314
import com.synonym.bitkitcore.TrezorCoinType
1415
import com.synonym.bitkitcore.TrezorDeviceInfo
@@ -202,6 +203,24 @@ class TrezorRepo @Inject constructor(
202203
}
203204
}
204205

206+
suspend fun getTransactionHistory(
207+
extendedKey: String,
208+
network: BitkitCoreNetwork = Env.network.toCoreNetwork(),
209+
scriptType: AccountType? = null,
210+
): Result<TransactionHistoryResult> = withContext(ioDispatcher) {
211+
runCatching {
212+
trezorService.getTransactionHistory(
213+
extendedKey = extendedKey,
214+
electrumUrl = electrumUrlForNetwork(network),
215+
network = network,
216+
scriptType = scriptType,
217+
)
218+
}.onFailure {
219+
Logger.error("Failed to get Trezor transaction history", it, context = TAG)
220+
_state.update { s -> s.copy(error = it.message) }
221+
}
222+
}
223+
205224
suspend fun getAccountInfo(
206225
extendedKey: String,
207226
network: BitkitCoreNetwork = Env.network.toCoreNetwork(),

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.synonym.bitkitcore.AccountType
55
import com.synonym.bitkitcore.ComposeParams
66
import com.synonym.bitkitcore.ComposeResult
77
import com.synonym.bitkitcore.SingleAddressInfoResult
8+
import com.synonym.bitkitcore.TransactionHistoryResult
89
import com.synonym.bitkitcore.TrezorAddressResponse
910
import com.synonym.bitkitcore.TrezorCoinType
1011
import com.synonym.bitkitcore.TrezorDeviceInfo
@@ -21,6 +22,7 @@ import com.synonym.bitkitcore.onchainBroadcastRawTx
2122
import com.synonym.bitkitcore.onchainComposeTransaction
2223
import com.synonym.bitkitcore.onchainGetAccountInfo
2324
import com.synonym.bitkitcore.onchainGetAddressInfo
25+
import com.synonym.bitkitcore.onchainGetTransactionHistory
2426
import com.synonym.bitkitcore.trezorClearCredentials
2527
import com.synonym.bitkitcore.trezorConnect
2628
import com.synonym.bitkitcore.trezorDisconnect
@@ -208,6 +210,22 @@ class TrezorService @Inject constructor(
208210
}
209211
}
210212

213+
suspend fun getTransactionHistory(
214+
extendedKey: String,
215+
electrumUrl: String,
216+
network: BitkitCoreNetwork?,
217+
scriptType: AccountType? = null,
218+
): TransactionHistoryResult {
219+
return ServiceQueue.CORE.background {
220+
onchainGetTransactionHistory(
221+
extendedKey = extendedKey,
222+
electrumUrl = electrumUrl,
223+
network = network,
224+
scriptType = scriptType,
225+
)
226+
}
227+
}
228+
211229
suspend fun getAccountInfo(
212230
extendedKey: String,
213231
electrumUrl: String,
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package to.bitkit.ui.screens.trezor
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.size
8+
import androidx.compose.material3.Icon
9+
import androidx.compose.material3.OutlinedTextField
10+
import androidx.compose.material3.OutlinedTextFieldDefaults
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.res.painterResource
15+
import androidx.compose.ui.tooling.preview.Preview
16+
import androidx.compose.ui.unit.dp
17+
import com.synonym.bitkitcore.HistoryTransaction
18+
import com.synonym.bitkitcore.TransactionHistoryResult
19+
import com.synonym.bitkitcore.TxDirection
20+
import to.bitkit.R
21+
import to.bitkit.ui.components.ButtonSize
22+
import to.bitkit.ui.components.Caption
23+
import to.bitkit.ui.components.Caption13Up
24+
import to.bitkit.ui.components.HorizontalSpacer
25+
import to.bitkit.ui.components.PrimaryButton
26+
import to.bitkit.ui.components.VerticalSpacer
27+
import to.bitkit.ui.shared.modifiers.clickableAlpha
28+
import to.bitkit.ui.theme.AppThemeSurface
29+
import to.bitkit.ui.theme.Colors
30+
import to.bitkit.ui.utils.copyToClipboard
31+
import java.text.SimpleDateFormat
32+
import java.util.Date
33+
import java.util.Locale
34+
35+
@Composable
36+
internal fun TransactionHistorySection(
37+
uiState: TrezorUiState,
38+
onInputChange: (String) -> Unit,
39+
onLookup: () -> Unit,
40+
) {
41+
Column {
42+
Caption13Up(
43+
text = "Transaction History",
44+
color = Colors.White64,
45+
)
46+
VerticalSpacer(8.dp)
47+
48+
OutlinedTextField(
49+
value = uiState.txHistoryInput,
50+
onValueChange = onInputChange,
51+
label = { Caption("xpub / vpub / zpub", color = Colors.White50) },
52+
colors = OutlinedTextFieldDefaults.colors(
53+
focusedTextColor = Colors.White,
54+
unfocusedTextColor = Colors.White,
55+
focusedBorderColor = Colors.Brand,
56+
unfocusedBorderColor = Colors.White32,
57+
cursorColor = Colors.Brand,
58+
),
59+
maxLines = 3,
60+
modifier = Modifier.fillMaxWidth(),
61+
)
62+
63+
VerticalSpacer(16.dp)
64+
65+
PrimaryButton(
66+
text = if (uiState.isLoadingTxHistory) "Loading..." else "Lookup History",
67+
onClick = onLookup,
68+
enabled = !uiState.isLoadingTxHistory && uiState.txHistoryInput.isNotBlank(),
69+
size = ButtonSize.Small,
70+
modifier = Modifier.fillMaxWidth(),
71+
)
72+
73+
AnimatedVisibility(visible = uiState.txHistoryResult != null) {
74+
uiState.txHistoryResult?.let { TransactionHistoryResultView(it) }
75+
}
76+
}
77+
}
78+
79+
@Composable
80+
private fun TransactionHistoryResultView(result: TransactionHistoryResult) {
81+
Column {
82+
VerticalSpacer(16.dp)
83+
84+
Caption13Up(text = "Summary", color = Colors.White64)
85+
VerticalSpacer(4.dp)
86+
ResultCard {
87+
InfoRow("Account Type", result.accountType.name)
88+
InfoRow("Transactions", "${result.txCount}")
89+
InfoRow("Block Height", "${result.blockHeight}")
90+
}
91+
92+
VerticalSpacer(12.dp)
93+
94+
Caption13Up(text = "Balance", color = Colors.White64)
95+
VerticalSpacer(4.dp)
96+
ResultCard {
97+
InfoRow("Confirmed", "${result.balance.confirmed} sats")
98+
InfoRow("Trusted Pending", "${result.balance.trustedPending} sats")
99+
InfoRow("Untrusted Pending", "${result.balance.untrustedPending} sats")
100+
InfoRow("Spendable", "${result.balance.spendable} sats")
101+
InfoRow("Total", "${result.balance.total} sats")
102+
}
103+
104+
if (result.transactions.isNotEmpty()) {
105+
VerticalSpacer(12.dp)
106+
Caption13Up(
107+
text = "Transactions (${result.transactions.size})",
108+
color = Colors.White64,
109+
)
110+
VerticalSpacer(4.dp)
111+
result.transactions.forEach { tx ->
112+
TransactionRow(tx)
113+
VerticalSpacer(4.dp)
114+
}
115+
}
116+
}
117+
}
118+
119+
@Composable
120+
private fun TransactionRow(tx: HistoryTransaction) {
121+
val onCopyTxid = copyToClipboard(text = tx.txid, label = "TXID")
122+
val directionLabel = when (tx.direction) {
123+
TxDirection.SENT -> "Sent"
124+
TxDirection.RECEIVED -> "Received"
125+
TxDirection.SELF_TRANSFER -> "Self Transfer"
126+
}
127+
ResultCard {
128+
Row(
129+
verticalAlignment = Alignment.CenterVertically,
130+
modifier = Modifier.fillMaxWidth(),
131+
) {
132+
Caption(
133+
text = "${tx.txid.take(8)}...${tx.txid.takeLast(8)}",
134+
color = Colors.Brand,
135+
modifier = Modifier.weight(1f),
136+
)
137+
HorizontalSpacer(8.dp)
138+
Icon(
139+
painter = painterResource(R.drawable.ic_copy),
140+
contentDescription = "Copy txid",
141+
tint = Colors.Brand,
142+
modifier = Modifier
143+
.size(16.dp)
144+
.clickableAlpha(onClick = onCopyTxid),
145+
)
146+
}
147+
InfoRow("Direction", directionLabel)
148+
InfoRow("Net", "${tx.net} sats")
149+
tx.fee?.let { InfoRow("Fee", "$it sats") }
150+
InfoRow("Confirmations", "${tx.confirmations}")
151+
tx.blockHeight?.let { InfoRow("Block", "$it") }
152+
tx.timestamp?.let { InfoRow("Time", formatTimestamp(it)) }
153+
}
154+
}
155+
156+
private fun formatTimestamp(epochSeconds: ULong, locale: Locale = Locale.getDefault()): String = runCatching {
157+
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", locale)
158+
sdf.format(Date(epochSeconds.toLong() * 1000))
159+
}.getOrDefault("$epochSeconds")
160+
161+
@Preview
162+
@Composable
163+
private fun PreviewTransactionHistoryEmpty() {
164+
AppThemeSurface {
165+
TransactionHistorySection(
166+
uiState = TrezorUiState(),
167+
onInputChange = {},
168+
onLookup = {},
169+
)
170+
}
171+
}
172+
173+
@Preview
174+
@Composable
175+
private fun PreviewTransactionHistoryLoading() {
176+
AppThemeSurface {
177+
TransactionHistorySection(
178+
uiState = TrezorUiState(txHistoryInput = "vpub5Y...", isLoadingTxHistory = true),
179+
onInputChange = {},
180+
onLookup = {},
181+
)
182+
}
183+
}
184+
185+
@Preview
186+
@Composable
187+
private fun PreviewTransactionHistoryWithResult() {
188+
AppThemeSurface {
189+
TransactionHistorySection(
190+
uiState = TrezorPreviewData.uiStateWithTxHistory,
191+
onInputChange = {},
192+
onLookup = {},
193+
)
194+
}
195+
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import com.synonym.bitkitcore.AccountType
66
import com.synonym.bitkitcore.AccountUtxo
77
import com.synonym.bitkitcore.ComposeAccount
88
import com.synonym.bitkitcore.ComposeResult
9+
import com.synonym.bitkitcore.HistoryTransaction
910
import com.synonym.bitkitcore.SingleAddressInfoResult
11+
import com.synonym.bitkitcore.TransactionHistoryResult
1012
import com.synonym.bitkitcore.TrezorAddressResponse
1113
import com.synonym.bitkitcore.TrezorDeviceInfo
1214
import com.synonym.bitkitcore.TrezorFeatures
1315
import com.synonym.bitkitcore.TrezorPublicKeyResponse
1416
import com.synonym.bitkitcore.TrezorSignedTx
1517
import com.synonym.bitkitcore.TrezorTransportType
18+
import com.synonym.bitkitcore.TxDirection
19+
import com.synonym.bitkitcore.WalletBalance
1620
import to.bitkit.repositories.KnownDevice
1721
import to.bitkit.repositories.TrezorState
1822
import com.synonym.bitkitcore.Network as BitkitCoreNetwork
@@ -223,6 +227,65 @@ internal object TrezorPreviewData {
223227
signedTxResult = sampleSignedTx,
224228
)
225229

230+
val sampleWalletBalance = WalletBalance(
231+
confirmed = 150_000uL,
232+
immature = 0uL,
233+
trustedPending = 5_000uL,
234+
untrustedPending = 0uL,
235+
spendable = 155_000uL,
236+
total = 155_000uL,
237+
)
238+
239+
val sampleHistoryTransactions = listOf(
240+
HistoryTransaction(
241+
txid = SAMPLE_TXID,
242+
received = 100_000uL,
243+
sent = 0uL,
244+
net = 100_000L,
245+
fee = null,
246+
direction = TxDirection.RECEIVED,
247+
blockHeight = 849_990u,
248+
timestamp = 1_700_000_000uL,
249+
confirmations = 10u,
250+
),
251+
HistoryTransaction(
252+
txid = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
253+
received = 0uL,
254+
sent = 50_000uL,
255+
net = -50_000L,
256+
fee = 1_200uL,
257+
direction = TxDirection.SENT,
258+
blockHeight = 849_995u,
259+
timestamp = 1_700_100_000uL,
260+
confirmations = 5u,
261+
),
262+
HistoryTransaction(
263+
txid = "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
264+
received = 5_000uL,
265+
sent = 5_000uL,
266+
net = 0L,
267+
fee = 500uL,
268+
direction = TxDirection.SELF_TRANSFER,
269+
blockHeight = null,
270+
timestamp = null,
271+
confirmations = 0u,
272+
),
273+
)
274+
275+
val sampleTransactionHistoryResult = TransactionHistoryResult(
276+
transactions = sampleHistoryTransactions,
277+
balance = sampleWalletBalance,
278+
txCount = 3u,
279+
blockHeight = 850_000u,
280+
accountType = AccountType.NATIVE_SEGWIT,
281+
)
282+
283+
val uiStateWithTxHistory = TrezorUiState(
284+
selectedNetwork = BitkitCoreNetwork.REGTEST,
285+
txHistoryInput = SAMPLE_XPUB,
286+
txHistoryResult = sampleTransactionHistoryResult,
287+
)
288+
226289
val uiStateBroadcast = TrezorUiState(
227290
selectedNetwork = BitkitCoreNetwork.REGTEST,
228291
sendStep = SendStep.SIGNED,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ private fun TrezorScreenContent(
151151
onBroadcast = viewModel::broadcastSignedTx,
152152
onBackToForm = viewModel::backToComposeForm,
153153
onResetSend = viewModel::resetSendFlow,
154+
onTxHistoryInputChange = viewModel::setTxHistoryInput,
155+
onLookupTxHistory = viewModel::lookupTransactionHistory,
154156
permissionsGranted = permissionsState.allPermissionsGranted,
155157
)
156158
}
@@ -187,6 +189,8 @@ private fun Content(
187189
onBroadcast: () -> Unit = {},
188190
onBackToForm: () -> Unit = {},
189191
onResetSend: () -> Unit = {},
192+
onTxHistoryInputChange: (String) -> Unit = {},
193+
onLookupTxHistory: () -> Unit = {},
190194
permissionsGranted: Boolean = true,
191195
) {
192196
Column(
@@ -391,6 +395,14 @@ private fun Content(
391395
onResetSend = onResetSend,
392396
)
393397

398+
// Transaction History (always visible, no device needed)
399+
VerticalSpacer(32.dp)
400+
TransactionHistorySection(
401+
uiState = uiState,
402+
onInputChange = onTxHistoryInputChange,
403+
onLookup = onLookupTxHistory,
404+
)
405+
394406
// Debug Log Window
395407
DebugLogSection()
396408
}

0 commit comments

Comments
 (0)