Skip to content

Commit 77088d7

Browse files
committed
refactor: create the LightningRepository
1 parent d3ef778 commit 77088d7

File tree

3 files changed

+269
-0
lines changed

3 files changed

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ open class AppError(override val message: String? = null) : Exception(message) {
2323
sealed class ServiceError(message: String) : AppError(message) {
2424
data object NodeNotSetup : ServiceError("Node is not setup")
2525
data object NodeNotStarted : ServiceError("Node is not started")
26+
data object NodeStartTimeout : ServiceError("Node took too long to start")
2627
class LdkNodeSqliteAlreadyExists(path: String) : ServiceError("LDK-node SQLite file already exists at $path")
2728
data object LdkToLdkNodeMigration : ServiceError("LDK to LDK-node migration issue")
2829
data object MnemonicNotFound : ServiceError("Mnemonic not found")

0 commit comments

Comments
 (0)