From c9c4257dd6509bde1be3f448b1b2a97f049c526c Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 10:42:58 -0300 Subject: [PATCH 1/2] feat: wrap method in executeWhenNodeRunning --- .../to/bitkit/models/NodeLifecycleState.kt | 1 + .../to/bitkit/repositories/LightningRepo.kt | 214 ++++++++++-------- 2 files changed, 118 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt b/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt index bd7b13073..5b6c66fc7 100644 --- a/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt +++ b/app/src/main/java/to/bitkit/models/NodeLifecycleState.kt @@ -12,6 +12,7 @@ sealed class NodeLifecycleState { fun isRunningOrStarting() = this is Running || this is Starting fun isStarting() = this is Starting fun isRunning() = this is Running + fun canRun() = this.isRunningOrStarting() || this is Initializing val displayState: String get() = when (this) { diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 5692a401f..6e3735154 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -4,7 +4,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.Address import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.Bolt11Invoice @@ -25,6 +27,7 @@ import to.bitkit.utils.Logger import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes @Singleton class LightningRepo @Inject constructor( @@ -36,12 +39,73 @@ class LightningRepo @Inject constructor( private val _nodeLifecycleState: MutableStateFlow = MutableStateFlow(NodeLifecycleState.Stopped) val nodeLifecycleState = _nodeLifecycleState.asStateFlow() + /** + * Executes the provided operation only if the node is running. + * If the node is not running, waits for it to be running for a specified timeout. + * + * @param operationName Name of the operation for logging + * @param waitTimeout Duration to wait for the node to be running + * @param operation Lambda to execute when the node is running + * @return Result of the operation, or failure if node isn't running or operation fails + */ + private suspend fun executeWhenNodeRunning( + operationName: String, + waitTimeout: Duration = 1.minutes, + operation: suspend () -> Result + ): Result = withContext(bgDispatcher) { + // If node is already running, execute immediately + Logger.debug("Operation called: $operationName", context = TAG) + + if (nodeLifecycleState.value.isRunning()) { + return@withContext executeOperation(operationName, operation) + } + + // If node is not in a state that can become running, fail fast + if (!nodeLifecycleState.value.canRun()) { + return@withContext Result.failure( + Exception("Cannot execute $operationName: Node is ${nodeLifecycleState.value} and not starting") + ) + } + + val nodeRunning = withTimeoutOrNull(waitTimeout) { + if (nodeLifecycleState.value.isRunning()) { + return@withTimeoutOrNull true + } + + // Otherwise, wait for it to transition to running state + Logger.debug("Waiting for node runs to execute $operationName", context = TAG) + _nodeLifecycleState.first { it.isRunning() } + Logger.debug("Operation executed: $operationName", context = TAG) + true + } ?: false + + if (!nodeRunning) { + return@withContext Result.failure( + Exception("Timeout waiting for node to be running to execute $operationName") + ) + } + + return@withContext executeOperation(operationName, operation) + } + + private suspend fun executeOperation( + operationName: String, + operation: suspend () -> Result + ): Result { + return try { + operation() + } catch (e: Throwable) { + Logger.error("$operationName error", e, context = TAG) + Result.failure(e) + } + } + suspend fun setup(walletIndex: Int): Result = withContext(bgDispatcher) { return@withContext try { lightningService.setup(walletIndex) Result.success(Unit) } catch (e: Throwable) { - Logger.error("Node setup error", e) + Logger.error("Node setup error", e, context = TAG) Result.failure(e) } } @@ -79,7 +143,7 @@ class LightningRepo @Inject constructor( _nodeLifecycleState.value = NodeLifecycleState.Running Result.success(Unit) } catch (e: Throwable) { - Logger.error("Node start error", e) + Logger.error("Node start error", e, context = TAG) _nodeLifecycleState.value = NodeLifecycleState.ErrorStarting(e) Result.failure(e) } @@ -96,19 +160,14 @@ class LightningRepo @Inject constructor( _nodeLifecycleState.value = NodeLifecycleState.Stopped Result.success(Unit) } catch (e: Throwable) { - Logger.error("Node stop error", e) + Logger.error("Node stop error", e, context = TAG) Result.failure(e) } } - suspend fun sync(): Result = withContext(bgDispatcher) { - try { - lightningService.sync() - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Sync error", e) - Result.failure(e) - } + suspend fun sync(): Result = executeWhenNodeRunning("Sync") { + lightningService.sync() + Result.success(Unit) } suspend fun wipeStorage(walletIndex: Int): Result = withContext(bgDispatcher) { @@ -116,131 +175,92 @@ class LightningRepo @Inject constructor( lightningService.wipeStorage(walletIndex) Result.success(Unit) } catch (e: Throwable) { - Logger.error("Wipe storage error", e) + Logger.error("Wipe storage error", e, context = TAG) Result.failure(e) } } - suspend fun connectToTrustedPeers(): Result = withContext(bgDispatcher) { - try { - lightningService.connectToTrustedPeers() - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Connect to trusted peers error", e) - Result.failure(e) - } + suspend fun connectToTrustedPeers(): Result = executeWhenNodeRunning("Connect to trusted peers") { + lightningService.connectToTrustedPeers() + Result.success(Unit) } - suspend fun disconnectPeer(peer: LnPeer): Result = withContext(bgDispatcher) { - try { - lightningService.disconnectPeer(peer) - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Disconnect peer error", e) - Result.failure(e) - } + suspend fun disconnectPeer(peer: LnPeer): Result = executeWhenNodeRunning("Disconnect peer") { + lightningService.disconnectPeer(peer) + Result.success(Unit) } - suspend fun newAddress(): Result = withContext(bgDispatcher) { - try { - val address = lightningService.newAddress() - Result.success(address) - } catch (e: Throwable) { - Logger.error("New address error", e) - Result.failure(e) - } + suspend fun newAddress(): Result = executeWhenNodeRunning("New address") { + val address = lightningService.newAddress() + Result.success(address) } - suspend fun checkAddressUsage(address: String): Result = withContext(bgDispatcher) { - try { - val addressInfo = addressChecker.getAddressInfo(address) - val hasTransactions = addressInfo.chain_stats.tx_count > 0 || addressInfo.mempool_stats.tx_count > 0 - Result.success(hasTransactions) - } catch (e: Throwable) { - Logger.error("Check address usage error", e) - Result.failure(e) - } + suspend fun checkAddressUsage(address: String): Result = executeWhenNodeRunning("Check address usage") { + val addressInfo = addressChecker.getAddressInfo(address) + val hasTransactions = addressInfo.chain_stats.tx_count > 0 || addressInfo.mempool_stats.tx_count > 0 + Result.success(hasTransactions) } suspend fun createInvoice( amountSats: ULong? = null, description: String, expirySeconds: UInt = 86_400u - ): Result = withContext(bgDispatcher) { - try { - val invoice = lightningService.receive(amountSats, description, expirySeconds) - Result.success(invoice) - } catch (e: Throwable) { - Logger.error("Create invoice error", e) - Result.failure(e) - } + ): Result = executeWhenNodeRunning("Create invoice") { + val invoice = lightningService.receive(amountSats, description, expirySeconds) + Result.success(invoice) } - suspend fun payInvoice(bolt11: String, sats: ULong? = null): Result = withContext(bgDispatcher) { - try { + suspend fun payInvoice(bolt11: String, sats: ULong? = null): Result = + executeWhenNodeRunning("Pay invoice") { val paymentId = lightningService.send(bolt11 = bolt11, sats = sats) Result.success(paymentId) - } catch (e: Throwable) { - Logger.error("Pay invoice error", e) - Result.failure(e) } - } - suspend fun sendOnChain(address: Address, sats: ULong): Result = withContext(bgDispatcher) { - try { + suspend fun sendOnChain(address: Address, sats: ULong): Result = + executeWhenNodeRunning("Send on-chain") { val txId = lightningService.send(address = address, sats = sats) Result.success(txId) - } catch (e: Throwable) { - Logger.error("sendOnChain error", e) - Result.failure(e) } - } - suspend fun getPayments(): Result> = withContext(bgDispatcher) { - try { - val payments = lightningService.payments - ?: return@withContext Result.failure(Exception("It wasn't possible get the payments")) - Result.success(payments) - } catch (e: Throwable) { - Logger.error("getPayments error", e) - Result.failure(e) - } + suspend fun getPayments(): Result> = executeWhenNodeRunning("Get payments") { + val payments = lightningService.payments + ?: return@executeWhenNodeRunning Result.failure(Exception("It wasn't possible get the payments")) + Result.success(payments) } suspend fun openChannel( peer: LnPeer, channelAmountSats: ULong, pushToCounterpartySats: ULong? = null - ): Result = withContext(bgDispatcher) { - try { - val result = lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats) - result - } catch (e: Throwable) { - Logger.error("Open channel error", e) - Result.failure(e) - } + ): Result = executeWhenNodeRunning("Open channel") { + lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats) } suspend fun closeChannel(userChannelId: String, counterpartyNodeId: String): Result = - withContext(bgDispatcher) { - try { - lightningService.closeChannel(userChannelId, counterpartyNodeId) - Result.success(Unit) - } catch (e: Throwable) { - Logger.error("Close channel error", e) - Result.failure(e) - } + executeWhenNodeRunning("Close channel") { + lightningService.closeChannel(userChannelId, counterpartyNodeId) + Result.success(Unit) } - fun canSend(amountSats: ULong): Boolean = lightningService.canSend(amountSats) + fun canSend(amountSats: ULong): Boolean = + nodeLifecycleState.value.isRunning() && lightningService.canSend(amountSats) fun getSyncFlow(): Flow = lightningService.syncFlow() - fun getNodeId(): String? = lightningService.nodeId - fun getBalances(): BalanceDetails? = lightningService.balances - fun getStatus(): NodeStatus? = lightningService.status - fun getPeers(): List? = lightningService.peers - fun getChannels(): List? = lightningService.channels + fun getNodeId(): String? = if (nodeLifecycleState.value.isRunning()) lightningService.nodeId else null + + fun getBalances(): BalanceDetails? = if (nodeLifecycleState.value.isRunning()) lightningService.balances else null + + fun getStatus(): NodeStatus? = if (nodeLifecycleState.value.isRunning()) lightningService.status else null - fun hasChannels(): Boolean = lightningService.channels?.isNotEmpty() == true + fun getPeers(): List? = if (nodeLifecycleState.value.isRunning()) lightningService.peers else null + + fun getChannels(): List? = + if (nodeLifecycleState.value.isRunning()) lightningService.channels else null + + fun hasChannels(): Boolean = nodeLifecycleState.value.isRunning() && lightningService.channels?.isNotEmpty() == true + + private companion object { + const val TAG = "LightningRepo" + } } From d0171d4a92a43befca8b5c9691e2d3790a26c6df Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 1 May 2025 10:52:17 -0300 Subject: [PATCH 2/2] feat: remove comment --- app/src/main/java/to/bitkit/repositories/LightningRepo.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 6e3735154..e6399b4ef 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -53,7 +53,6 @@ class LightningRepo @Inject constructor( waitTimeout: Duration = 1.minutes, operation: suspend () -> Result ): Result = withContext(bgDispatcher) { - // If node is already running, execute immediately Logger.debug("Operation called: $operationName", context = TAG) if (nodeLifecycleState.value.isRunning()) {