From a9a3a06dfe8ba379a5532966f5cf75fcb821d8af Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 01:44:28 +0200 Subject: [PATCH 01/13] feat: BDK storage path --- app/src/main/java/to/bitkit/App.kt | 2 +- app/src/main/java/to/bitkit/Constants.kt | 27 +++++++++++-------- .../java/to/bitkit/ldk/LightningService.kt | 12 +++++++-- .../java/to/bitkit/ldk/MigrationService.kt | 2 +- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/to/bitkit/App.kt b/app/src/main/java/to/bitkit/App.kt index 88a467eea..e4c10c2b6 100644 --- a/app/src/main/java/to/bitkit/App.kt +++ b/app/src/main/java/to/bitkit/App.kt @@ -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() { diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index ec28359d1..eff390d99 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -3,7 +3,7 @@ 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 @@ -36,20 +36,25 @@ internal val PEER = LnPeer( 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 } } diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index b06a226e0..a88b3b876 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -14,6 +14,7 @@ import to.bitkit.SEED import to.bitkit.Tag.LDK import to.bitkit.bdk.BitcoinService +// TODO support concurrency class LightningService { companion object { val shared by lazy { @@ -24,7 +25,7 @@ class LightningService { lateinit var node: Node fun init(mnemonic: String = SEED) { - val dir = Env.LdkStorage.path + val dir = Env.Storage.ldk val builder = Builder .fromConfig( @@ -83,15 +84,17 @@ class LightningService { node.syncWallets() } - // region State + // region state val nodeId: String get() = node.nodeId() val balances get() = node.listBalances() val status get() = node.status() val peers get() = node.listPeers() val channels get() = node.listChannels() val payments get() = node.listPayments() + // endregion } +// region peers internal fun LightningService.connectPeer(peer: LnPeer) { Log.d(LDK, "Connecting peer: $peer") val res = runCatching { @@ -99,7 +102,9 @@ internal fun LightningService.connectPeer(peer: LnPeer) { } Log.d(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") } +// endregion +// region channels internal suspend fun LightningService.openChannel() { val peer = peers.first() @@ -152,7 +157,9 @@ internal suspend fun LightningService.closeChannel(userChannelId: String, counte // mine 1 block & wait for esplora to pick up block sync() } +// endregion +// region payments internal fun LightningService.createInvoice(): String { return node.bolt11Payment().receive(amountMsat = 112u, description = "description", expirySecs = 7200u) } @@ -182,6 +189,7 @@ internal suspend fun LightningService.payInvoice(invoice: String): Boolean { return true } +// endregion internal fun warmupNode() { runCatching { diff --git a/app/src/main/java/to/bitkit/ldk/MigrationService.kt b/app/src/main/java/to/bitkit/ldk/MigrationService.kt index 9bbf074a5..f70e3288a 100644 --- a/app/src/main/java/to/bitkit/ldk/MigrationService.kt +++ b/app/src/main/java/to/bitkit/ldk/MigrationService.kt @@ -22,7 +22,7 @@ class MigrationService @Inject constructor( fun migrate(seed: ByteArray, manager: ByteArray, monitors: List) { Log.d(LDK, "Migrating LDK backup…") - val file = Path(Env.LdkStorage.path, LDK_DB_NAME).toFile() + val file = Path(Env.Storage.ldk, LDK_DB_NAME).toFile() // Skip if db already exists if (file.exists()) { From 503087aea3305eb36b545f03ffa824248e457ec7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 01:57:18 +0200 Subject: [PATCH 02/13] deps: BDK 1.0 alpha --- app/src/main/java/to/bitkit/Constants.kt | 4 + .../main/java/to/bitkit/bdk/BitcoinService.kt | 73 +++++++++++-------- .../java/to/bitkit/ldk/LightningService.kt | 2 +- .../main/java/to/bitkit/ui/MainViewModel.kt | 3 +- gradle/libs.versions.toml | 2 +- 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index eff390d99..d72cfda11 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -9,6 +9,7 @@ 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" @@ -32,7 +33,9 @@ internal val PEER = LnPeer( host = HOST, port = "9737", ) +// endregion +// region env internal object Env { val isDebug = BuildConfig.DEBUG @@ -71,6 +74,7 @@ internal object Env { else -> null } } +// endregion data class LnPeer( val nodeId: String, diff --git a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt index 052c9424e..e319be45e 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt @@ -1,22 +1,20 @@ 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 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 kotlin.io.path.Path +import kotlin.io.path.pathString +// TODO support concurrency internal class BitcoinService { companion object { val shared by lazy { @@ -24,44 +22,57 @@ internal class BitcoinService { } } + private val parallelRequests = 5_UL + private val stopGap = 20_UL + private var hasSynced = false + + private val esploraClient by lazy { EsploraClient(url = REST) } + private val wallet by lazy { val network = Env.network.bdk val mnemonic = Mnemonic.fromString(SEED) val key = DescriptorSecretKey(network, mnemonic, null) + val dbPath = Path(Env.Storage.bdk, "db.sqlite").pathString + + Log.i(BDK, "Creating wallet…") + Wallet( descriptor = Descriptor.newBip84(key, KeychainKind.INTERNAL, network), changeDescriptor = Descriptor.newBip84(key, KeychainKind.EXTERNAL, network), + persistenceBackendPath = dbPath, network = network, - databaseConfig = DatabaseConfig.Memory, - ) - } - private val blockchain by lazy { - Blockchain( - BlockchainConfig.Esplora( - EsploraConfig( - baseUrl = REST, - proxy = null, - concurrency = 5u, - stopGap = 20u, - timeout = null, - ) - ) ) } + // region sync fun sync() { - wallet.sync( - blockchain = blockchain, - progress = object : Progress { - override fun update(progress: Float, message: String?) { - Log.d(BDK, "Updating wallet: $progress $message") - } - } - ) + Log.d(BDK, "Wallet syncing…") + + val request = wallet.startSyncWithRevealedSpks() + val update = esploraClient.sync(request, parallelRequests) + wallet.applyUpdate(update) + + hasSynced = true + Log.d(BDK, "Wallet synced") + } + + fun fullScan() { + Log.d(BDK, "Wallet full scan…") + + 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.d(BDK, "Wallet fully scanned") } + // endregion - // region State - val balance get() = wallet.getBalance() - val address get() = wallet.getAddress(AddressIndex.LastUnused).address.asString() + // region state + val balance get() = if (hasSynced) wallet.getBalance() else null + val address get() = wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString() + // endregion } diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index a88b3b876..ac782003c 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -199,7 +199,7 @@ internal fun warmupNode() { sync() } BitcoinService.shared.apply { - sync() + fullScan() } }.onFailure { Log.e(LDK, "Warmup error:", it) diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index 9f3a0102c..b9b6ee39d 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -54,10 +54,11 @@ class MainViewModel @Inject constructor( } fun sync() { + bitcoinService.sync() ldkNodeId.value = lightningService.nodeId ldkBalance.value = lightningService.balances.totalLightningBalanceSats.toString() btcAddress.value = bitcoinService.address - btcBalance.value = bitcoinService.balance.total.toString() + btcBalance.value = bitcoinService.balance?.total?.toSat().toString() mnemonic.value = SEED peers.syncTo(lightningService.peers) channels.syncTo(lightningService.channels) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 637e1be3f..38fad6b2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ activityCompose = "1.9.1" agp = "8.5.2" appcompat = "1.7.0" -bdk = "0.31.1" +bdk = "1.0.0-alpha.11" composeBom = "2024.06.00" # https://developer.android.com/develop/ui/compose/bom/bom-mapping coreKtx = "1.13.1" datastorePrefs = "1.1.1" From fcecbd937862e4a76730d46cf1b4afe59d58277b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 16:38:24 +0200 Subject: [PATCH 03/13] refactor: Optimise android tests --- .../java/to/bitkit/LdkMigrationTest.kt | 11 ++++++----- .../bitkit/data/keychain/KeychainStoreTest.kt | 19 +++++++++++-------- .../main/java/to/bitkit/ui/MainActivity.kt | 5 ----- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt index 88664529f..bf62a816c 100644 --- a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt +++ b/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt @@ -1,6 +1,7 @@ package to.bitkit import android.content.Context +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Test @@ -14,14 +15,14 @@ import kotlin.test.assertTrue 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() } @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)) diff --git a/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt b/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt index 44135d041..ead243050 100644 --- a/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt +++ b/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt @@ -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() } private lateinit var db: AppDb private lateinit var sut: KeychainStore @@ -53,7 +53,7 @@ class KeychainStoreTest : BaseTest() { fun dbSeed() = test { val config = db.configDao().getAll().first() - assertTrue { config.first().walletIndex == 0L } + assertEquals(0L, config.first().walletIndex) } @Test @@ -67,11 +67,10 @@ class KeychainStoreTest : BaseTest() { @Test fun saveString_existingKey_shouldThrow() = test { - assertFailsWith { - val key = "key" - sut.saveString(key, "value1") - sut.saveString(key, "value2") - } + val key = "key" + sut.saveString(key, "value1") + + assertFailsWith { sut.saveString(key, "value2") } } @Test @@ -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 diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index bb10c6655..d3c9e3b4f 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -46,21 +46,16 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import to.bitkit.R -import to.bitkit.data.keychain.KeychainStore import to.bitkit.ext.requiresPermission import to.bitkit.ext.toast import to.bitkit.ui.shared.Channels import to.bitkit.ui.shared.Peers import to.bitkit.ui.theme.AppThemeSurface -import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel by viewModels() - @Inject - lateinit var keychain: KeychainStore - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 6dfc39783423b5e946e6179e6fe072e66f26b2ef Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 16:40:20 +0200 Subject: [PATCH 04/13] feat: ServiceQueue --- .../main/java/to/bitkit/di/ServiceQueue.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/src/main/java/to/bitkit/di/ServiceQueue.kt diff --git a/app/src/main/java/to/bitkit/di/ServiceQueue.kt b/app/src/main/java/to/bitkit/di/ServiceQueue.kt new file mode 100644 index 000000000..ea7ce78c7 --- /dev/null +++ b/app/src/main/java/to/bitkit/di/ServiceQueue.kt @@ -0,0 +1,56 @@ +package to.bitkit.di + +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.withContext +import to.bitkit.Tag.APP +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory +import kotlin.coroutines.CoroutineContext +import kotlin.system.measureTimeMillis + +enum class ServiceQueue { + LDK, BDK, MIGRATION; + + private val scope: CoroutineScope + get() = when (this) { + LDK -> ldkScope + BDK -> bdkScope + MIGRATION -> migrationScope + } + + suspend fun background( + coroutineContext: CoroutineContext = scope.coroutineContext, + functionName: String = Thread.currentThread().stackTrace[1].methodName, + block: suspend CoroutineScope.() -> T, + ): T { + return withContext(coroutineContext) { + try { + var result: T + val timeElapsed = measureTimeMillis { + result = block() + } + Log.d(APP, "$functionName took ${timeElapsed / 1000.0} seconds on queue: $name") + + return@withContext result + } catch (e: Exception) { + Log.e(APP, "ServiceQueue.$name error", e) + throw e + } + } + } + + companion object { + private val ldkScope by lazy { CoroutineScope(dispatcher("ldk-queue") + SupervisorJob()) } + private val bdkScope by lazy { CoroutineScope(dispatcher("bdk-queue") + SupervisorJob()) } + private val migrationScope by lazy { CoroutineScope(dispatcher("migration-queue") + SupervisorJob()) } + + private fun dispatcher(name: String): ExecutorCoroutineDispatcher { + val threadFactory = ThreadFactory { Thread(it, name).apply { priority = Thread.NORM_PRIORITY - 1 } } + return Executors.newSingleThreadExecutor(threadFactory).asCoroutineDispatcher() + } + } +} From eb233a078b4997027fe5a502c72c7c594037728e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 16:52:28 +0200 Subject: [PATCH 05/13] feat: BaseCoroutineScope --- .../bitkit/data/keychain/KeychainStoreTest.kt | 4 ++-- .../to/bitkit/async/BaseCoroutineScope.kt | 22 +++++++++++++++++++ .../to/bitkit/data/keychain/KeychainStore.kt | 18 +++++---------- 3 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/to/bitkit/async/BaseCoroutineScope.kt diff --git a/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt b/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt index ead243050..0e6c7514e 100644 --- a/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt +++ b/app/src/androidTest/java/to/bitkit/data/keychain/KeychainStoreTest.kt @@ -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 - assertEquals(0L, config.first().walletIndex) + assertEquals(0L, walletIndex) } @Test diff --git a/app/src/main/java/to/bitkit/async/BaseCoroutineScope.kt b/app/src/main/java/to/bitkit/async/BaseCoroutineScope.kt new file mode 100644 index 000000000..2b4ec0106 --- /dev/null +++ b/app/src/main/java/to/bitkit/async/BaseCoroutineScope.kt @@ -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 runBlocking( + context: CoroutineContext = coroutineContext, + block: suspend CoroutineScope.() -> T, + ): T { + return kotlinx.coroutines.runBlocking(context, block) + } +} diff --git a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt index b3ea787bc..1f6f67cc5 100644 --- a/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/KeychainStore.kt @@ -4,18 +4,15 @@ package to.bitkit.data.keychain import android.content.Context import android.util.Log -import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import to.bitkit.Tag.APP +import to.bitkit.async.BaseCoroutineScope import to.bitkit.data.AppDb import to.bitkit.di.IoDispatcher import to.bitkit.ext.fromBase64 @@ -26,21 +23,16 @@ class KeychainStore @Inject constructor( private val db: AppDb, @ApplicationContext private val context: Context, @IoDispatcher private val dispatcher: CoroutineDispatcher, -) : CoroutineScope { - - private val job = Job() - override val coroutineContext = dispatcher + job - +) : BaseCoroutineScope(dispatcher) { private val alias = "keychain" private val keyStore by lazy { AndroidKeyStore(alias) } - private val Context.keychain: DataStore by preferencesDataStore(alias, scope = this) - val snapshot get() = runBlocking(coroutineContext) { context.keychain.data.first() } + private val Context.keychain by preferencesDataStore(alias, scope = this) + val snapshot get() = runBlocking { context.keychain.data.first() } fun loadString(key: String): String? = load(key)?.let { keyStore.decrypt(it) } private fun load(key: String): ByteArray? { - // TODO throw/warn if not found return snapshot[key.indexed]?.fromBase64() } @@ -72,7 +64,7 @@ class KeychainStore @Inject constructor( private val String.indexed: Preferences.Key get() { - val walletIndex = runBlocking(coroutineContext) { db.configDao().getAll().first() }.first().walletIndex + val walletIndex = runBlocking { db.configDao().getAll().first() }.first().walletIndex return "${this}_$walletIndex".let(::stringPreferencesKey) } } From 81ae0b5068e99c89e560d5d178c21fc71e5adcde Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 18:43:33 +0200 Subject: [PATCH 06/13] refactor: Use workDataOf --- app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt | 2 +- app/src/main/java/to/bitkit/di/DispatchersModule.kt | 5 +++-- app/src/main/java/to/bitkit/fcm/FcmService.kt | 9 ++++----- app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt | 10 +++------- app/src/main/java/to/bitkit/ldk/LightningService.kt | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt index bf62a816c..9540c042e 100644 --- a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt +++ b/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt @@ -26,7 +26,7 @@ class LdkMigrationTest { MigrationService(appContext).migrate(seed, manager, listOf(monitor)) - with(LightningService()) { + with(LightningService.shared) { init(mnemonic) start() diff --git a/app/src/main/java/to/bitkit/di/DispatchersModule.kt b/app/src/main/java/to/bitkit/di/DispatchersModule.kt index f64f8e874..d769f672c 100644 --- a/app/src/main/java/to/bitkit/di/DispatchersModule.kt +++ b/app/src/main/java/to/bitkit/di/DispatchersModule.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package to.bitkit.di import dagger.Module @@ -23,7 +25,6 @@ annotation class IoDispatcher @Module @InstallIn(SingletonComponent::class) object DispatchersModule { - @UiDispatcher @Provides fun provideUiDispatcher(): CoroutineDispatcher { @@ -41,4 +42,4 @@ object DispatchersModule { fun provideIoDispatcher(): CoroutineDispatcher { return Dispatchers.IO } -} \ No newline at end of file +} diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index 345b151dd..e45b99e00 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -1,9 +1,9 @@ package to.bitkit.fcm import android.util.Log -import androidx.work.Data import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager +import androidx.work.workDataOf import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import to.bitkit.Tag.FCM @@ -52,9 +52,9 @@ internal class FcmService : FirebaseMessagingService() { private fun scheduleJob(messageData: Map) { val work = OneTimeWorkRequestBuilder() .setInputData( - Data.Builder() - .putString("bolt11", messageData["bolt11"].orEmpty()) - .build() + workDataOf( + "bolt11" to messageData["bolt11"].orEmpty() + ) ) .build() WorkManager.getInstance(this) @@ -72,4 +72,3 @@ internal class FcmService : FirebaseMessagingService() { Log.d(FCM, "FCM registration token refreshed: $token") } } - diff --git a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt index 6be0b8701..d25b3a05a 100644 --- a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt @@ -4,8 +4,8 @@ import android.content.Context import android.util.Log import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker -import androidx.work.Data import androidx.work.WorkerParameters +import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject import to.bitkit.Tag.FCM @@ -24,9 +24,7 @@ class Wake2PayWorker @AssistedInject constructor( warmupNode() val bolt11 = workerParams.inputData.getString("bolt11") ?: return Result.failure( - Data.Builder() - .putString("reason", "bolt11 field missing") - .build() + workDataOf("reason" to "bolt11 field missing") ) val isSuccess = LightningService.shared.payInvoice(bolt11) @@ -34,9 +32,7 @@ class Wake2PayWorker @AssistedInject constructor( Result.success() } else { Result.failure( - Data.Builder() - .putString("reason", "payment error") - .build() + workDataOf("reason" to "payment error") ) } } diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index ac782003c..01be740be 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -202,6 +202,6 @@ internal fun warmupNode() { fullScan() } }.onFailure { - Log.e(LDK, "Warmup error:", it) + Log.e(LDK, "Node warmup error", it) } } From ba5098ac315147caf231581f7abf1d369a7b9264 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 18:48:00 +0200 Subject: [PATCH 07/13] feat: Services DI and concurrency support --- .../main/java/to/bitkit/bdk/BitcoinService.kt | 12 +++++-- .../main/java/to/bitkit/di/LightningModule.kt | 15 -------- .../main/java/to/bitkit/di/ServicesModule.kt | 29 +++++++++++++++ .../java/to/bitkit/ldk/LightningService.kt | 12 +++++-- .../main/java/to/bitkit/ui/MainViewModel.kt | 35 +++++++++---------- 5 files changed, 64 insertions(+), 39 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/di/LightningModule.kt create mode 100644 app/src/main/java/to/bitkit/di/ServicesModule.kt diff --git a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt index e319be45e..595446914 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt @@ -1,6 +1,8 @@ package to.bitkit.bdk import android.util.Log +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import org.bitcoindevkit.Descriptor import org.bitcoindevkit.DescriptorSecretKey import org.bitcoindevkit.EsploraClient @@ -11,14 +13,18 @@ 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 javax.inject.Inject import kotlin.io.path.Path import kotlin.io.path.pathString -// TODO support concurrency -internal class BitcoinService { +class BitcoinService @Inject constructor( + @BgDispatcher bgDispatcher: CoroutineDispatcher, +) : BaseCoroutineScope(bgDispatcher) { companion object { val shared by lazy { - BitcoinService() + BitcoinService(Dispatchers.Default) } } diff --git a/app/src/main/java/to/bitkit/di/LightningModule.kt b/app/src/main/java/to/bitkit/di/LightningModule.kt deleted file mode 100644 index 0eba354f0..000000000 --- a/app/src/main/java/to/bitkit/di/LightningModule.kt +++ /dev/null @@ -1,15 +0,0 @@ -package to.bitkit.di - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import to.bitkit.ldk.LightningService - -@Suppress("unused") -@Module -@InstallIn(SingletonComponent::class) -abstract class LightningModule { - @Binds - abstract fun bindLightningService(service: LightningService): LightningService -} diff --git a/app/src/main/java/to/bitkit/di/ServicesModule.kt b/app/src/main/java/to/bitkit/di/ServicesModule.kt new file mode 100644 index 000000000..e1df6815b --- /dev/null +++ b/app/src/main/java/to/bitkit/di/ServicesModule.kt @@ -0,0 +1,29 @@ +@file:Suppress("unused", "UNUSED_PARAMETER") + +package to.bitkit.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import to.bitkit.bdk.BitcoinService +import to.bitkit.ldk.LightningService + +@Module +@InstallIn(SingletonComponent::class) +object ServicesModule { + @Provides + fun provideLightningService( + @BgDispatcher bgDispatcher: CoroutineDispatcher, + ): LightningService { + return LightningService.shared + } + + @Provides + fun provideBitcoinService( + @BgDispatcher bgDispatcher: CoroutineDispatcher, + ): BitcoinService { + return BitcoinService.shared + } +} diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index 01be740be..71fab54b7 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -1,6 +1,8 @@ package to.bitkit.ldk import android.util.Log +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import org.lightningdevkit.ldknode.AnchorChannelsConfig import org.lightningdevkit.ldknode.Builder import org.lightningdevkit.ldknode.Event @@ -12,13 +14,17 @@ import to.bitkit.LnPeer import to.bitkit.REST import to.bitkit.SEED import to.bitkit.Tag.LDK +import to.bitkit.async.BaseCoroutineScope import to.bitkit.bdk.BitcoinService +import to.bitkit.di.BgDispatcher +import javax.inject.Inject -// TODO support concurrency -class LightningService { +class LightningService @Inject constructor( + @BgDispatcher bgDispatcher: CoroutineDispatcher, +) : BaseCoroutineScope(bgDispatcher) { companion object { val shared by lazy { - LightningService() + LightningService(Dispatchers.Default) } } diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index b9b6ee39d..8e3b11db3 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -30,6 +30,8 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val bitcoinService: BitcoinService, + private val lightningService: LightningService, private val keychain: KeychainStore, private val appDb: AppDb, ) : ViewModel() { @@ -42,9 +44,6 @@ class MainViewModel @Inject constructor( val peers = mutableStateListOf() val channels = mutableStateListOf() - val lightningService = LightningService.shared - private val bitcoinService = BitcoinService.shared - private val node = lightningService.node init { @@ -53,7 +52,7 @@ class MainViewModel @Inject constructor( } } - fun sync() { + private fun sync() { bitcoinService.sync() ldkNodeId.value = lightningService.nodeId ldkBalance.value = lightningService.balances.totalLightningBalanceSats.toString() @@ -124,22 +123,22 @@ class MainViewModel @Inject constructor( keychain.saveString(key, "testValue") } } -} -fun MainViewModel.refresh() { - viewModelScope.launch { - "Refreshing…".also { - ldkNodeId.value = it - ldkBalance.value = it - btcAddress.value = it - btcBalance.value = it - } - peers.clear() - channels.clear() + fun refresh() { + viewModelScope.launch { + "Refreshing…".also { + ldkNodeId.value = it + ldkBalance.value = it + btcAddress.value = it + btcBalance.value = it + } + peers.clear() + channels.clear() - delay(50) - lightningService.sync() - sync() + delay(50) + lightningService.sync() + sync() + } } } From ffc37cde20182c3510dd76e83da895162910e4b0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 18:48:15 +0200 Subject: [PATCH 08/13] refactor: Move LdkMigrationTest to ldk package --- .../androidTest/java/to/bitkit/{ => ldk}/LdkMigrationTest.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename app/src/androidTest/java/to/bitkit/{ => ldk}/LdkMigrationTest.kt (93%) diff --git a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt similarity index 93% rename from app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt rename to app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt index 9540c042e..62ff72a9f 100644 --- a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt +++ b/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt @@ -1,4 +1,4 @@ -package to.bitkit +package to.bitkit.ldk import android.content.Context import androidx.test.core.app.ApplicationProvider @@ -7,8 +7,6 @@ import androidx.test.platform.app.InstrumentationRegistry 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) From 61f032d3feffbf3c68bb4a68df51c3a0a9556b57 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 19:48:57 +0200 Subject: [PATCH 09/13] feat: Performance logging utility --- app/src/main/java/to/bitkit/Constants.kt | 1 + .../main/java/to/bitkit/di/ServiceQueue.kt | 13 +++++------- app/src/main/java/to/bitkit/ext/Thread.kt | 10 +++++++++ app/src/main/java/to/bitkit/shared/Perf.kt | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ext/Thread.kt create mode 100644 app/src/main/java/to/bitkit/shared/Perf.kt diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index d72cfda11..eeb9546fb 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -16,6 +16,7 @@ internal object Tag { const val BDK = "BDK" const val DEV = "DEV" const val APP = "APP" + const val PERF = "PERF" } internal const val HOST = "10.0.2.2" diff --git a/app/src/main/java/to/bitkit/di/ServiceQueue.kt b/app/src/main/java/to/bitkit/di/ServiceQueue.kt index ea7ce78c7..8a59e7934 100644 --- a/app/src/main/java/to/bitkit/di/ServiceQueue.kt +++ b/app/src/main/java/to/bitkit/di/ServiceQueue.kt @@ -7,10 +7,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.withContext import to.bitkit.Tag.APP +import to.bitkit.ext.callerName +import to.bitkit.shared.measured import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory import kotlin.coroutines.CoroutineContext -import kotlin.system.measureTimeMillis enum class ServiceQueue { LDK, BDK, MIGRATION; @@ -24,18 +25,14 @@ enum class ServiceQueue { suspend fun background( coroutineContext: CoroutineContext = scope.coroutineContext, - functionName: String = Thread.currentThread().stackTrace[1].methodName, + functionName: String = Thread.currentThread().callerName, block: suspend CoroutineScope.() -> T, ): T { return withContext(coroutineContext) { try { - var result: T - val timeElapsed = measureTimeMillis { - result = block() + measured(functionName) { + block() } - Log.d(APP, "$functionName took ${timeElapsed / 1000.0} seconds on queue: $name") - - return@withContext result } catch (e: Exception) { Log.e(APP, "ServiceQueue.$name error", e) throw e diff --git a/app/src/main/java/to/bitkit/ext/Thread.kt b/app/src/main/java/to/bitkit/ext/Thread.kt new file mode 100644 index 000000000..b15ae7ead --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/Thread.kt @@ -0,0 +1,10 @@ +package to.bitkit.ext + +val Thread.callerName: String + get() { + // [ getThreadStackTrace, getStackTrace, this@getCallerName, background, actual caller] + val element = stackTrace[4] + val classSimpleName = element.className.substringAfterLast('.') + val methodName = element.methodName + return "$classSimpleName.$methodName" + } diff --git a/app/src/main/java/to/bitkit/shared/Perf.kt b/app/src/main/java/to/bitkit/shared/Perf.kt new file mode 100644 index 000000000..b424ef77c --- /dev/null +++ b/app/src/main/java/to/bitkit/shared/Perf.kt @@ -0,0 +1,21 @@ +package to.bitkit.shared + +import android.util.Log +import to.bitkit.Tag.PERF +import kotlin.system.measureTimeMillis + +internal inline fun measured( + functionName: String, + block: () -> T, +): T { + var result: T + + val elapsed = measureTimeMillis { + result = block() + }.let { it / 1000.0 } + + val threadName = Thread.currentThread().name + Log.v(PERF, "$functionName took $elapsed seconds on $threadName") + + return result +} From 935d925b9012c34e59032ae4bca6f91ee3becebd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 19:50:06 +0200 Subject: [PATCH 10/13] feat: LightningService concurrency --- .../java/to/bitkit/ldk/LdkMigrationTest.kt | 7 +- .../main/java/to/bitkit/LauncherActivity.kt | 3 +- .../java/to/bitkit/ldk/LightningService.kt | 69 +++++++++++-------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt index 62ff72a9f..13bbdc172 100644 --- a/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt +++ b/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt @@ -4,6 +4,7 @@ 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 @@ -25,13 +26,13 @@ class LdkMigrationTest { MigrationService(appContext).migrate(seed, manager, listOf(monitor)) with(LightningService.shared) { - init(mnemonic) - start() + setup(mnemonic) + runBlocking { start() } assertTrue { nodeId == "02cd08b7b375e4263849121f9f0ffb2732a0b88d0fb74487575ac539b374f45a55" } assertTrue { channels.isNotEmpty() } - stop() + runBlocking { stop() } } } } diff --git a/app/src/main/java/to/bitkit/LauncherActivity.kt b/app/src/main/java/to/bitkit/LauncherActivity.kt index f34d979bd..d7974a6fb 100644 --- a/app/src/main/java/to/bitkit/LauncherActivity.kt +++ b/app/src/main/java/to/bitkit/LauncherActivity.kt @@ -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 @@ -15,7 +16,7 @@ class LauncherActivity : AppCompatActivity() { super.onCreate(savedInstanceState) initNotificationChannel() logFcmToken() - warmupNode() + runBlocking { warmupNode() } startActivity(Intent(this, MainActivity::class.java)) } } diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index 71fab54b7..36817df38 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -17,6 +17,7 @@ import to.bitkit.Tag.LDK import to.bitkit.async.BaseCoroutineScope import to.bitkit.bdk.BitcoinService import to.bitkit.di.BgDispatcher +import to.bitkit.di.ServiceQueue import javax.inject.Inject class LightningService @Inject constructor( @@ -30,7 +31,7 @@ class LightningService @Inject constructor( lateinit var node: Node - fun init(mnemonic: String = SEED) { + fun setup(mnemonic: String = SEED) { val dir = Env.Storage.ldk val builder = Builder @@ -57,37 +58,51 @@ class LightningService @Inject constructor( setEntropyBip39Mnemonic(mnemonic, passphrase = null) } - Log.d(LDK, "Building node...") + Log.d(LDK, "Setting up node…") node = builder.build() - Log.i(LDK, "Node initialised.") + Log.i(LDK, "Node set up") } - fun start() { + suspend fun start() { check(::node.isInitialized) { "LDK node is not initialised" } - Log.d(LDK, "Starting node...") - node.start() + Log.d(LDK, "Starting node…") - Log.i(LDK, "Node started.") + ServiceQueue.LDK.background { + node.start() + } + + Log.i(LDK, "Node started") connectToTrustedPeers() } - fun stop() { - Log.d(LDK, "Stopping node...") - node.stop() + suspend fun stop() { + Log.d(LDK, "Stopping node…") + ServiceQueue.LDK.background { + node.stop() + } Log.i(LDK, "Node stopped.") } - private fun connectToTrustedPeers() { - for (peer in Env.trustedLnPeers) { - connectPeer(peer) + private suspend fun connectToTrustedPeers() { + ServiceQueue.LDK.background { + for (peer in Env.trustedLnPeers) { + connectPeer(peer) + } } } - fun sync() { - node.syncWallets() + suspend fun sync() { + Log.d(LDK, "Syncing node…") + + ServiceQueue.LDK.background { + node.syncWallets() + // setMaxDustHtlcExposureForCurrentChannels() + } + + Log.i(LDK, "Node synced") } // region state @@ -106,7 +121,7 @@ internal fun LightningService.connectPeer(peer: LnPeer) { val res = runCatching { node.connect(peer.nodeId, peer.address, persist = true) } - Log.d(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") + Log.i(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") } // endregion @@ -117,8 +132,8 @@ internal suspend fun LightningService.openChannel() { // sendToAddress // mine 6 blocks & wait for esplora to pick up block // wait for esplora to pick up tx - sync() + node.connectOpenChannel( nodeId = peer.nodeId, address = peer.address, @@ -131,16 +146,14 @@ internal suspend fun LightningService.openChannel() { val pendingEvent = node.nextEventAsync() check(pendingEvent is Event.ChannelPending) { "Expected ChannelPending event, got $pendingEvent" } - Log.d(LDK, "Channel pending with peer: ${peer.address}") node.eventHandled() - val fundingTxid = pendingEvent.fundingTxo.txid - Log.d(LDK, "Channel funding txid: $fundingTxid") + Log.d(LDK, "Channel pending with peer: ${peer.address}") + Log.d(LDK, "Channel funding txid: ${pendingEvent.fundingTxo.txid}") // wait for counterparty to pickup event: ChannelPending // wait for esplora to pick up tx: fundingTx // mine 6 blocks & wait for esplora to pick up block - sync() val readyEvent = node.nextEventAsync() @@ -148,8 +161,8 @@ internal suspend fun LightningService.openChannel() { node.eventHandled() // wait for counterparty to pickup event: ChannelReady - val userChannelId = readyEvent.userChannelId - Log.i(LDK, "Channel ready: $userChannelId") + + Log.i(LDK, "Channel ready: ${readyEvent.userChannelId}") } internal suspend fun LightningService.closeChannel(userChannelId: String, counterpartyNodeId: String) { @@ -157,11 +170,12 @@ internal suspend fun LightningService.closeChannel(userChannelId: String, counte val event = node.nextEventAsync() check(event is Event.ChannelClosed) { "Expected ChannelClosed event, got $event" } - Log.i(LDK, "Channel closed: $userChannelId") node.eventHandled() // mine 1 block & wait for esplora to pick up block sync() + + Log.i(LDK, "Channel closed: $userChannelId") } // endregion @@ -174,6 +188,7 @@ internal suspend fun LightningService.payInvoice(invoice: String): Boolean { Log.d(LDK, "Paying invoice: $invoice") node.bolt11Payment().send(invoice) + node.eventHandled() when (val event = node.nextEventAsync()) { is Event.PaymentSuccessful -> { @@ -191,16 +206,14 @@ internal suspend fun LightningService.payInvoice(invoice: String): Boolean { } } - node.eventHandled() - return true } // endregion -internal fun warmupNode() { +internal suspend fun warmupNode() { runCatching { LightningService.shared.apply { - init() + setup() start() sync() } From 3e7db427d4eb741ad2e7cf636147fa71ebefba99 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 20:56:24 +0200 Subject: [PATCH 11/13] feat: BitcoinService concurrency --- .../main/java/to/bitkit/bdk/BitcoinService.kt | 59 ++++++++++++------- .../java/to/bitkit/ldk/LightningService.kt | 1 + 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt index 595446914..f2ec2a84a 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt @@ -15,6 +15,7 @@ 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 @@ -33,52 +34,66 @@ class BitcoinService @Inject constructor( private var hasSynced = false private val esploraClient by lazy { EsploraClient(url = REST) } + private val dbPath by lazy { Path(Env.Storage.bdk, "db.sqlite").pathString } - private val wallet by lazy { + private lateinit var wallet: Wallet + + suspend fun setup() { val network = Env.network.bdk val mnemonic = Mnemonic.fromString(SEED) val key = DescriptorSecretKey(network, mnemonic, null) - val dbPath = Path(Env.Storage.bdk, "db.sqlite").pathString + Log.d(BDK, "Setting up wallet…") - Log.i(BDK, "Creating wallet…") + ServiceQueue.BDK.background { + wallet = Wallet( + descriptor = Descriptor.newBip84(key, KeychainKind.INTERNAL, network), + changeDescriptor = Descriptor.newBip84(key, KeychainKind.EXTERNAL, network), + persistenceBackendPath = dbPath, + network = network, + ) + } - Wallet( - descriptor = Descriptor.newBip84(key, KeychainKind.INTERNAL, network), - changeDescriptor = Descriptor.newBip84(key, KeychainKind.EXTERNAL, network), - persistenceBackendPath = dbPath, - network = network, - ) + Log.i(BDK, "Wallet set up") } - // region sync - fun sync() { + suspend fun syncWithRevealedSpks() { Log.d(BDK, "Wallet syncing…") - val request = wallet.startSyncWithRevealedSpks() - val update = esploraClient.sync(request, parallelRequests) - wallet.applyUpdate(update) + ServiceQueue.BDK.background { + val request = wallet.startSyncWithRevealedSpks() + val update = esploraClient.sync(request, parallelRequests) + wallet.applyUpdate(update) + } hasSynced = true - Log.d(BDK, "Wallet synced") + Log.i(BDK, "Wallet synced") } - fun fullScan() { + suspend fun fullScan() { Log.d(BDK, "Wallet full scan…") - val request = wallet.startFullScan() - val update = esploraClient.fullScan(request, stopGap, parallelRequests) - wallet.applyUpdate(update) - // TODO: Persist wallet once BDK is updated to beta release + 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.d(BDK, "Wallet fully scanned") + Log.i(BDK, "Wallet fully scanned") } // endregion // region state val balance get() = if (hasSynced) wallet.getBalance() else null - val address get() = wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString() + + suspend fun getAddress(): String { + return ServiceQueue.BDK.background { + val addressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL).address + addressInfo.asString() + } + } // endregion } diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index 36817df38..c26e37776 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -218,6 +218,7 @@ internal suspend fun warmupNode() { sync() } BitcoinService.shared.apply { + setup() fullScan() } }.onFailure { From 98702f6344ba2c3f14c4c1e083db7e8d77655373 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 21:00:56 +0200 Subject: [PATCH 12/13] feat: BitcoinService wipeStorage --- app/src/main/java/to/bitkit/LauncherActivity.kt | 1 + app/src/main/java/to/bitkit/bdk/BitcoinService.kt | 13 ++++++++++--- app/src/main/java/to/bitkit/ui/MainActivity.kt | 11 ++++------- app/src/main/java/to/bitkit/ui/MainViewModel.kt | 12 +++++++++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/to/bitkit/LauncherActivity.kt b/app/src/main/java/to/bitkit/LauncherActivity.kt index d7974a6fb..894aa91b8 100644 --- a/app/src/main/java/to/bitkit/LauncherActivity.kt +++ b/app/src/main/java/to/bitkit/LauncherActivity.kt @@ -16,6 +16,7 @@ class LauncherActivity : AppCompatActivity() { super.onCreate(savedInstanceState) initNotificationChannel() logFcmToken() + // TODO share mainViewModel in both activities, move warmupNode to it & call it suspending runBlocking { warmupNode() } startActivity(Intent(this, MainActivity::class.java)) } diff --git a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt index f2ec2a84a..f2574ff91 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt @@ -34,7 +34,7 @@ class BitcoinService @Inject constructor( private var hasSynced = false private val esploraClient by lazy { EsploraClient(url = REST) } - private val dbPath by lazy { Path(Env.Storage.bdk, "db.sqlite").pathString } + private val dbPath by lazy { Path(Env.Storage.bdk, "db.sqlite") } private lateinit var wallet: Wallet @@ -49,7 +49,7 @@ class BitcoinService @Inject constructor( wallet = Wallet( descriptor = Descriptor.newBip84(key, KeychainKind.INTERNAL, network), changeDescriptor = Descriptor.newBip84(key, KeychainKind.EXTERNAL, network), - persistenceBackendPath = dbPath, + persistenceBackendPath = dbPath.pathString, network = network, ) } @@ -84,7 +84,14 @@ class BitcoinService @Inject constructor( Log.i(BDK, "Wallet fully scanned") } - // endregion + + 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 diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index d3c9e3b4f..4729ba7b0 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -21,7 +21,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.NotificationAdd import androidx.compose.material.icons.filled.NotificationsNone import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.Button import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -30,6 +29,7 @@ import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -65,12 +65,9 @@ class MainActivity : ComponentActivity() { MainScreen(viewModel) { WalletScreen(viewModel) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - Button(onClick = viewModel::debugDb) { - Text(text = "Debug DB") - } - Button(onClick = viewModel::debugKeychain) { - Text(text = "Debug Keychain") - } + TextButton(onClick = viewModel::debugDb) { Text(text = "Debug DB") } + TextButton(onClick = viewModel::debugKeychain) { Text(text = "Debug Keychain") } + TextButton(onClick = viewModel::debugWipeBdk) { Text(text = "Wipe BDK") } } Peers(viewModel.peers, viewModel::togglePeerConnection) diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index 8e3b11db3..102762ee2 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -52,11 +52,11 @@ class MainViewModel @Inject constructor( } } - private fun sync() { - bitcoinService.sync() + private suspend fun sync() { + bitcoinService.syncWithRevealedSpks() ldkNodeId.value = lightningService.nodeId ldkBalance.value = lightningService.balances.totalLightningBalanceSats.toString() - btcAddress.value = bitcoinService.address + btcAddress.value = bitcoinService.getAddress() btcBalance.value = bitcoinService.balance?.total?.toSat().toString() mnemonic.value = SEED peers.syncTo(lightningService.peers) @@ -106,6 +106,7 @@ class MainViewModel @Inject constructor( } } + // region debug fun debugDb() { viewModelScope.launch { appDb.configDao().getAll().collect { @@ -124,6 +125,11 @@ class MainViewModel @Inject constructor( } } + fun debugWipeBdk() { + bitcoinService.wipeStorage() + } + // endregion + fun refresh() { viewModelScope.launch { "Refreshing…".also { From 076a5deab77ae925ae7f754192a9a54c500c6a6a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Aug 2024 21:47:56 +0200 Subject: [PATCH 13/13] refactor: ServiceQueue scope as lazy prop It only gets called once, so no expensive thread/queue creation would occur too often. --- app/src/main/java/to/bitkit/di/ServiceQueue.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/di/ServiceQueue.kt b/app/src/main/java/to/bitkit/di/ServiceQueue.kt index 8a59e7934..be333b28d 100644 --- a/app/src/main/java/to/bitkit/di/ServiceQueue.kt +++ b/app/src/main/java/to/bitkit/di/ServiceQueue.kt @@ -16,12 +16,7 @@ import kotlin.coroutines.CoroutineContext enum class ServiceQueue { LDK, BDK, MIGRATION; - private val scope: CoroutineScope - get() = when (this) { - LDK -> ldkScope - BDK -> bdkScope - MIGRATION -> migrationScope - } + private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) } suspend fun background( coroutineContext: CoroutineContext = scope.coroutineContext, @@ -41,10 +36,6 @@ enum class ServiceQueue { } companion object { - private val ldkScope by lazy { CoroutineScope(dispatcher("ldk-queue") + SupervisorJob()) } - private val bdkScope by lazy { CoroutineScope(dispatcher("bdk-queue") + SupervisorJob()) } - private val migrationScope by lazy { CoroutineScope(dispatcher("migration-queue") + SupervisorJob()) } - private fun dispatcher(name: String): ExecutorCoroutineDispatcher { val threadFactory = ThreadFactory { Thread(it, name).apply { priority = Thread.NORM_PRIORITY - 1 } } return Executors.newSingleThreadExecutor(threadFactory).asCoroutineDispatcher()