Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
77088d7
refactor: create the LightningRepository
jvsena42 Apr 24, 2025
ba2d19b
refactor: create the WalletRepository
jvsena42 Apr 24, 2025
e68b813
refactor: create the WalletRepository
jvsena42 Apr 24, 2025
fd6bb1b
refactor: replace business logic in viewmodel WIP
jvsena42 Apr 24, 2025
5a04366
refactor: replace business logic in viewmodel WIP
jvsena42 Apr 25, 2025
f8a57bd
refactor: move default invoice message to env file
jvsena42 Apr 25, 2025
fa17c3c
refactor: move move business logic from viewmodel to service
jvsena42 Apr 25, 2025
46fd709
refactor: move move business logic from viewmodel to service
jvsena42 Apr 25, 2025
eb337da
refactor: hold node state in a mutable state flow
jvsena42 Apr 25, 2025
d0f514d
refactor: only sync after the node starts with success
jvsena42 Apr 25, 2025
b9ff988
refactor: replace LightningService direct access with repository in W…
jvsena42 Apr 25, 2025
447c05a
refactor: rename WalletRepository
jvsena42 Apr 25, 2025
8e6de6a
refactor: rename LightningRepository
jvsena42 Apr 25, 2025
0035c44
refactor: replace LightningService direct access in AppViewModel.kt
jvsena42 Apr 25, 2025
7fe233f
refactor: replace LightningService direct access in ActivityViewModel.kt
jvsena42 Apr 25, 2025
99b5cd4
refactor: replace LightningService direct access in TransferViewModel.kt
jvsena42 Apr 25, 2025
8e6b7a6
test: update tests
jvsena42 Apr 25, 2025
3a1c23f
Merge branch 'master' into refactor/lightning-repository
jvsena42 Apr 25, 2025
77e460b
fix: solve conflicts
jvsena42 Apr 25, 2025
9363f4f
refactor: move logic to repository
jvsena42 Apr 25, 2025
36f0a7c
test: update tests
jvsena42 Apr 25, 2025
4b4d9af
test: update tests
jvsena42 Apr 28, 2025
6b6e222
fix: add wipe wallet method
jvsena42 Apr 28, 2025
ff7b5c5
reactor: remove unnecessary module
jvsena42 Apr 28, 2025
3e61c0a
reactor: remove unnecessary save string method
jvsena42 Apr 28, 2025
e7ebcba
fix: change sendOnChain typeAlias return
jvsena42 Apr 28, 2025
8f663c7
refactor: apply env variable
jvsena42 Apr 28, 2025
71b62c0
refactor: remove comment
jvsena42 Apr 28, 2025
03d07ff
refactor: implement bg dispatcher
jvsena42 Apr 28, 2025
84d81c8
refactor: add todo
jvsena42 Apr 28, 2025
4a21145
refactor: remove unused method
jvsena42 Apr 28, 2025
395718b
refactor: remove unused method
jvsena42 Apr 28, 2025
7e819d8
refactor: remove unused code
jvsena42 Apr 28, 2025
98625f2
refactor: fix typo
jvsena42 Apr 28, 2025
fec5a31
refactor: implement bg dispatcher
jvsena42 Apr 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ internal object Env {

const val PIN_LENGTH = 4
const val PIN_ATTEMPTS = 8
const val DEFAULT_INVOICE_MESSAGE = "Bitkit"
}
14 changes: 6 additions & 8 deletions app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import to.bitkit.models.blocktank.BlocktankNotificationType.incomingHtlc
import to.bitkit.models.blocktank.BlocktankNotificationType.mutualClose
import to.bitkit.models.blocktank.BlocktankNotificationType.orderPaymentConfirmed
import to.bitkit.models.blocktank.BlocktankNotificationType.wakeToTimeout
import to.bitkit.repositories.LightningRepo
import to.bitkit.services.CoreService
import to.bitkit.services.LightningService
import to.bitkit.ui.pushNotification
import to.bitkit.utils.Logger
import to.bitkit.utils.withPerformanceLogging
Expand All @@ -36,7 +36,7 @@ class WakeNodeWorker @AssistedInject constructor(
@Assisted private val appContext: Context,
@Assisted private val workerParams: WorkerParameters,
private val coreService: CoreService,
private val lightningService: LightningService,
private val lightningRepo: LightningRepo,
) : CoroutineWorker(appContext, workerParams) {
private val self = this

Expand All @@ -63,10 +63,8 @@ class WakeNodeWorker @AssistedInject constructor(

try {
withPerformanceLogging {
// TODO: Only start node if it's not running or implement & use StateLocker
lightningService.setup(walletIndex = 0)
lightningService.start(timeout) { event -> handleLdkEvent(event) }
lightningService.connectToTrustedPeers()
lightningRepo.start(walletIndex = 0, timeout= timeout) { event -> handleLdkEvent(event) }
lightningRepo.connectToTrustedPeers()

// Once node is started, handle the manual channel opening if needed
if (self.notificationType == orderPaymentConfirmed) {
Expand Down Expand Up @@ -136,7 +134,7 @@ class WakeNodeWorker @AssistedInject constructor(
self.bestAttemptContent?.title = "Payment received"
self.bestAttemptContent?.body = "Via new channel"

lightningService.channels?.find { it.channelId == event.channelId }?.let { channel ->
lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel ->
val sats = channel.outboundCapacityMsat / 1000u
self.bestAttemptContent?.title = "Received ⚡ $sats sats"
// Save for UI to pick up
Expand Down Expand Up @@ -185,7 +183,7 @@ class WakeNodeWorker @AssistedInject constructor(
}

private suspend fun deliver() {
lightningService.stop()
lightningRepo.stop()

bestAttemptContent?.run {
pushNotification(title, body, context = appContext)
Expand Down
246 changes: 246 additions & 0 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package to.bitkit.repositories

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import org.lightningdevkit.ldknode.Address
import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.Bolt11Invoice
import org.lightningdevkit.ldknode.ChannelDetails
import org.lightningdevkit.ldknode.NodeStatus
import org.lightningdevkit.ldknode.PaymentDetails
import org.lightningdevkit.ldknode.PaymentId
import org.lightningdevkit.ldknode.Txid
import org.lightningdevkit.ldknode.UserChannelId
import to.bitkit.di.BgDispatcher
import to.bitkit.models.LnPeer
import to.bitkit.models.NodeLifecycleState
import to.bitkit.services.LdkNodeEventBus
import to.bitkit.services.LightningService
import to.bitkit.services.NodeEventHandler
import to.bitkit.utils.AddressChecker
import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration

@Singleton
class LightningRepo @Inject constructor(
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
private val lightningService: LightningService,
private val ldkNodeEventBus: LdkNodeEventBus,
private val addressChecker: AddressChecker
) {
private val _nodeLifecycleState: MutableStateFlow<NodeLifecycleState> = MutableStateFlow(NodeLifecycleState.Stopped)
val nodeLifecycleState = _nodeLifecycleState.asStateFlow()

suspend fun setup(walletIndex: Int): Result<Unit> = withContext(bgDispatcher) {
return@withContext try {
lightningService.setup(walletIndex)
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Node setup error", e)
Result.failure(e)
}
}

suspend fun start(
walletIndex: Int,
timeout: Duration? = null,
eventHandler: NodeEventHandler? = null
): Result<Unit> =
withContext(bgDispatcher) {
if (nodeLifecycleState.value.isRunningOrStarting()) {
return@withContext Result.success(Unit)
}

try {
_nodeLifecycleState.value = NodeLifecycleState.Starting

// Setup if not already setup
if (lightningService.node == null) {
val setupResult = setup(walletIndex)
if (setupResult.isFailure) {
_nodeLifecycleState.value = NodeLifecycleState.ErrorStarting(
setupResult.exceptionOrNull() ?: Exception("Unknown setup error")
)
return@withContext setupResult
}
}

// Start the node service
lightningService.start(timeout) { event ->
eventHandler?.invoke(event)
ldkNodeEventBus.emit(event)
}

_nodeLifecycleState.value = NodeLifecycleState.Running
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Node start error", e)
_nodeLifecycleState.value = NodeLifecycleState.ErrorStarting(e)
Result.failure(e)
}
}

suspend fun stop(): Result<Unit> = withContext(bgDispatcher) {
if (nodeLifecycleState.value.isStoppedOrStopping()) {
return@withContext Result.success(Unit)
}

try {
_nodeLifecycleState.value = NodeLifecycleState.Stopping
lightningService.stop()
_nodeLifecycleState.value = NodeLifecycleState.Stopped
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Node stop error", e)
Result.failure(e)
}
}

suspend fun sync(): Result<Unit> = withContext(bgDispatcher) {
try {
lightningService.sync()
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Sync error", e)
Result.failure(e)
}
}

suspend fun wipeStorage(walletIndex: Int): Result<Unit> = withContext(bgDispatcher) {
try {
lightningService.wipeStorage(walletIndex)
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Wipe storage error", e)
Result.failure(e)
}
}

suspend fun connectToTrustedPeers(): Result<Unit> = withContext(bgDispatcher) {
try {
lightningService.connectToTrustedPeers()
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Connect to trusted peers error", e)
Result.failure(e)
}
}

suspend fun disconnectPeer(peer: LnPeer): Result<Unit> = withContext(bgDispatcher) {
try {
lightningService.disconnectPeer(peer)
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Disconnect peer error", e)
Result.failure(e)
}
}

suspend fun newAddress(): Result<String> = withContext(bgDispatcher) {
try {
val address = lightningService.newAddress()
Result.success(address)
} catch (e: Throwable) {
Logger.error("New address error", e)
Result.failure(e)
}
}

suspend fun checkAddressUsage(address: String): Result<Boolean> = 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 createInvoice(
amountSats: ULong? = null,
description: String,
expirySeconds: UInt = 86_400u
): Result<Bolt11Invoice> = 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)
}
}

suspend fun payInvoice(bolt11: String, sats: ULong? = null): Result<PaymentId> = withContext(bgDispatcher) {
try {
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<Txid> = withContext(bgDispatcher) {
try {
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<List<PaymentDetails>> = 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 openChannel(
peer: LnPeer,
channelAmountSats: ULong,
pushToCounterpartySats: ULong? = null
): Result<UserChannelId> = withContext(bgDispatcher) {
try {
val result = lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats)
result
} catch (e: Throwable) {
Logger.error("Open channel error", e)
Result.failure(e)
}
}

suspend fun closeChannel(userChannelId: String, counterpartyNodeId: String): Result<Unit> =
withContext(bgDispatcher) {
try {
lightningService.closeChannel(userChannelId, counterpartyNodeId)
Result.success(Unit)
} catch (e: Throwable) {
Logger.error("Close channel error", e)
Result.failure(e)
}
}

fun canSend(amountSats: ULong): Boolean = lightningService.canSend(amountSats)

fun getSyncFlow(): Flow<Unit> = lightningService.syncFlow()

fun getNodeId(): String? = lightningService.nodeId
fun getBalances(): BalanceDetails? = lightningService.balances
fun getStatus(): NodeStatus? = lightningService.status
fun getPeers(): List<LnPeer>? = lightningService.peers
fun getChannels(): List<ChannelDetails>? = lightningService.channels

fun hasChannels(): Boolean = lightningService.channels?.isNotEmpty() == true
}
Loading