@@ -58,10 +58,12 @@ import to.bitkit.data.SettingsStore
5858import to.bitkit.data.backup.VssBackupClientLdk
5959import to.bitkit.data.keychain.Keychain
6060import to.bitkit.di.BgDispatcher
61+ import to.bitkit.env.Defaults
6162import to.bitkit.env.Env
6263import to.bitkit.ext.getSatsPerVByteFor
6364import to.bitkit.ext.nowTimestamp
6465import to.bitkit.ext.toPeerDetailsList
66+ import to.bitkit.ext.totalNextOutboundHtlcLimitSats
6567import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS
6668import to.bitkit.models.CoinSelectionPreference
6769import to.bitkit.models.NATIVE_WITNESS_TYPES
@@ -93,6 +95,7 @@ import javax.inject.Inject
9395import javax.inject.Singleton
9496import kotlin.coroutines.cancellation.CancellationException
9597import kotlin.time.Duration
98+ import kotlin.time.Duration.Companion.milliseconds
9699import kotlin.time.Duration.Companion.minutes
97100import 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 }
0 commit comments