Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class)
class KeychainStoreTest : BaseTest() {

private val appContext: Context by lazy { ApplicationProvider.getApplicationContext() }
private val appContext by lazy { ApplicationProvider.getApplicationContext<Context>() }
private lateinit var db: AppDb

private lateinit var sut: KeychainStore
Expand All @@ -51,9 +51,9 @@ class KeychainStoreTest : BaseTest() {

@Test
fun dbSeed() = test {
val config = db.configDao().getAll().first()
val walletIndex = db.configDao().getAll().first().first().walletIndex

assertTrue { config.first().walletIndex == 0L }
assertEquals(0L, walletIndex)
}

@Test
Expand All @@ -67,11 +67,10 @@ class KeychainStoreTest : BaseTest() {

@Test
fun saveString_existingKey_shouldThrow() = test {
assertFailsWith<IllegalArgumentException> {
val key = "key"
sut.saveString(key, "value1")
sut.saveString(key, "value2")
}
val key = "key"
sut.saveString(key, "value1")

assertFailsWith<IllegalArgumentException> { sut.saveString(key, "value2") }
}

@Test
Expand All @@ -85,7 +84,11 @@ class KeychainStoreTest : BaseTest() {
}

@Test
fun exists() {
fun exists() = test {
val (key, value) = "keyToExist" to "value"
sut.saveString(key, value)

assertTrue { sut.exists(key) }
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
package to.bitkit
package to.bitkit.ldk

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import to.bitkit.ext.readAsset
import to.bitkit.ldk.LightningService
import to.bitkit.ldk.MigrationService
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
class LdkMigrationTest {
private val mnemonic = "pool curve feature leader elite dilemma exile toast smile couch crane public"

private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val appContext: Context by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
private val testContext by lazy { InstrumentationRegistry.getInstrumentation().context }
private val appContext by lazy { ApplicationProvider.getApplicationContext<Context>() }

@Test
fun nodeShouldStartFromBackupAfterMigration() {
val seed = context.readAsset("ldk-backup/seed.bin")
val manager = context.readAsset("ldk-backup/manager.bin")
val monitor = context.readAsset("ldk-backup/monitor.bin")
val seed = testContext.readAsset("ldk-backup/seed.bin")
val manager = testContext.readAsset("ldk-backup/manager.bin")
val monitor = testContext.readAsset("ldk-backup/monitor.bin")

MigrationService(appContext).migrate(seed, manager, listOf(monitor))

with(LightningService()) {
init(mnemonic)
start()
with(LightningService.shared) {
setup(mnemonic)
runBlocking { start() }

assertTrue { nodeId == "02cd08b7b375e4263849121f9f0ffb2732a0b88d0fb74487575ac539b374f45a55" }
assertTrue { channels.isNotEmpty() }

stop()
runBlocking { stop() }
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal class App : Application(), Configuration.Provider {
super.onCreate()
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }

Env.LdkStorage.init(filesDir.absolutePath)
Env.Storage.init(filesDir.absolutePath)
}

override fun onTerminate() {
Expand Down
32 changes: 21 additions & 11 deletions app/src/main/java/to/bitkit/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
package to.bitkit

import android.util.Log
import to.bitkit.Tag.LDK
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
import kotlin.io.path.Path
import org.lightningdevkit.ldknode.Network as LdkNetwork

// region globals
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
const val PERF = "PERF"
}

internal const val HOST = "10.0.2.2"
Expand All @@ -32,24 +34,31 @@ internal val PEER = LnPeer(
host = HOST,
port = "9737",
)
// endregion

// region env
internal object Env {
val isDebug = BuildConfig.DEBUG

object LdkStorage {
lateinit var path: String
object Storage {
private var base = ""
fun init(basePath: String) {
require(basePath.isNotEmpty()) { "Base storage path cannot be empty" }
base = basePath
Log.i(APP, "Storage path: $basePath")
}

val ldk get() = storagePathOf(0, network.id, "ldk")
val bdk get() = storagePathOf(0, network.id, "bdk")

fun init(base: String): String {
require(base.isNotEmpty()) { "Base path for LDK storage cannot be empty" }
if (::path.isInitialized) {
Log.w(LDK, "Storage path already set: $path")
}
path = Path(base, network.id, "ldk")
private fun storagePathOf(walletIndex: Int, network: String, dir: String): String {
require(base.isNotEmpty()) { "Base storage path cannot be empty" }
val absolutePath = Path(base, network, "wallet$walletIndex", dir)
.toFile()
.ensureDir()
.absolutePath
Log.d(LDK, "Storage path: $path")
return path
Log.d(APP, "$dir storage path: $absolutePath")
return absolutePath
}
}

Expand All @@ -66,6 +75,7 @@ internal object Env {
else -> null
}
}
// endregion

data class LnPeer(
val nodeId: String,
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.initNotificationChannel
Expand All @@ -15,7 +16,8 @@ class LauncherActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
initNotificationChannel()
logFcmToken()
warmupNode()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/to/bitkit/async/BaseCoroutineScope.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package to.bitkit.async

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import to.bitkit.di.IoDispatcher
import kotlin.coroutines.CoroutineContext

open class BaseCoroutineScope(
@IoDispatcher private val dispatcher: CoroutineDispatcher,
) : CoroutineScope {
private val job = Job()
override val coroutineContext = dispatcher + job

@Throws(InterruptedException::class)
protected fun <T> runBlocking(
context: CoroutineContext = coroutineContext,
block: suspend CoroutineScope.() -> T,
): T {
return kotlinx.coroutines.runBlocking(context, block)
}
}
117 changes: 78 additions & 39 deletions app/src/main/java/to/bitkit/bdk/BitcoinService.kt
Original file line number Diff line number Diff line change
@@ -1,67 +1,106 @@
package to.bitkit.bdk

import android.util.Log
import org.bitcoindevkit.AddressIndex
import org.bitcoindevkit.Blockchain
import org.bitcoindevkit.BlockchainConfig
import org.bitcoindevkit.DatabaseConfig
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import org.bitcoindevkit.Descriptor
import org.bitcoindevkit.DescriptorSecretKey
import org.bitcoindevkit.EsploraConfig
import org.bitcoindevkit.EsploraClient
import org.bitcoindevkit.KeychainKind
import org.bitcoindevkit.Mnemonic
import org.bitcoindevkit.Progress
import org.bitcoindevkit.Wallet
import to.bitkit.Env
import to.bitkit.REST
import to.bitkit.SEED
import to.bitkit.Tag.BDK
import to.bitkit.async.BaseCoroutineScope
import to.bitkit.di.BgDispatcher
import to.bitkit.di.ServiceQueue
import javax.inject.Inject
import kotlin.io.path.Path
import kotlin.io.path.pathString

internal class BitcoinService {
class BitcoinService @Inject constructor(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
) : BaseCoroutineScope(bgDispatcher) {
companion object {
val shared by lazy {
BitcoinService()
BitcoinService(Dispatchers.Default)
}
}

private val wallet by lazy {
private val parallelRequests = 5_UL
private val stopGap = 20_UL
private var hasSynced = false

private val esploraClient by lazy { EsploraClient(url = REST) }
private val dbPath by lazy { Path(Env.Storage.bdk, "db.sqlite") }

private lateinit var wallet: Wallet

suspend fun setup() {
val network = Env.network.bdk
val mnemonic = Mnemonic.fromString(SEED)
val key = DescriptorSecretKey(network, mnemonic, null)

Wallet(
descriptor = Descriptor.newBip84(key, KeychainKind.INTERNAL, network),
changeDescriptor = Descriptor.newBip84(key, KeychainKind.EXTERNAL, network),
network = network,
databaseConfig = DatabaseConfig.Memory,
)
}
private val blockchain by lazy {
Blockchain(
BlockchainConfig.Esplora(
EsploraConfig(
baseUrl = REST,
proxy = null,
concurrency = 5u,
stopGap = 20u,
timeout = null,
)
Log.d(BDK, "Setting up wallet…")

ServiceQueue.BDK.background {
wallet = Wallet(
descriptor = Descriptor.newBip84(key, KeychainKind.INTERNAL, network),
changeDescriptor = Descriptor.newBip84(key, KeychainKind.EXTERNAL, network),
persistenceBackendPath = dbPath.pathString,
network = network,
)
)
}

Log.i(BDK, "Wallet set up")
}

fun sync() {
wallet.sync(
blockchain = blockchain,
progress = object : Progress {
override fun update(progress: Float, message: String?) {
Log.d(BDK, "Updating wallet: $progress $message")
}
}
)
suspend fun syncWithRevealedSpks() {
Log.d(BDK, "Wallet syncing…")

ServiceQueue.BDK.background {
val request = wallet.startSyncWithRevealedSpks()
val update = esploraClient.sync(request, parallelRequests)
wallet.applyUpdate(update)
}

hasSynced = true
Log.i(BDK, "Wallet synced")
}

// region State
val balance get() = wallet.getBalance()
val address get() = wallet.getAddress(AddressIndex.LastUnused).address.asString()
suspend fun fullScan() {
Log.d(BDK, "Wallet full scan…")

ServiceQueue.BDK.background {
val request = wallet.startFullScan()
val update = esploraClient.fullScan(request, stopGap, parallelRequests)
wallet.applyUpdate(update)
// TODO: Persist wallet once BDK is updated to beta release
}

hasSynced = true

Log.i(BDK, "Wallet fully scanned")
}

fun wipeStorage() {
Log.w(BDK, "Wiping wallet storage…")

dbPath.toFile()?.parentFile?.deleteRecursively()

Log.i(BDK, "Wallet storage wiped")
}

// region state
val balance get() = if (hasSynced) wallet.getBalance() else null

suspend fun getAddress(): String {
return ServiceQueue.BDK.background {
val addressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL).address
addressInfo.asString()
}
}
// endregion
}
Loading