Skip to content

Commit 1b7436c

Browse files
authored
Merge pull request #209 from synonymdev/feat/lightning-conn
Lightning connections screen
2 parents 8fa6cd6 + 80765ff commit 1b7436c

File tree

21 files changed

+1002
-164
lines changed

21 files changed

+1002
-164
lines changed

app/src/androidTest/java/to/bitkit/data/keychain/KeychainTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import org.junit.Test
1313
import org.junit.runner.RunWith
1414
import to.bitkit.data.AppDb
1515
import to.bitkit.data.entities.ConfigEntity
16-
import to.bitkit.utils.KeychainError
1716
import to.bitkit.test.BaseAndroidTest
1817
import kotlin.test.assertEquals
1918
import kotlin.test.assertFailsWith

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,31 @@ class CacheStore @Inject constructor(
2828
store.updateData(transform)
2929
}
3030

31+
suspend fun addPaidOrder(orderId: String, txId: String) {
32+
store.updateData {
33+
val newEntry = mapOf(orderId to txId)
34+
val updatedOrders = newEntry + it.paidOrders
35+
val limitedOrders = when {
36+
updatedOrders.size > MAX_PAID_ORDERS -> updatedOrders.toList().take(MAX_PAID_ORDERS).toMap()
37+
else -> updatedOrders
38+
}
39+
Logger.debug("Cached paid order '$orderId'")
40+
it.copy(paidOrders = limitedOrders)
41+
}
42+
}
43+
3144
suspend fun reset() {
3245
store.updateData { AppCacheData() }
3346
Logger.info("Deleted all app cached data.")
3447
}
48+
49+
companion object {
50+
private const val MAX_PAID_ORDERS = 50
51+
}
3552
}
3653

3754
@Serializable
3855
data class AppCacheData(
3956
val cachedRates: List<FxRate> = listOf(),
57+
val paidOrders: Map<String, String> = mapOf(),
4058
)

app/src/main/java/to/bitkit/ext/ChannelDetails.kt

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,45 @@ val ChannelDetails.amountOnClose: ULong
1515
return outboundCapacitySat + reserveSats
1616
}
1717

18-
fun mockChannelDetails(
19-
channelId: String,
20-
isChannelReady: Boolean = true,
21-
): ChannelDetails {
18+
fun createChannelDetails(): ChannelDetails {
2219
return ChannelDetails(
23-
channelId = channelId,
20+
channelId = "channelId",
2421
counterpartyNodeId = "counterpartyNodeId",
2522
fundingTxo = null,
26-
channelValueSats = 100_000uL,
27-
unspendablePunishmentReserve = 354uL,
28-
userChannelId = "userChannelId",
29-
feerateSatPer1000Weight = 5u,
30-
outboundCapacityMsat = 50_000uL,
31-
inboundCapacityMsat = 50_000uL,
32-
confirmationsRequired = 0u,
33-
confirmations = 12u,
23+
shortChannelId = null,
24+
outboundScidAlias = null,
25+
inboundScidAlias = null,
26+
channelValueSats = 0u,
27+
unspendablePunishmentReserve = null,
28+
userChannelId = "0",
29+
feerateSatPer1000Weight = 0u,
30+
outboundCapacityMsat = 0u,
31+
inboundCapacityMsat = 0u,
32+
confirmationsRequired = null,
33+
confirmations = null,
3434
isOutbound = false,
35-
isChannelReady = isChannelReady,
36-
isUsable = true,
35+
isChannelReady = false,
36+
isUsable = false,
3737
isAnnounced = false,
3838
cltvExpiryDelta = null,
39-
counterpartyUnspendablePunishmentReserve = 0uL,
39+
counterpartyUnspendablePunishmentReserve = 0u,
4040
counterpartyOutboundHtlcMinimumMsat = null,
4141
counterpartyOutboundHtlcMaximumMsat = null,
4242
counterpartyForwardingInfoFeeBaseMsat = null,
4343
counterpartyForwardingInfoFeeProportionalMillionths = null,
4444
counterpartyForwardingInfoCltvExpiryDelta = null,
45-
nextOutboundHtlcLimitMsat = 50_000uL,
46-
nextOutboundHtlcMinimumMsat = 0uL,
45+
nextOutboundHtlcLimitMsat = 0u,
46+
nextOutboundHtlcMinimumMsat = 0u,
4747
forceCloseSpendDelay = null,
48-
inboundHtlcMinimumMsat = 0uL,
48+
inboundHtlcMinimumMsat = 0u,
4949
inboundHtlcMaximumMsat = null,
5050
config = ChannelConfig(
5151
forwardingFeeProportionalMillionths = 0u,
5252
forwardingFeeBaseMsat = 0u,
5353
cltvExpiryDelta = 0u,
54-
maxDustHtlcExposure = MaxDustHtlcExposure.FixedLimit(limitMsat = 0uL),
55-
forceCloseAvoidanceMaxFeeSatoshis = 0uL,
54+
maxDustHtlcExposure = MaxDustHtlcExposure.FixedLimit(limitMsat = 0u),
55+
forceCloseAvoidanceMaxFeeSatoshis = 0u,
5656
acceptUnderpayingHtlcs = false,
5757
),
58-
shortChannelId = 1234uL,
59-
outboundScidAlias = 2345uL,
60-
inboundScidAlias = 3456uL,
6158
)
6259
}
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package to.bitkit.ext
22

3-
inline fun <reified T : Enum<T>> enumValueOfOrNull(name: String): T? {
4-
return try {
5-
enumValueOf<T>(name)
6-
} catch (e: Exception) {
7-
null
3+
inline fun <reified T : Enum<T>> getEnumValueOf(name: String): Result<T> {
4+
return runCatching {
5+
enumValues<T>().first { it.name.equals(name, ignoreCase = true) }
86
}
97
}

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

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,29 @@ import to.bitkit.BuildConfig
66
import to.bitkit.data.ChatwootHttpClient
77
import to.bitkit.di.BgDispatcher
88
import to.bitkit.env.Env
9+
import to.bitkit.ext.getEnumValueOf
910
import to.bitkit.models.ChatwootMessage
1011
import to.bitkit.utils.Logger
11-
import to.bitkit.viewmodels.LogFile
1212
import java.io.BufferedReader
13+
import java.io.ByteArrayOutputStream
1314
import java.io.File
15+
import java.io.FileInputStream
1416
import java.io.FileReader
1517
import java.util.Base64
18+
import java.util.zip.ZipEntry
19+
import java.util.zip.ZipOutputStream
1620
import javax.inject.Inject
1721
import javax.inject.Singleton
1822

1923
@Singleton
2024
class LogsRepo @Inject constructor(
2125
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
2226
private val chatwootHttpClient: ChatwootHttpClient
23-
2427
) {
25-
2628
suspend fun postQuestion(email: String, message: String): Result<Unit> = withContext(bgDispatcher) {
2729
return@withContext try {
28-
29-
val lastLog = getLogs().getOrNull()?.lastOrNull()
30-
val logFile = lastLog?.file
31-
32-
var logsBase64 = ""
33-
var logsFileName = ""
34-
35-
if (logFile != null && logFile.exists()) {
36-
val fileContent = logFile.readBytes()
37-
logsBase64 = Base64.getEncoder().encodeToString(fileContent)
38-
39-
logsFileName = logFile.name.substringBeforeLast(".")
40-
} else {
41-
Logger.warn("No log file found", context = TAG)
42-
}
30+
val logsBase64 = zipLogs().getOrDefault("")
31+
val logsFileName = "bitkit_logs_${System.currentTimeMillis()}.zip"
4332

4433
chatwootHttpClient.postQuestion(
4534
message = ChatwootMessage(
@@ -48,7 +37,7 @@ class LogsRepo @Inject constructor(
4837
platform = "${Env.PLATFORM} ${Env.androidSDKVersion}",
4938
version = "${BuildConfig.VERSION_NAME} ${BuildConfig.VERSION_CODE}",
5039
logs = logsBase64,
51-
logsFileName = logsFileName
40+
logsFileName = logsFileName,
5241
)
5342
)
5443
Result.success(Unit)
@@ -58,6 +47,7 @@ class LogsRepo @Inject constructor(
5847
}
5948
}
6049

50+
/** * Lists log files sorted by newest first */
6151
suspend fun getLogs(): Result<List<LogFile>> = withContext(bgDispatcher) {
6252
try {
6353
val logDir = File(Env.logDir)
@@ -73,13 +63,14 @@ class LogsRepo @Inject constructor(
7363

7464
val serviceName = components.firstOrNull()
7565
?.let { c -> c.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } }
76-
?: "Unknown"
66+
?: LogSource.Unknown.name
7767
val timestamp = if (components.size >= 3) components[components.size - 2] else ""
7868
val displayName = "$serviceName Log: $timestamp"
7969

8070
LogFile(
8171
displayName = displayName,
8272
file = file,
73+
source = getEnumValueOf<LogSource>(serviceName).getOrDefault(LogSource.Unknown),
8374
)
8475
}
8576
?.sortedByDescending { it.file.lastModified() }
@@ -112,7 +103,75 @@ class LogsRepo @Inject constructor(
112103
}
113104
}
114105

106+
/** Zips up the most recent logs and returns base64 of zip file */
107+
suspend fun zipLogs(
108+
limit: Int = 20,
109+
includeAllSources: Boolean = false
110+
): Result<String> = withContext(bgDispatcher) {
111+
return@withContext try {
112+
val logsResult = getLogs()
113+
if (logsResult.isFailure) {
114+
return@withContext Result.failure(logsResult.exceptionOrNull() ?: Exception("Failed to get logs"))
115+
}
116+
117+
val allLogs = logsResult.getOrNull()?.filter { it.source != LogSource.Unknown } ?: emptyList()
118+
val logsToZip = if (includeAllSources) {
119+
allLogs.take(limit)
120+
} else {
121+
// Group by source and take most recent from each
122+
allLogs.groupBy { it.source }
123+
.values
124+
.flatMap { logs ->
125+
val sourcesCount = LogSource.entries.filter { it != LogSource.Unknown }.size
126+
logs.take(limit / sourcesCount.coerceAtLeast(1))
127+
}
128+
.take(limit)
129+
}
130+
131+
if (logsToZip.isEmpty()) {
132+
return@withContext Result.failure(Exception("No log files found"))
133+
}
134+
135+
val base64String = createZipBase64(logsToZip)
136+
Result.success(base64String)
137+
} catch (e: Exception) {
138+
Logger.error("Failed to zip logs", e, context = TAG)
139+
Result.failure(e)
140+
}
141+
}
142+
143+
private fun createZipBase64(logFiles: List<LogFile>): String {
144+
val zipBytes = ByteArrayOutputStream().use { byteArrayOut ->
145+
ZipOutputStream(byteArrayOut).use { zipOut ->
146+
logFiles.forEach { logFile ->
147+
if (logFile.file.exists()) {
148+
val zipEntry = ZipEntry("${logFile.source.name.lowercase()}/${logFile.fileName}")
149+
zipOut.putNextEntry(zipEntry)
150+
151+
FileInputStream(logFile.file).use { fileIn ->
152+
fileIn.copyTo(zipOut)
153+
}
154+
zipOut.closeEntry()
155+
}
156+
}
157+
}
158+
byteArrayOut.toByteArray()
159+
}
160+
161+
return Base64.getEncoder().encodeToString(zipBytes)
162+
}
163+
115164
private companion object {
116165
const val TAG = "SupportRepo"
117166
}
118167
}
168+
169+
data class LogFile(
170+
val displayName: String,
171+
val file: File,
172+
val source: LogSource,
173+
) {
174+
val fileName: String get() = file.name
175+
}
176+
177+
enum class LogSource { Ldk, Bitkit, Unknown }

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,11 @@ fun List<ChannelDetails>.filterOpen(): List<ChannelDetails> {
578578
return this.filter { it.isChannelReady }
579579
}
580580

581+
/** Returns only `pending` channels. */
582+
fun List<ChannelDetails>.filterPending(): List<ChannelDetails> {
583+
return this.filterNot { it.isChannelReady }
584+
}
585+
581586
private fun generateLogFilePath(): String {
582587
val dateFormatter = SimpleDateFormat(DatePattern.LOG_FILE, Locale.US).apply {
583588
timeZone = TimeZone.getTimeZone("UTC")

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ import to.bitkit.ui.settings.BlocktankRegtestScreen
9797
import to.bitkit.ui.settings.BlocktankRegtestViewModel
9898
import to.bitkit.ui.settings.CJitDetailScreen
9999
import to.bitkit.ui.settings.ChannelOrdersScreen
100-
import to.bitkit.ui.settings.LightningSettingsScreen
101100
import to.bitkit.ui.settings.LogDetailScreen
102101
import to.bitkit.ui.settings.LogsScreen
103102
import to.bitkit.ui.settings.OrderDetailScreen
@@ -120,6 +119,9 @@ import to.bitkit.ui.settings.general.GeneralSettingsScreen
120119
import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen
121120
import to.bitkit.ui.settings.general.TagsSettingsScreen
122121
import to.bitkit.ui.settings.general.WidgetsSettingsScreen
122+
import to.bitkit.ui.settings.lightning.ChannelDetailScreen
123+
import to.bitkit.ui.settings.lightning.LightningConnectionsScreen
124+
import to.bitkit.ui.settings.lightning.LightningConnectionsViewModel
123125
import to.bitkit.ui.settings.pin.ChangePinConfirmScreen
124126
import to.bitkit.ui.settings.pin.ChangePinNewScreen
125127
import to.bitkit.ui.settings.pin.ChangePinResultScreen
@@ -370,6 +372,7 @@ fun ContentView(
370372
BottomSheetType.BackupNavigation -> BackupNavigationSheet(
371373
onDismiss = { appViewModel.hideSheet() },
372374
)
375+
373376
null -> Unit
374377
}
375378
}
@@ -423,7 +426,7 @@ private fun RootNavHost(
423426
channelOrdersSettings(navController)
424427
orderDetailSettings(navController)
425428
cjitDetailSettings(navController)
426-
lightningConnections(walletViewModel, navController)
429+
lightningConnections(navController)
427430
devSettings(walletViewModel, navController)
428431
regtestSettings(navController)
429432
activityItem(activityListViewModel, navController)
@@ -897,11 +900,24 @@ private fun NavGraphBuilder.cjitDetailSettings(
897900
}
898901

899902
private fun NavGraphBuilder.lightningConnections(
900-
viewModel: WalletViewModel,
901903
navController: NavHostController,
902904
) {
903-
composableWithDefaultTransitions<Routes.LightningConnections> {
904-
LightningSettingsScreen(viewModel, navController)
905+
navigation<Routes.ConnectionsNav>(
906+
startDestination = Routes.LightningConnections,
907+
) {
908+
composableWithDefaultTransitions<Routes.LightningConnections> {
909+
val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ConnectionsNav) }
910+
val viewModel = hiltViewModel<LightningConnectionsViewModel>(parentEntry)
911+
LightningConnectionsScreen(navController, viewModel)
912+
}
913+
composableWithDefaultTransitions<Routes.ChannelDetail> {
914+
val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ConnectionsNav) }
915+
val viewModel = hiltViewModel<LightningConnectionsViewModel>(parentEntry)
916+
ChannelDetailScreen(
917+
navController = navController,
918+
viewModel = viewModel,
919+
)
920+
}
905921
}
906922
}
907923

@@ -1492,9 +1508,15 @@ object Routes {
14921508
@Serializable
14931509
data class CjitDetail(val id: String)
14941510

1511+
@Serializable
1512+
data object ConnectionsNav
1513+
14951514
@Serializable
14961515
data object LightningConnections
14971516

1517+
@Serializable
1518+
data object ChannelDetail
1519+
14981520
@Serializable
14991521
data object DevSettings
15001522

@@ -1657,7 +1679,6 @@ object Routes {
16571679
@Serializable
16581680
data object PriceEdit
16591681

1660-
16611682
@Serializable
16621683
data object CalculatorPreview
16631684
}

app/src/main/java/to/bitkit/ui/components/LightningChannel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ private fun PreviewChannelPending() {
172172
private fun PreviewChannelClosed() {
173173
AppThemeSurface {
174174
LightningChannel(
175-
capacity = 1_000_000,
176-
localBalance = 0,
177-
remoteBalance = 0,
175+
capacity = 500_000,
176+
localBalance = 400_000,
177+
remoteBalance = 100_000,
178178
status = ChannelStatusUi.CLOSED,
179179
showLabels = true,
180180
modifier = Modifier.padding(16.dp)

0 commit comments

Comments
 (0)