Skip to content

Commit 4599e58

Browse files
authored
Merge pull request #878 from synonymdev/feat/connection-issues-view
feat: connection issues view
2 parents 4fa5787 + 55341fd commit 4599e58

31 files changed

Lines changed: 1318 additions & 618 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
- Fix Spending and Savings screens scrolling behind top bar and add gradient fade effect #892
910

1011
### Changed
1112
- Improve Pubky profile restore, contact editing, and contact routing flows #905
@@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2526
- Show loading state on Spending tab when node is not running #875
2627

2728
### Added
29+
- Transfer from Savings button on empty Spending screen when savings balance exists #882
2830
- Pubky profile onboarding with contact sync, import, and editing #824
2931
- Lightning Connections empty state with onboarding screen #857
3032
- Unified PIN management screen (enable/disable/change in one place) #857

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ internal object Env {
246246
@Suppress("ConstPropertyName")
247247
object Defaults {
248248
/** Default Bolt11 invoice expiry in seconds. */
249-
const val bolt11InvoiceExpirySeconds = 3_600u
249+
const val bolt11ExpirySec = 86_400u
250250

251251
/** Recommended transaction base fee in sats */
252252
const val recommendedBaseFee = 256u

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import org.lightningdevkit.ldknode.ChannelDetails
4141
import to.bitkit.async.ServiceQueue
4242
import to.bitkit.data.CacheStore
4343
import to.bitkit.di.BgDispatcher
44-
import to.bitkit.env.Defaults
4544
import to.bitkit.env.Env
4645
import to.bitkit.ext.calculateRemoteBalance
4746
import to.bitkit.ext.nowTimestamp
@@ -57,6 +56,7 @@ import javax.inject.Named
5756
import javax.inject.Singleton
5857
import kotlin.math.ceil
5958
import kotlin.time.Duration
59+
import kotlin.time.Duration.Companion.hours
6060
import kotlin.time.Duration.Companion.seconds
6161

6262
@Singleton
@@ -463,7 +463,7 @@ class BlocktankRepo @Inject constructor(
463463
val invoice = lightningRepo.createInvoice(
464464
amountSats = null,
465465
description = "blocktank-gift-code:$code",
466-
expirySeconds = Defaults.bolt11InvoiceExpirySeconds,
466+
expirySeconds = 1.hours.inWholeSeconds.toUInt(),
467467
).getOrThrow()
468468

469469
Logger.debug("Created invoice for gift code, requesting payment from LSP", context = TAG)

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

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ import to.bitkit.data.SettingsStore
5858
import to.bitkit.data.backup.VssBackupClientLdk
5959
import to.bitkit.data.keychain.Keychain
6060
import to.bitkit.di.BgDispatcher
61+
import to.bitkit.env.Defaults
6162
import to.bitkit.env.Env
6263
import to.bitkit.ext.getSatsPerVByteFor
6364
import to.bitkit.ext.nowTimestamp
6465
import to.bitkit.ext.toPeerDetailsList
66+
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
6567
import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS
6668
import to.bitkit.models.CoinSelectionPreference
6769
import to.bitkit.models.NATIVE_WITNESS_TYPES
@@ -93,6 +95,7 @@ import javax.inject.Inject
9395
import javax.inject.Singleton
9496
import kotlin.coroutines.cancellation.CancellationException
9597
import kotlin.time.Duration
98+
import kotlin.time.Duration.Companion.milliseconds
9699
import kotlin.time.Duration.Companion.minutes
97100
import kotlin.time.Duration.Companion.seconds
98101

@@ -151,6 +154,10 @@ class LightningRepo @Inject constructor(
151154
return@collect
152155
}
153156

157+
if (_lightningState.value.nodeLifecycleState.isRunning()) {
158+
connectToTrustedPeers()
159+
}
160+
154161
// Start retry loop if sync is failing
155162
startSyncRetryLoopIfNeeded()
156163
}
@@ -533,7 +540,11 @@ class LightningRepo @Inject constructor(
533540

534541
private fun handleLdkEvent(event: Event) {
535542
when (event) {
536-
is Event.ChannelPending, is Event.ChannelReady -> scope.launch { refreshChannelCache() }
543+
is Event.ChannelPending, is Event.ChannelReady -> scope.launch {
544+
refreshChannelCache()
545+
syncState()
546+
}
547+
537548
is Event.ChannelClosed -> scope.launch { registerClosedChannel(event.channelId, event.reason) }
538549
else -> Unit
539550
}
@@ -917,7 +928,7 @@ class LightningRepo @Inject constructor(
917928
suspend fun createInvoice(
918929
amountSats: ULong? = null,
919930
description: String,
920-
expirySeconds: UInt = 86_400u,
931+
expirySeconds: UInt = Defaults.bolt11ExpirySec,
921932
): Result<String> = executeWhenNodeRunning("createInvoice") {
922933
updateGeoBlockState()
923934
runCatching { lightningService.receive(amountSats, description, expirySeconds) }
@@ -926,7 +937,7 @@ class LightningRepo @Inject constructor(
926937
suspend fun createInvoiceMsats(
927938
amountMsats: ULong,
928939
description: String,
929-
expirySeconds: UInt = 86_400u,
940+
expirySeconds: UInt = Defaults.bolt11ExpirySec,
930941
): Result<String> = executeWhenNodeRunning("createInvoiceMsats") {
931942
updateGeoBlockState()
932943
runCatching { lightningService.receiveMsats(amountMsats, description, expirySeconds) }
@@ -1009,15 +1020,69 @@ class LightningRepo @Inject constructor(
10091020
}
10101021
}
10111022

1012-
private suspend fun waitForUsableChannels() {
1013-
if (lightningService.channels?.any { it.isUsable } == true) return
1023+
suspend fun waitForUsableChannels() = withContext(bgDispatcher) {
1024+
var state = _lightningState.value
1025+
if (!state.nodeLifecycleState.canRun()) {
1026+
delayNoUsableChannelsFeedback()
1027+
return@withContext
1028+
}
1029+
if (state.hasUsableChannels()) return@withContext
1030+
1031+
state = waitForChannelsToLoadIfNeeded(state) ?: return@withContext
1032+
if (!state.nodeLifecycleState.canRun()) {
1033+
delayNoUsableChannelsFeedback()
1034+
return@withContext
1035+
}
1036+
1037+
if (state.channels.isEmpty()) {
1038+
if (state.nodeLifecycleState.isRunning()) {
1039+
syncState()
1040+
state = _lightningState.value
1041+
}
1042+
1043+
if (state.channels.isEmpty()) {
1044+
delayNoUsableChannelsFeedback()
1045+
return@withContext
1046+
}
1047+
if (state.hasUsableChannels()) return@withContext
1048+
}
10141049

10151050
Logger.info("Waiting for usable channels before sending payment", context = TAG)
1016-
syncState()
10171051

1018-
withTimeoutOrNull(CHANNELS_USABLE_TIMEOUT_MS) {
1019-
_lightningState.first { state -> state.channels.any { it.isUsable } }
1020-
} ?: Logger.warn("Timeout waiting for usable channels", context = TAG)
1052+
val finalState = withTimeoutOrNull(CHANNELS_USABLE_TIMEOUT) {
1053+
_lightningState.first { it.shouldStopWaitingForUsableChannels() }
1054+
} ?: run {
1055+
Logger.warn("Timed out waiting for usable channels", context = TAG)
1056+
return@withContext
1057+
}
1058+
1059+
if (!finalState.nodeLifecycleState.canRun() || finalState.channels.isEmpty()) {
1060+
delayNoUsableChannelsFeedback()
1061+
}
1062+
}
1063+
1064+
private suspend fun waitForChannelsToLoadIfNeeded(state: LightningState): LightningState? {
1065+
if (state.channels.isNotEmpty() || state.nodeLifecycleState.isRunning()) return state
1066+
1067+
Logger.info("Waiting for node to load channels before sending payment", context = TAG)
1068+
return withTimeoutOrNull(CHANNELS_USABLE_TIMEOUT) {
1069+
_lightningState.first { it.shouldStopWaitingForLoadedChannels() }
1070+
} ?: run {
1071+
Logger.warn("Timed out waiting for node to load channels", context = TAG)
1072+
null
1073+
}
1074+
}
1075+
1076+
private fun LightningState.hasUsableChannels() = channels.any { it.isUsable }
1077+
1078+
private fun LightningState.shouldStopWaitingForLoadedChannels() =
1079+
!nodeLifecycleState.canRun() || nodeLifecycleState.isRunning() || channels.isNotEmpty()
1080+
1081+
private fun LightningState.shouldStopWaitingForUsableChannels() =
1082+
!nodeLifecycleState.canRun() || channels.isEmpty() || hasUsableChannels()
1083+
1084+
private suspend fun delayNoUsableChannelsFeedback() {
1085+
delay(NO_USABLE_CHANNELS_FEEDBACK_DELAY)
10211086
}
10221087

10231088
@Suppress("LongParameterList")
@@ -1229,19 +1294,20 @@ class LightningRepo @Inject constructor(
12291294
}
12301295
}
12311296

1232-
suspend fun canSend(amountSats: ULong, fallbackToCachedBalance: Boolean = true) = withContext(bgDispatcher) {
1233-
if (!_lightningState.value.nodeLifecycleState.canRun()) {
1234-
return@withContext false
1235-
}
1236-
if (_lightningState.value.nodeLifecycleState.isStarting() && fallbackToCachedBalance) {
1237-
return@withContext amountSats <= (cacheStore.data.first().balance?.maxSendLightningSats ?: 0u)
1238-
}
1239-
if (lightningService.channels == null) {
1240-
withTimeoutOrNull(CHANNELS_READY_TIMEOUT_MS) {
1241-
_lightningState.first { lightningService.channels != null }
1297+
suspend fun awaitPeerConnected(timeout: Duration = 30.seconds) = withContext(bgDispatcher) {
1298+
if (lightningService.peers?.any { it.isConnected } == true) return@withContext
1299+
Logger.debug("Waiting for peer to reconnect (timeout='$timeout')...", context = TAG)
1300+
withTimeoutOrNull(timeout) {
1301+
while (lightningService.peers?.any { it.isConnected } != true) {
1302+
delay(1.seconds)
12421303
}
12431304
}
1244-
return@withContext lightningService.canSend(amountSats)
1305+
}
1306+
1307+
fun canSend(amountSats: ULong): Boolean {
1308+
val state = _lightningState.value
1309+
if (!state.nodeLifecycleState.canRun()) return false
1310+
return state.channels.totalNextOutboundHtlcLimitSats() >= amountSats
12451311
}
12461312

12471313
fun getNodeId(): String? =
@@ -1478,8 +1544,8 @@ class LightningRepo @Inject constructor(
14781544
private const val LENGTH_CHANNEL_ID_PREVIEW = 10
14791545
private const val MS_SYNC_LOOP_DEBOUNCE = 500L
14801546
private const val SYNC_RETRY_DELAY_MS = 15_000L
1481-
private const val CHANNELS_READY_TIMEOUT_MS = 15_000L
1482-
private const val CHANNELS_USABLE_TIMEOUT_MS = 15_000L
1547+
private val CHANNELS_USABLE_TIMEOUT = 15.seconds
1548+
private val NO_USABLE_CHANNELS_FEEDBACK_DELAY = 2_500.milliseconds
14831549
val SEND_LN_TIMEOUT = 10.seconds
14841550
private val PROBE_TIMEOUT = 60.seconds
14851551
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import org.lightningdevkit.ldknode.TransactionDetails
7070
import to.bitkit.async.ServiceQueue
7171
import to.bitkit.data.CacheStore
7272
import to.bitkit.data.SettingsStore
73+
import to.bitkit.env.Defaults
7374
import to.bitkit.env.Env
7475
import to.bitkit.ext.amountSats
7576
import to.bitkit.ext.channelId
@@ -1523,11 +1524,15 @@ class BlocktankService(
15231524
)
15241525
}
15251526

1526-
suspend fun regtestCloseChannel(fundingTxId: String, vout: UInt, forceCloseAfterS: ULong = 86_400uL): String {
1527+
suspend fun regtestCloseChannel(
1528+
fundingTxId: String,
1529+
vout: UInt,
1530+
forceCloseAfterS: UInt = Defaults.bolt11ExpirySec,
1531+
): String {
15271532
return com.synonym.bitkitcore.regtestCloseChannel(
15281533
fundingTxId = fundingTxId,
15291534
vout = vout,
1530-
forceCloseAfterS = forceCloseAfterS,
1535+
forceCloseAfterS = forceCloseAfterS.toULong(),
15311536
)
15321537
}
15331538
}

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

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import to.bitkit.data.keychain.Keychain
4848
import to.bitkit.di.BgDispatcher
4949
import to.bitkit.env.Defaults
5050
import to.bitkit.env.Env
51-
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
5251
import to.bitkit.ext.uByteList
5352
import to.bitkit.ext.uri
5453
import to.bitkit.models.OpenChannelResult
@@ -596,15 +595,15 @@ class LightningService @Inject constructor(
596595
suspend fun receive(
597596
sat: ULong? = null,
598597
description: String,
599-
expirySecs: UInt = Defaults.bolt11InvoiceExpirySeconds,
598+
expirySecs: UInt = Defaults.bolt11ExpirySec,
600599
): String {
601600
return receiveMsats(amountMsat = sat?.let { it * 1000u }, description = description, expirySecs = expirySecs)
602601
}
603602

604603
suspend fun receiveMsats(
605604
amountMsat: ULong? = null,
606605
description: String,
607-
expirySecs: UInt = Defaults.bolt11InvoiceExpirySeconds,
606+
expirySecs: UInt = Defaults.bolt11ExpirySec,
608607
): String {
609608
val node = this.node ?: throw ServiceError.NodeNotSetup()
610609

@@ -630,23 +629,6 @@ class LightningService @Inject constructor(
630629
}
631630
}
632631

633-
fun canSend(amountSats: ULong): Boolean {
634-
val channels = this.channels
635-
if (channels == null) {
636-
Logger.warn("Channels not available", context = TAG)
637-
return false
638-
}
639-
640-
val totalNextOutboundHtlcLimitSats = channels.totalNextOutboundHtlcLimitSats()
641-
642-
if (totalNextOutboundHtlcLimitSats < amountSats) {
643-
Logger.warn("Insufficient outbound capacity: $totalNextOutboundHtlcLimitSats < $amountSats", context = TAG)
644-
return false
645-
}
646-
647-
return true
648-
}
649-
650632
suspend fun send(
651633
address: Address,
652634
sats: ULong,

0 commit comments

Comments
 (0)