Skip to content

Commit ca35f76

Browse files
authored
Merge pull request #113 from synonymdev/refactor/lightning-repository
Improve Wallet and LDK node business logic handling
2 parents 5fc5782 + fec5a31 commit ca35f76

File tree

11 files changed

+745
-338
lines changed

11 files changed

+745
-338
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,5 @@ internal object Env {
105105

106106
const val PIN_LENGTH = 4
107107
const val PIN_ATTEMPTS = 8
108+
const val DEFAULT_INVOICE_MESSAGE = "Bitkit"
108109
}

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import to.bitkit.models.blocktank.BlocktankNotificationType.incomingHtlc
2424
import to.bitkit.models.blocktank.BlocktankNotificationType.mutualClose
2525
import to.bitkit.models.blocktank.BlocktankNotificationType.orderPaymentConfirmed
2626
import to.bitkit.models.blocktank.BlocktankNotificationType.wakeToTimeout
27+
import to.bitkit.repositories.LightningRepo
2728
import to.bitkit.services.CoreService
28-
import to.bitkit.services.LightningService
2929
import to.bitkit.ui.pushNotification
3030
import to.bitkit.utils.Logger
3131
import to.bitkit.utils.withPerformanceLogging
@@ -36,7 +36,7 @@ class WakeNodeWorker @AssistedInject constructor(
3636
@Assisted private val appContext: Context,
3737
@Assisted private val workerParams: WorkerParameters,
3838
private val coreService: CoreService,
39-
private val lightningService: LightningService,
39+
private val lightningRepo: LightningRepo,
4040
) : CoroutineWorker(appContext, workerParams) {
4141
private val self = this
4242

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

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

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

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

187185
private suspend fun deliver() {
188-
lightningService.stop()
186+
lightningRepo.stop()
189187

190188
bestAttemptContent?.run {
191189
pushNotification(title, body, context = appContext)
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package to.bitkit.repositories
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.MutableStateFlow
6+
import kotlinx.coroutines.flow.asStateFlow
7+
import kotlinx.coroutines.withContext
8+
import org.lightningdevkit.ldknode.Address
9+
import org.lightningdevkit.ldknode.BalanceDetails
10+
import org.lightningdevkit.ldknode.Bolt11Invoice
11+
import org.lightningdevkit.ldknode.ChannelDetails
12+
import org.lightningdevkit.ldknode.NodeStatus
13+
import org.lightningdevkit.ldknode.PaymentDetails
14+
import org.lightningdevkit.ldknode.PaymentId
15+
import org.lightningdevkit.ldknode.Txid
16+
import org.lightningdevkit.ldknode.UserChannelId
17+
import to.bitkit.di.BgDispatcher
18+
import to.bitkit.models.LnPeer
19+
import to.bitkit.models.NodeLifecycleState
20+
import to.bitkit.services.LdkNodeEventBus
21+
import to.bitkit.services.LightningService
22+
import to.bitkit.services.NodeEventHandler
23+
import to.bitkit.utils.AddressChecker
24+
import to.bitkit.utils.Logger
25+
import javax.inject.Inject
26+
import javax.inject.Singleton
27+
import kotlin.time.Duration
28+
29+
@Singleton
30+
class LightningRepo @Inject constructor(
31+
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
32+
private val lightningService: LightningService,
33+
private val ldkNodeEventBus: LdkNodeEventBus,
34+
private val addressChecker: AddressChecker
35+
) {
36+
private val _nodeLifecycleState: MutableStateFlow<NodeLifecycleState> = MutableStateFlow(NodeLifecycleState.Stopped)
37+
val nodeLifecycleState = _nodeLifecycleState.asStateFlow()
38+
39+
suspend fun setup(walletIndex: Int): Result<Unit> = withContext(bgDispatcher) {
40+
return@withContext try {
41+
lightningService.setup(walletIndex)
42+
Result.success(Unit)
43+
} catch (e: Throwable) {
44+
Logger.error("Node setup error", e)
45+
Result.failure(e)
46+
}
47+
}
48+
49+
suspend fun start(
50+
walletIndex: Int,
51+
timeout: Duration? = null,
52+
eventHandler: NodeEventHandler? = null
53+
): Result<Unit> =
54+
withContext(bgDispatcher) {
55+
if (nodeLifecycleState.value.isRunningOrStarting()) {
56+
return@withContext Result.success(Unit)
57+
}
58+
59+
try {
60+
_nodeLifecycleState.value = NodeLifecycleState.Starting
61+
62+
// Setup if not already setup
63+
if (lightningService.node == null) {
64+
val setupResult = setup(walletIndex)
65+
if (setupResult.isFailure) {
66+
_nodeLifecycleState.value = NodeLifecycleState.ErrorStarting(
67+
setupResult.exceptionOrNull() ?: Exception("Unknown setup error")
68+
)
69+
return@withContext setupResult
70+
}
71+
}
72+
73+
// Start the node service
74+
lightningService.start(timeout) { event ->
75+
eventHandler?.invoke(event)
76+
ldkNodeEventBus.emit(event)
77+
}
78+
79+
_nodeLifecycleState.value = NodeLifecycleState.Running
80+
Result.success(Unit)
81+
} catch (e: Throwable) {
82+
Logger.error("Node start error", e)
83+
_nodeLifecycleState.value = NodeLifecycleState.ErrorStarting(e)
84+
Result.failure(e)
85+
}
86+
}
87+
88+
suspend fun stop(): Result<Unit> = withContext(bgDispatcher) {
89+
if (nodeLifecycleState.value.isStoppedOrStopping()) {
90+
return@withContext Result.success(Unit)
91+
}
92+
93+
try {
94+
_nodeLifecycleState.value = NodeLifecycleState.Stopping
95+
lightningService.stop()
96+
_nodeLifecycleState.value = NodeLifecycleState.Stopped
97+
Result.success(Unit)
98+
} catch (e: Throwable) {
99+
Logger.error("Node stop error", e)
100+
Result.failure(e)
101+
}
102+
}
103+
104+
suspend fun sync(): Result<Unit> = withContext(bgDispatcher) {
105+
try {
106+
lightningService.sync()
107+
Result.success(Unit)
108+
} catch (e: Throwable) {
109+
Logger.error("Sync error", e)
110+
Result.failure(e)
111+
}
112+
}
113+
114+
suspend fun wipeStorage(walletIndex: Int): Result<Unit> = withContext(bgDispatcher) {
115+
try {
116+
lightningService.wipeStorage(walletIndex)
117+
Result.success(Unit)
118+
} catch (e: Throwable) {
119+
Logger.error("Wipe storage error", e)
120+
Result.failure(e)
121+
}
122+
}
123+
124+
suspend fun connectToTrustedPeers(): Result<Unit> = withContext(bgDispatcher) {
125+
try {
126+
lightningService.connectToTrustedPeers()
127+
Result.success(Unit)
128+
} catch (e: Throwable) {
129+
Logger.error("Connect to trusted peers error", e)
130+
Result.failure(e)
131+
}
132+
}
133+
134+
suspend fun disconnectPeer(peer: LnPeer): Result<Unit> = withContext(bgDispatcher) {
135+
try {
136+
lightningService.disconnectPeer(peer)
137+
Result.success(Unit)
138+
} catch (e: Throwable) {
139+
Logger.error("Disconnect peer error", e)
140+
Result.failure(e)
141+
}
142+
}
143+
144+
suspend fun newAddress(): Result<String> = withContext(bgDispatcher) {
145+
try {
146+
val address = lightningService.newAddress()
147+
Result.success(address)
148+
} catch (e: Throwable) {
149+
Logger.error("New address error", e)
150+
Result.failure(e)
151+
}
152+
}
153+
154+
suspend fun checkAddressUsage(address: String): Result<Boolean> = withContext(bgDispatcher) {
155+
try {
156+
val addressInfo = addressChecker.getAddressInfo(address)
157+
val hasTransactions = addressInfo.chain_stats.tx_count > 0 || addressInfo.mempool_stats.tx_count > 0
158+
Result.success(hasTransactions)
159+
} catch (e: Throwable) {
160+
Logger.error("Check address usage error", e)
161+
Result.failure(e)
162+
}
163+
}
164+
165+
suspend fun createInvoice(
166+
amountSats: ULong? = null,
167+
description: String,
168+
expirySeconds: UInt = 86_400u
169+
): Result<Bolt11Invoice> = withContext(bgDispatcher) {
170+
try {
171+
val invoice = lightningService.receive(amountSats, description, expirySeconds)
172+
Result.success(invoice)
173+
} catch (e: Throwable) {
174+
Logger.error("Create invoice error", e)
175+
Result.failure(e)
176+
}
177+
}
178+
179+
suspend fun payInvoice(bolt11: String, sats: ULong? = null): Result<PaymentId> = withContext(bgDispatcher) {
180+
try {
181+
val paymentId = lightningService.send(bolt11 = bolt11, sats = sats)
182+
Result.success(paymentId)
183+
} catch (e: Throwable) {
184+
Logger.error("Pay invoice error", e)
185+
Result.failure(e)
186+
}
187+
}
188+
189+
suspend fun sendOnChain(address: Address, sats: ULong): Result<Txid> = withContext(bgDispatcher) {
190+
try {
191+
val txId = lightningService.send(address = address, sats = sats)
192+
Result.success(txId)
193+
} catch (e: Throwable) {
194+
Logger.error("sendOnChain error", e)
195+
Result.failure(e)
196+
}
197+
}
198+
199+
suspend fun getPayments(): Result<List<PaymentDetails>> = withContext(bgDispatcher) {
200+
try {
201+
val payments = lightningService.payments
202+
?: return@withContext Result.failure(Exception("It wasn't possible get the payments"))
203+
Result.success(payments)
204+
} catch (e: Throwable) {
205+
Logger.error("getPayments error", e)
206+
Result.failure(e)
207+
}
208+
}
209+
210+
suspend fun openChannel(
211+
peer: LnPeer,
212+
channelAmountSats: ULong,
213+
pushToCounterpartySats: ULong? = null
214+
): Result<UserChannelId> = withContext(bgDispatcher) {
215+
try {
216+
val result = lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats)
217+
result
218+
} catch (e: Throwable) {
219+
Logger.error("Open channel error", e)
220+
Result.failure(e)
221+
}
222+
}
223+
224+
suspend fun closeChannel(userChannelId: String, counterpartyNodeId: String): Result<Unit> =
225+
withContext(bgDispatcher) {
226+
try {
227+
lightningService.closeChannel(userChannelId, counterpartyNodeId)
228+
Result.success(Unit)
229+
} catch (e: Throwable) {
230+
Logger.error("Close channel error", e)
231+
Result.failure(e)
232+
}
233+
}
234+
235+
fun canSend(amountSats: ULong): Boolean = lightningService.canSend(amountSats)
236+
237+
fun getSyncFlow(): Flow<Unit> = lightningService.syncFlow()
238+
239+
fun getNodeId(): String? = lightningService.nodeId
240+
fun getBalances(): BalanceDetails? = lightningService.balances
241+
fun getStatus(): NodeStatus? = lightningService.status
242+
fun getPeers(): List<LnPeer>? = lightningService.peers
243+
fun getChannels(): List<ChannelDetails>? = lightningService.channels
244+
245+
fun hasChannels(): Boolean = lightningService.channels?.isNotEmpty() == true
246+
}

0 commit comments

Comments
 (0)