Skip to content

Commit 3a38977

Browse files
committed
feat: sync transfers on events
1 parent caed7eb commit 3a38977

File tree

9 files changed

+66
-109
lines changed

9 files changed

+66
-109
lines changed

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

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.lightningdevkit.ldknode.BalanceDetails
3030
import org.lightningdevkit.ldknode.BestBlock
3131
import org.lightningdevkit.ldknode.ChannelConfig
3232
import org.lightningdevkit.ldknode.ChannelDetails
33+
import org.lightningdevkit.ldknode.ClosureReason
3334
import org.lightningdevkit.ldknode.Event
3435
import org.lightningdevkit.ldknode.NodeStatus
3536
import org.lightningdevkit.ldknode.PaymentDetails
@@ -62,7 +63,6 @@ import to.bitkit.utils.AppError
6263
import to.bitkit.utils.Logger
6364
import to.bitkit.utils.ServiceError
6465
import to.bitkit.utils.errLogOf
65-
import to.bitkit.utils.measured
6666
import java.util.concurrent.ConcurrentHashMap
6767
import java.util.concurrent.atomic.AtomicBoolean
6868
import javax.inject.Inject
@@ -317,23 +317,19 @@ class LightningRepo @Inject constructor(
317317
return@executeWhenNodeRunning Result.success(Unit)
318318
}
319319

320-
Logger.debug("Sync started", context = TAG)
321320
try {
322-
measured("Sync") {
323-
do {
324-
syncPending.set(false)
325-
_lightningState.update { it.copy(isSyncingWallet = true) }
326-
lightningService.sync()
327-
refreshChannelCache()
328-
syncState()
329-
if (syncPending.get()) delay(SYNC_LOOP_DEBOUNCE_MS)
330-
} while (syncPending.getAndSet(false))
331-
}
321+
do {
322+
syncPending.set(false)
323+
_lightningState.update { it.copy(isSyncingWallet = true) }
324+
lightningService.sync()
325+
refreshChannelCache()
326+
syncState()
327+
if (syncPending.get()) delay(SYNC_LOOP_DEBOUNCE_MS)
328+
} while (syncPending.getAndSet(false))
332329
} finally {
333330
_lightningState.update { it.copy(isSyncingWallet = false) }
334331
syncMutex.unlock()
335332
}
336-
Logger.debug("Sync completed", context = TAG)
337333

338334
Result.success(Unit)
339335
}
@@ -352,39 +348,25 @@ class LightningRepo @Inject constructor(
352348

353349
private fun handleLdkEvent(event: Event) {
354350
when (event) {
355-
is Event.ChannelPending -> {
356-
scope.launch {
357-
refreshChannelCache()
358-
}
359-
}
360-
361-
is Event.ChannelReady -> {
362-
scope.launch {
363-
refreshChannelCache()
364-
}
365-
}
366-
367-
is Event.ChannelClosed -> {
368-
val channelId = event.channelId
369-
val reason = event.reason?.toString() ?: ""
370-
scope.launch {
371-
registerClosedChannel(channelId, reason)
372-
}
351+
is Event.ChannelPending,
352+
is Event.ChannelReady,
353+
-> scope.launch { refreshChannelCache() }
354+
355+
is Event.ChannelClosed -> scope.launch {
356+
registerClosedChannel(
357+
channelId = event.channelId,
358+
reason = event.reason,
359+
)
373360
}
374361

375-
else -> {
376-
// Other events don't need special handling
377-
}
362+
else -> Unit // Other events don't need special handling
378363
}
379364
}
380365

381-
private suspend fun registerClosedChannel(channelId: String, reason: String?) = withContext(bgDispatcher) {
366+
private suspend fun registerClosedChannel(channelId: String, reason: ClosureReason?) = withContext(bgDispatcher) {
382367
try {
383368
val channel = channelCache[channelId] ?: run {
384-
Logger.error(
385-
"Could not find channel details for closed channel: channelId=$channelId",
386-
context = TAG
387-
)
369+
Logger.error("Could not find channel details for closed channel: channelId=$channelId", context = TAG)
388370
return@withContext
389371
}
390372

@@ -416,7 +398,7 @@ class LightningRepo @Inject constructor(
416398
forwardingFeeProportionalMillionths = channel.config.forwardingFeeProportionalMillionths,
417399
forwardingFeeBaseMsat = channel.config.forwardingFeeBaseMsat,
418400
channelName = channelName,
419-
channelClosureReason = reason.orEmpty()
401+
channelClosureReason = reason?.toString().orEmpty(),
420402
)
421403

422404
coreService.activity.upsertClosedChannelList(listOf(closedChannel))

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ class TransferRepo @Inject constructor(
102102
Logger.debug("Channel $channelId balance swept, settled transfer: ${transfer.id}", context = TAG)
103103
}
104104
}
105+
}.onSuccess {
106+
Logger.verbose("syncTransferStates completed", context = TAG)
105107
}.onFailure { e ->
106-
Logger.error("Failed to sync transfer states", e, context = TAG)
108+
Logger.error("syncTransferStates error", e, context = TAG)
107109
}
108110
}
109111

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import to.bitkit.utils.Bip21Utils
3636
import to.bitkit.utils.Logger
3737
import to.bitkit.utils.ServiceError
3838
import to.bitkit.utils.errLogOf
39+
import to.bitkit.utils.measured
3940
import javax.inject.Inject
4041
import javax.inject.Singleton
4142
import kotlin.coroutines.cancellation.CancellationException
@@ -52,6 +53,7 @@ class WalletRepo @Inject constructor(
5253
private val preActivityMetadataRepo: PreActivityMetadataRepo,
5354
private val deriveBalanceStateUseCase: DeriveBalanceStateUseCase,
5455
private val wipeWalletUseCase: WipeWalletUseCase,
56+
private val transferRepo: TransferRepo,
5557
) {
5658
private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob())
5759

@@ -166,25 +168,31 @@ class WalletRepo @Inject constructor(
166168
preActivityMetadataRepo.addPreActivityMetadata(preActivityMetadata)
167169
}
168170

169-
suspend fun syncNodeAndWallet(): Result<Unit> = withContext(bgDispatcher) {
171+
suspend fun syncNodeAndWallet(source: SyncSource = SyncSource.AUTO): Result<Unit> = withContext(bgDispatcher) {
170172
if (!lightningRepo.lightningState.value.nodeLifecycleState.isRunning()) {
171173
Logger.debug("syncNodeAndWallet skipped: node not running", context = TAG)
172174
return@withContext Result.failure(Exception("Node not running"))
173175
}
176+
177+
val sourceLabel = source.name.lowercase()
174178
val startHeight = lightningRepo.lightningState.value.block()?.height
175-
Logger.verbose("syncNodeAndWallet started at block height=$startHeight", context = TAG)
176-
syncBalances()
177-
lightningRepo.sync().onSuccess {
179+
Logger.debug("Sync $sourceLabel started at block height=$startHeight", context = TAG)
180+
181+
val result = measured("Sync $sourceLabel") {
178182
syncBalances()
179-
val endHeight = lightningRepo.lightningState.value.block()?.height
180-
Logger.verbose("syncNodeAndWallet completed at block height=$endHeight", context = TAG)
181-
return@withContext Result.success(Unit)
182-
}.onFailure { e ->
183-
if (e is TimeoutCancellationException) {
183+
lightningRepo.sync().onSuccess {
184184
syncBalances()
185+
}.onFailure { e ->
186+
if (e is TimeoutCancellationException) {
187+
syncBalances()
188+
}
185189
}
186-
return@withContext Result.failure(e)
187190
}
191+
192+
val endHeight = lightningRepo.lightningState.value.block()?.height
193+
Logger.debug("Sync $sourceLabel completed at block height=$endHeight", context = TAG)
194+
195+
result
188196
}
189197

190198
suspend fun syncBalances() {
@@ -204,6 +212,7 @@ class WalletRepo @Inject constructor(
204212
eventSyncJob = repoScope.launch {
205213
delay(EVENT_SYNC_DEBOUNCE_MS)
206214
syncNodeAndWallet()
215+
transferRepo.syncTransferStates()
207216
}
208217
}
209218

@@ -611,3 +620,5 @@ data class WalletState(
611620
val receiveOnSpendingBalance: Boolean = true,
612621
val walletExists: Boolean = false,
613622
)
623+
624+
enum class SyncSource { AUTO, MANUAL }

app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import androidx.compose.runtime.remember
1111
import androidx.compose.ui.Modifier
1212
import androidx.compose.ui.platform.LocalContext
1313
import androidx.compose.ui.platform.testTag
14-
import androidx.hilt.navigation.compose.hiltViewModel
14+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
1515
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1616
import androidx.navigation.compose.NavHost
1717
import androidx.navigation.compose.rememberNavController
@@ -25,7 +25,6 @@ import to.bitkit.ui.walletViewModel
2525
import to.bitkit.viewmodels.AmountInputViewModel
2626
import to.bitkit.viewmodels.MainUiState
2727
import to.bitkit.viewmodels.SettingsViewModel
28-
import to.bitkit.viewmodels.WalletViewModelEffects
2928

3029
@Composable
3130
fun ReceiveSheet(
@@ -65,16 +64,6 @@ fun ReceiveSheet(
6564
showCreateCjit.value = !cjitInvoice.value.isNullOrBlank()
6665
}
6766

68-
LaunchedEffect(Unit) {
69-
wallet.walletEffect.collect { effect ->
70-
when (effect) {
71-
WalletViewModelEffects.NavigateGeoBlockScreen -> {
72-
navController.navigate(ReceiveRoute.GeoBlock)
73-
}
74-
}
75-
}
76-
}
77-
7867
ReceiveQrScreen(
7968
cjitInvoice = cjitInvoice.value,
8069
walletState = walletState,

app/src/main/java/to/bitkit/utils/Perf.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@ import java.time.Instant
44
import kotlin.system.measureTimeMillis
55

66
internal inline fun <T> measured(
7-
functionName: String,
7+
label: String,
88
block: () -> T,
99
): T {
1010
var result: T
1111

12-
val elapsed = measureTimeMillis {
12+
val elapsedMs = measureTimeMillis {
1313
result = block()
14-
}.let { it / 1000.0 }
14+
}
1515

16-
val threadName = Thread.currentThread().name
17-
Logger.performance("$functionName took $elapsed seconds on $threadName")
16+
Logger.debug("$label took ${elapsedMs}ms")
1817

1918
return result
2019
}

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ import kotlin.time.ExperimentalTime
117117
class AppViewModel @Inject constructor(
118118
connectivityRepo: ConnectivityRepo,
119119
healthRepo: HealthRepo,
120-
@param:ApplicationContext private val context: Context,
121-
@param:BgDispatcher private val bgDispatcher: CoroutineDispatcher,
120+
toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager,
121+
@ApplicationContext private val context: Context,
122+
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
122123
private val keychain: Keychain,
123124
private val lightningRepo: LightningRepo,
124125
private val walletRepo: WalletRepo,
@@ -132,7 +133,6 @@ class AppViewModel @Inject constructor(
132133
private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler,
133134
private val cacheStore: CacheStore,
134135
private val transferRepo: TransferRepo,
135-
private val toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager,
136136
) : ViewModel() {
137137
val healthState = healthRepo.healthState
138138

@@ -272,7 +272,10 @@ class AppViewModel @Inject constructor(
272272
}
273273
}
274274

275-
private suspend fun handleBalanceChanged() = walletRepo.syncBalances()
275+
private suspend fun handleBalanceChanged() {
276+
walletRepo.syncBalances()
277+
transferRepo.syncTransferStates()
278+
}
276279

277280
private suspend fun handleChannelReady(event: Event.ChannelReady) {
278281
transferRepo.syncTransferStates()

app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@ import androidx.lifecycle.viewModelScope
88
import dagger.hilt.android.lifecycle.HiltViewModel
99
import kotlinx.coroutines.CoroutineDispatcher
1010
import kotlinx.coroutines.Job
11-
import kotlinx.coroutines.TimeoutCancellationException
1211
import kotlinx.coroutines.delay
13-
import kotlinx.coroutines.flow.MutableSharedFlow
1412
import kotlinx.coroutines.flow.MutableStateFlow
15-
import kotlinx.coroutines.flow.asSharedFlow
1613
import kotlinx.coroutines.flow.asStateFlow
1714
import kotlinx.coroutines.flow.first
1815
import kotlinx.coroutines.flow.map
@@ -29,11 +26,11 @@ import to.bitkit.repositories.BackupRepo
2926
import to.bitkit.repositories.BlocktankRepo
3027
import to.bitkit.repositories.LightningRepo
3128
import to.bitkit.repositories.RecoveryModeException
29+
import to.bitkit.repositories.SyncSource
3230
import to.bitkit.repositories.WalletRepo
3331
import to.bitkit.ui.onboarding.LOADING_MS
3432
import to.bitkit.ui.shared.toast.ToastEventBus
3533
import to.bitkit.utils.Logger
36-
import to.bitkit.utils.ServiceError
3734
import javax.inject.Inject
3835
import kotlin.coroutines.cancellation.CancellationException
3936
import kotlin.time.Duration.Companion.milliseconds
@@ -66,10 +63,6 @@ class WalletViewModel @Inject constructor(
6663
@Deprecated("Prioritize get the wallet and lightning states from LightningRepo or WalletRepo")
6764
val uiState = _uiState.asStateFlow()
6865

69-
private val _walletEffect = MutableSharedFlow<WalletViewModelEffects>(extraBufferCapacity = 1)
70-
val walletEffect = _walletEffect.asSharedFlow()
71-
private fun walletEffect(effect: WalletViewModelEffects) = viewModelScope.launch { _walletEffect.emit(effect) }
72-
7366
private var syncJob: Job? = null
7467

7568
init {
@@ -146,7 +139,7 @@ class WalletViewModel @Inject constructor(
146139
.onSuccess {
147140
walletRepo.setWalletExistsState()
148141
walletRepo.syncBalances()
149-
// Skip refreshing during restore, it will be called when it completes
142+
// Skip refresh during restore, it will be called after completion
150143
if (restoreState.isIdle()) {
151144
walletRepo.refreshBip21()
152145
}
@@ -177,9 +170,7 @@ class WalletViewModel @Inject constructor(
177170
.onFailure { error ->
178171
if (error is CancellationException) return@onFailure
179172
Logger.error("Failed to refresh state: ${error.message}", error)
180-
if (error !is TimeoutCancellationException) {
181-
ToastEventBus.send(error)
182-
}
173+
ToastEventBus.send(error)
183174
}
184175
}
185176

@@ -192,7 +183,7 @@ class WalletViewModel @Inject constructor(
192183
syncJob = viewModelScope.launch {
193184
_uiState.update { it.copy(isRefreshing = true) }
194185
try {
195-
walletRepo.syncNodeAndWallet()
186+
walletRepo.syncNodeAndWallet(source = SyncSource.MANUAL)
196187
} finally {
197188
_uiState.update { it.copy(isRefreshing = false) }
198189
}
@@ -233,21 +224,6 @@ class WalletViewModel @Inject constructor(
233224
}
234225
}
235226

236-
fun toggleReceiveOnSpending() {
237-
viewModelScope.launch {
238-
walletRepo.toggleReceiveOnSpendingBalance()
239-
.onSuccess {
240-
updateBip21Invoice()
241-
}.onFailure { e ->
242-
if (e is ServiceError.GeoBlocked) {
243-
walletEffect(WalletViewModelEffects.NavigateGeoBlockScreen)
244-
return@launch
245-
}
246-
updateBip21Invoice()
247-
}
248-
}
249-
}
250-
251227
fun refreshReceiveState() = viewModelScope.launch {
252228
launch { blocktankRepo.refreshInfo() }
253229
lightningRepo.updateGeoBlockState()
@@ -303,10 +279,6 @@ class WalletViewModel @Inject constructor(
303279
walletRepo.resetPreActivityMetadataTagsForCurrentInvoice()
304280
}
305281

306-
fun loadTagsForCurrentInvoice() = viewModelScope.launch {
307-
walletRepo.loadTagsForCurrentInvoice()
308-
}
309-
310282
fun updateBip21Description(newText: String) {
311283
if (newText.isEmpty()) {
312284
Logger.warn("Empty")
@@ -339,10 +311,6 @@ data class MainUiState(
339311
val selectedTags: List<String> = listOf(),
340312
)
341313

342-
sealed interface WalletViewModelEffects {
343-
data object NavigateGeoBlockScreen : WalletViewModelEffects
344-
}
345-
346314
sealed interface RestoreState {
347315
data object Initial : RestoreState
348316
sealed interface InProgress : RestoreState {

0 commit comments

Comments
 (0)