diff --git a/.editorconfig b/.editorconfig index 88e426e7e..73648402f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,7 @@ indent_size = 4 [*.md] max_line_length = off trim_trailing_whitespace = false + +[*.json] +max_line_length = off +indent_size = 2 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1ec10b90..b3b0c397c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,26 +1,23 @@ plugins { - id("com.android.application") - id("kotlin-android") - id("org.jetbrains.kotlin.plugin.serialization") - id("com.google.devtools.ksp") - id("com.google.dagger.hilt.android") - id("com.google.gms.google-services") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt.android) + alias(libs.plugins.google.services) + alias(libs.plugins.room) } - android { namespace = "to.bitkit" compileSdk = 34 - ndkVersion = "26.1.10909125" - + ndkVersion = "26.1.10909125" // probably required by LDK bindings? - safer to keep it for now. defaultConfig { applicationId = "to.bitkit" minSdk = 28 targetSdk = 34 versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { useSupportLibrary = true } @@ -36,7 +33,7 @@ android { buildTypes { debug { signingConfig = signingConfigs.getByName("debug") - // applicationIdSuffix = ".dev" + applicationIdSuffix = ".dev" } release { isMinifyEnabled = false @@ -47,17 +44,18 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } buildFeatures { buildConfig = true compose = true } composeOptions { + // https://developer.android.com/jetpack/androidx/releases/compose-kotlin#pre-release_kotlin_compatibility kotlinCompilerExtensionVersion = "1.5.14" } packaging { @@ -66,76 +64,73 @@ android { } } } - dependencies { implementation(fileTree("libs") { include("*.aar") }) - - // BDK & LDK - implementation("org.bitcoindevkit:bdk-android:0.30.0") - implementation("org.lightningdevkit:ldk-node-android:0.3.0") - - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.activity:activity-compose:1.9.1") - + implementation(libs.core.ktx) + implementation(libs.appcompat) + implementation(libs.activity.compose) + implementation(libs.material) + // BDK + LDK + implementation(libs.bdk.android) + implementation(libs.ldk.node.android) // Firebase - implementation(platform("com.google.firebase:firebase-bom:33.1.2")) - implementation("com.google.firebase:firebase-messaging") - implementation("com.google.firebase:firebase-analytics") - + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.messaging) + implementation(libs.firebase.analytics) // Lifecycle - val lifecycleVersion = "2.8.4" - // ViewModel - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion") // ViewModel utils for Compose - implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") // LiveData - implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") // Lifecycles wo ViewModel/LiveData - implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion") // Lifecycle utils for Compose - implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycleVersion") // Saved state for ViewModel - + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.viewmodel.compose) // ViewModel utils for Compose + implementation(libs.lifecycle.livedata.ktx) // LiveData + implementation(libs.lifecycle.runtime.ktx) // Lifecycles wo ViewModel/LiveData + implementation(libs.lifecycle.runtime.compose) // Lifecycle utils for Compose + implementation(libs.lifecycle.viewmodel.savedstate) // Saved state for ViewModel + // Compose + implementation(platform(libs.compose.bom)) + androidTestImplementation(platform(libs.compose.bom)) + implementation(libs.material3) + implementation(libs.material.icons.extended) + implementation(libs.ui.tooling.preview) + debugImplementation(libs.ui.tooling) + debugImplementation(libs.ui.test.manifest) + androidTestImplementation(libs.ui.test.junit4) // Compose Navigation - val composeNavigationVersion = "2.7.7" - implementation("androidx.navigation:navigation-compose:$composeNavigationVersion") - androidTestImplementation("androidx.navigation:navigation-testing:$composeNavigationVersion") - implementation("androidx.hilt:hilt-navigation-compose:1.2.0") - - // Compose Tooling for Android Studio Preview - val composeToolingVersion = "1.6.8" - implementation("androidx.compose.ui:ui-tooling-preview:$composeToolingVersion") - debugImplementation("androidx.compose.ui:ui-tooling:$composeToolingVersion") - - // Dagger-Hilt - val hiltVersion = "2.51.1" - implementation("com.google.dagger:hilt-android:$hiltVersion") - ksp("com.google.dagger:hilt-android-compiler:$hiltVersion") - ksp("androidx.hilt:hilt-compiler:1.2.0") - + implementation(libs.navigation.compose) + androidTestImplementation(libs.navigation.testing) + implementation(libs.hilt.navigation.compose) + // Hilt - DI + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + ksp(libs.hilt.compiler) // WorkManager - implementation("androidx.hilt:hilt-work:1.2.0") - implementation("androidx.work:work-runtime-ktx:2.9.0") - - // Material Design - implementation("com.google.android.material:material:1.12.0") - implementation("androidx.compose.material3:material3:1.2.1") - implementation("androidx.compose.material:material-icons-extended:1.7.0-beta06") - - // Ktor - val ktorVersion = "2.3.8" - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-okhttp:$ktorVersion") - implementation("io.ktor:ktor-client-cio:$ktorVersion") - implementation("io.ktor:ktor-client-logging:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("ch.qos.logback:logback-classic:1.2.11") - - // Testing - debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.8") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") - androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.8") - + implementation(libs.hilt.work) + implementation(libs.work.runtime.ktx) + // Ktor - Networking + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + debugImplementation(libs.slf4j.simple) + // Room - DB + implementation(libs.room.ktx) + implementation(libs.room.runtime) + ksp(libs.room.compiler) + testImplementation(libs.room.testing) + // Test + Debug + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.junit.ext) + androidTestImplementation(libs.kotlin.test.junit) + testImplementation(libs.junit) + testImplementation(libs.kotlin.test.junit) // Other - implementation("com.google.guava:guava:31.1-android") // for ByteArray.toHex()+ + implementation(libs.guava) // for ByteArray.toHex()+ +} +ksp { + // cool but strict: https://developer.android.com/jetpack/androidx/releases/room#2.6.0 + // arg("room.generateKotlin", "true") +} +// https://developer.android.com/jetpack/androidx/releases/room#gradle-plugin +room { + schemaDirectory("$projectDir/schemas") } diff --git a/app/libs/LDK-release.aar b/app/libs/LDK-release.aar new file mode 100644 index 000000000..e0b883249 Binary files /dev/null and b/app/libs/LDK-release.aar differ diff --git a/app/schemas/to.bitkit.data.AppDb/1.json b/app/schemas/to.bitkit.data.AppDb/1.json new file mode 100644 index 000000000..4b5011545 --- /dev/null +++ b/app/schemas/to.bitkit.data.AppDb/1.json @@ -0,0 +1,34 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "ea0d5b36d92a5a3fb1523c3064686f7d", + "entities": [ + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletIndex` INTEGER NOT NULL, PRIMARY KEY(`walletIndex`))", + "fields": [ + { + "fieldPath": "walletIndex", + "columnName": "walletIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletIndex" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea0d5b36d92a5a3fb1523c3064686f7d')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/assets/ldk-backup/manager.bin b/app/src/androidTest/assets/ldk-backup/manager.bin new file mode 100644 index 000000000..c95428d20 Binary files /dev/null and b/app/src/androidTest/assets/ldk-backup/manager.bin differ diff --git a/app/src/androidTest/assets/ldk-backup/monitor.bin b/app/src/androidTest/assets/ldk-backup/monitor.bin new file mode 100644 index 000000000..b63e3f645 Binary files /dev/null and b/app/src/androidTest/assets/ldk-backup/monitor.bin differ diff --git a/app/src/androidTest/assets/ldk-backup/seed.bin b/app/src/androidTest/assets/ldk-backup/seed.bin new file mode 100644 index 000000000..754d9499d --- /dev/null +++ b/app/src/androidTest/assets/ldk-backup/seed.bin @@ -0,0 +1 @@ +nì õçJÓL¦Çm¨ÀΚÚþL&‰ÚíéÇ¥Ó4¬œ~Þ \ No newline at end of file diff --git a/app/src/androidTest/java/to/bitkit/ExampleInstrumentedTest.kt b/app/src/androidTest/java/to/bitkit/ExampleInstrumentedTest.kt deleted file mode 100644 index d269b3e82..000000000 --- a/app/src/androidTest/java/to/bitkit/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package to.bitkit - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals(BuildConfig.APPLICATION_ID, appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt new file mode 100644 index 000000000..88664529f --- /dev/null +++ b/app/src/androidTest/java/to/bitkit/LdkMigrationTest.kt @@ -0,0 +1,38 @@ +package to.bitkit + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +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) +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 } + + @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") + + MigrationService(appContext).migrate(seed, manager, listOf(monitor)) + + with(LightningService()) { + init(mnemonic) + start() + + assertTrue { nodeId == "02cd08b7b375e4263849121f9f0ffb2732a0b88d0fb74487575ac539b374f45a55" } + assertTrue { channels.isNotEmpty() } + + stop() + } + } +} diff --git a/app/src/main/java/to/bitkit/App.kt b/app/src/main/java/to/bitkit/App.kt index 77b6a296b..88a467eea 100644 --- a/app/src/main/java/to/bitkit/App.kt +++ b/app/src/main/java/to/bitkit/App.kt @@ -23,6 +23,8 @@ internal class App : Application(), Configuration.Provider { override fun onCreate() { super.onCreate() currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) } + + Env.LdkStorage.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 6db142a86..ec28359d1 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -1,26 +1,36 @@ +@file:Suppress("unused") + package to.bitkit import android.util.Log import to.bitkit.Tag.LDK +import to.bitkit.env.Network +import to.bitkit.ext.ensureDir import kotlin.io.path.Path -import org.bitcoindevkit.Network as BdkNetwork import org.lightningdevkit.ldknode.Network as LdkNetwork -object Tag { - internal const val FCM = "FCM" - internal const val LDK = "LDK" - internal const val BDK = "BDK" - internal const val DEV = "DEV" - internal const val APP = "APP" +internal object Tag { + const val FCM = "FCM" + const val LDK = "LDK" + const val BDK = "BDK" + const val DEV = "DEV" + const val APP = "APP" } internal const val HOST = "10.0.2.2" internal const val REST = "https://electrs-regtest.synonym.to" internal const val SEED = "universe more push obey later jazz huge buzz magnet team muscle robust" + +internal val PEER_REMOTE = LnPeer( + nodeId = "033f4d3032ce7f54224f4bd9747b50b7cd72074a859758e40e1ca46ffa79a34324", + host = HOST, + port = "9736", +) + internal val PEER = LnPeer( nodeId = "02faf2d1f5dc153e8931d8444c4439e46a81cb7eeadba8562e7fec3690c261ce87", host = HOST, - port = "9736", + port = "9737", ) internal object Env { @@ -31,30 +41,27 @@ internal object Env { fun init(base: String): String { require(base.isNotEmpty()) { "Base path for LDK storage cannot be empty" } - return Path(base, Network.ldk.name.lowercase(), "ldk") + if (::path.isInitialized) { + Log.w(LDK, "Storage path already set: $path") + } + path = Path(base, network.id, "ldk") .toFile() - // .also { - // if (!it.mkdirs()) throw Error("Cannot create LDK data directory") - // } + .ensureDir() .absolutePath - .also { - path = it - Log.d(LDK, "Storage path: $it") - } + Log.d(LDK, "Storage path: $path") + return path } } - object Network { - val ldk: LdkNetwork = LdkNetwork.REGTEST - val bdk = BdkNetwork.REGTEST - } + val network = Network.Regtest val trustedLnPeers = listOf( - PEER, + PEER_REMOTE, + // PEER, ) val ldkRgsServerUrl: String? - get() = when (Network.ldk) { + get() = when (network.ldk) { LdkNetwork.BITCOIN -> "https://rapidsync.lightningdevkit.org/snapshot/" else -> null } @@ -71,6 +78,6 @@ data class LnPeer( address.substringAfter(":"), ) - fun address() = "$host:$port" - override fun toString() = "$nodeId@${address()}" + val address get() = "$host:$port" + override fun toString() = "$nodeId@${address}" } diff --git a/app/src/main/java/to/bitkit/LauncherActivity.kt b/app/src/main/java/to/bitkit/LauncherActivity.kt index 92bc5c022..f34d979bd 100644 --- a/app/src/main/java/to/bitkit/LauncherActivity.kt +++ b/app/src/main/java/to/bitkit/LauncherActivity.kt @@ -3,17 +3,19 @@ package to.bitkit import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint import to.bitkit.ldk.warmupNode import to.bitkit.ui.MainActivity import to.bitkit.ui.initNotificationChannel import to.bitkit.ui.logFcmToken +@AndroidEntryPoint class LauncherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initNotificationChannel() logFcmToken() - warmupNode(filesDir.absolutePath) + 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 a55b94630..052c9424e 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt @@ -25,7 +25,7 @@ internal class BitcoinService { } private val wallet by lazy { - val network = Env.Network.bdk + val network = Env.network.bdk val mnemonic = Mnemonic.fromString(SEED) val key = DescriptorSecretKey(network, mnemonic, null) diff --git a/app/src/main/java/to/bitkit/data/AppDb.kt b/app/src/main/java/to/bitkit/data/AppDb.kt new file mode 100644 index 000000000..371d4ba5d --- /dev/null +++ b/app/src/main/java/to/bitkit/data/AppDb.kt @@ -0,0 +1,82 @@ +package to.bitkit.data + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Upsert +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import to.bitkit.BuildConfig +import to.bitkit.Env +import to.bitkit.data.entities.ConfigEntity + +@Database( + entities = [ + ConfigEntity::class, + ], + version = 1, +) +abstract class AppDb : RoomDatabase() { + abstract fun configDao(): ConfigDao + + companion object { + private val NAME = "${BuildConfig.APPLICATION_ID}.${Env.network.id}.sqlite" + + @Volatile + private var instance: AppDb? = null + + fun getInstance(context: Context): AppDb { + return instance ?: synchronized(this) { + instance ?: buildDatabase(context).also { + instance = it + } + } + } + + private fun buildDatabase(context: Context): AppDb { + return Room.databaseBuilder(context, AppDb::class.java, NAME) + .setJournalMode(JournalMode.TRUNCATE) + .addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + val request = OneTimeWorkRequestBuilder().build() + WorkManager.getInstance(context).enqueue(request) + } + }) + .build() + } + } +} + +@Dao +interface ConfigDao { + @Query("SELECT * FROM config") + fun getAll(): Flow> + + @Upsert + suspend fun upsert(vararg entities: ConfigEntity) +} + +internal class SeedDbWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { + override suspend fun doWork(): Result = coroutineScope { + try { + val db = AppDb.getInstance(applicationContext) + db.configDao().upsert( + ConfigEntity( + walletIndex = 0L, + ), + ) + Result.success() + } catch (ex: Exception) { + Result.failure() + } + } +} diff --git a/app/src/main/java/to/bitkit/data/entities/ConfigEntity.kt b/app/src/main/java/to/bitkit/data/entities/ConfigEntity.kt new file mode 100644 index 000000000..34dcde8d0 --- /dev/null +++ b/app/src/main/java/to/bitkit/data/entities/ConfigEntity.kt @@ -0,0 +1,9 @@ +package to.bitkit.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "config") +data class ConfigEntity( + @PrimaryKey val walletIndex: Long = 0, +) diff --git a/app/src/main/java/to/bitkit/di/DbModule.kt b/app/src/main/java/to/bitkit/di/DbModule.kt new file mode 100644 index 000000000..5a08c8db7 --- /dev/null +++ b/app/src/main/java/to/bitkit/di/DbModule.kt @@ -0,0 +1,23 @@ +package to.bitkit.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import to.bitkit.data.AppDb +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object DbModule { + + @Provides + @Singleton + fun provideAppDb( + @ApplicationContext applicationContext: Context, + ): AppDb { + return AppDb.getInstance(applicationContext) + } +} diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index 8c5ae6c5e..1db00918b 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -6,7 +6,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.DEFAULT +import io.ktor.client.plugins.logging.ANDROID import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging @@ -34,7 +34,7 @@ object HttpModule { fun provideHttpClient(json: Json): HttpClient { return HttpClient { install(Logging) { - logger = Logger.DEFAULT + logger = Logger.ANDROID level = LogLevel.BODY } install(ContentNegotiation) { @@ -48,4 +48,4 @@ object HttpModule { fun provideRestApi(esploraApi: EsploraApi): RestApi { return esploraApi } -} \ No newline at end of file +} diff --git a/app/src/main/java/to/bitkit/env/Network.kt b/app/src/main/java/to/bitkit/env/Network.kt new file mode 100644 index 000000000..a3feb925e --- /dev/null +++ b/app/src/main/java/to/bitkit/env/Network.kt @@ -0,0 +1,19 @@ +package to.bitkit.env + +import org.bitcoindevkit.Network as BdkNetwork +import org.lightningdevkit.ldknode.Network as LdkNetwork + +object Network { + + data class Network( + val id: String, + val ldk: LdkNetwork, + val bdk: BdkNetwork, + ) + + val Regtest = Network( + id = "regtest", + ldk = LdkNetwork.REGTEST, + bdk = BdkNetwork.REGTEST, + ) +} diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index ed401e5a0..6f30de4a8 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package to.bitkit.ext import com.google.common.io.BaseEncoding @@ -8,14 +10,24 @@ fun ByteArray.toHex(): String { return BaseEncoding.base16().encode(this).lowercase() } -fun String.toByteArray(): ByteArray { +// TODO check if this can be replaced with existing ByteArray.toHex() +val ByteArray.hex: String get() = joinToString("") { "%02x".format(it) } + +fun String.asByteArray(): ByteArray { return BaseEncoding.base16().decode(this.uppercase()) } -fun convertToByteArray(obj: Any): ByteArray { +val String.hex: ByteArray get() { + check(length % 2 == 0) { "Cannot convert string of uneven length to hex ByteArray: $this" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() +} + +fun Any.convertToByteArray(): ByteArray { val bos = ByteArrayOutputStream() val oos = ObjectOutputStream(bos) - oos.writeObject(obj) + oos.writeObject(this) oos.flush() return bos.toByteArray() -} \ No newline at end of file +} diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index 56d88b22b..d2318bc91 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -1,14 +1,22 @@ +@file:Suppress("unused") + package to.bitkit.ext import android.app.NotificationManager import android.content.Context import android.content.Context.NOTIFICATION_SERVICE import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.util.Log import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import to.bitkit.Tag.APP import to.bitkit.currentActivity import to.bitkit.ui.MainActivity +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream // System Services @@ -33,3 +41,25 @@ internal fun toast( Toast.makeText(this, text, duration).show() } } + +// File System + +fun Context.readAsset(path: String) = assets.open(path).use(InputStream::readBytes) + +fun Context.copyAssetToStorage(asset: String, dest: String) { + val destFile = File(dest) + + try { + this.assets.open(asset).use { inputStream -> + FileOutputStream(destFile).use { outputStream -> + val buffer = ByteArray(1024) + var length: Int + while (inputStream.read(buffer).also { length = it } > 0) { + outputStream.write(buffer, 0, length) + } + } + } + } catch (e: IOException) { + Log.e(APP, "Failed to copy asset file: $asset", e) + } +} diff --git a/app/src/main/java/to/bitkit/ext/FileSystem.kt b/app/src/main/java/to/bitkit/ext/FileSystem.kt new file mode 100644 index 000000000..2be98849b --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/FileSystem.kt @@ -0,0 +1,11 @@ +package to.bitkit.ext + +import java.io.File +import kotlin.io.path.exists + +fun File.ensureDir() = this.also { + if (toPath().exists()) return this + + val path = if (extension.isEmpty()) this else parentFile + if (!path.mkdirs()) throw Error("Cannot create path: $this") +} diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index 64c0a2969..345b151dd 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -2,7 +2,7 @@ package to.bitkit.fcm import android.util.Log import androidx.work.Data -import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage @@ -50,7 +50,7 @@ internal class FcmService : FirebaseMessagingService() { * Schedule async work via WorkManager for tasks of 10+ seconds. */ private fun scheduleJob(messageData: Map) { - val work = OneTimeWorkRequest.Builder(Wake2PayWorker::class.java) + val work = OneTimeWorkRequestBuilder() .setInputData( Data.Builder() .putString("bolt11", messageData["bolt11"].orEmpty()) diff --git a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt index 27436a8ed..6be0b8701 100644 --- a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt @@ -21,7 +21,7 @@ class Wake2PayWorker @AssistedInject constructor( override suspend fun doWork(): Result { Log.d(FCM, "Node wakeup from notification…") - warmupNode(appContext.filesDir.absolutePath) + warmupNode() val bolt11 = workerParams.inputData.getString("bolt11") ?: return Result.failure( Data.Builder() diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index 2b6ed383e..b06a226e0 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -23,22 +23,23 @@ class LightningService { lateinit var node: Node - fun init(cwd: String) { - val dir = Env.LdkStorage.init(cwd) - - val builder = Builder.fromConfig( - defaultConfig().apply { - storageDirPath = dir - logDirPath = dir - network = Env.Network.ldk - logLevel = LogLevel.TRACE - - trustedPeers0conf = Env.trustedLnPeers.map { it.nodeId } - anchorChannelsConfig = AnchorChannelsConfig( - trustedPeersNoReserve = trustedPeers0conf, - perChannelReserveSats = 2000u, // TODO set correctly - ) - }) + fun init(mnemonic: String = SEED) { + val dir = Env.LdkStorage.path + + val builder = Builder + .fromConfig( + defaultConfig().apply { + storageDirPath = dir + logDirPath = dir + network = Env.network.ldk + logLevel = LogLevel.TRACE + + trustedPeers0conf = Env.trustedLnPeers.map { it.nodeId } + anchorChannelsConfig = AnchorChannelsConfig( + trustedPeersNoReserve = trustedPeers0conf, + perChannelReserveSats = 2000u, // TODO set correctly + ) + }) .apply { setEsploraServer(REST) if (Env.ldkRgsServerUrl != null) { @@ -46,7 +47,7 @@ class LightningService { } else { setGossipSourceP2p() } - setEntropyBip39Mnemonic(mnemonic = SEED, passphrase = null) + setEntropyBip39Mnemonic(mnemonic, passphrase = null) } Log.d(LDK, "Building node...") @@ -66,6 +67,12 @@ class LightningService { connectToTrustedPeers() } + fun stop() { + Log.d(LDK, "Stopping node...") + node.stop() + Log.i(LDK, "Node stopped.") + } + private fun connectToTrustedPeers() { for (peer in Env.trustedLnPeers) { connectPeer(peer) @@ -88,7 +95,7 @@ class LightningService { internal fun LightningService.connectPeer(peer: LnPeer) { Log.d(LDK, "Connecting peer: $peer") val res = runCatching { - node.connect(peer.nodeId, peer.address(), persist = true) + node.connect(peer.nodeId, peer.address, persist = true) } Log.d(LDK, "Connection ${if (res.isSuccess) "succeeded" else "failed"} with: $peer") } @@ -126,7 +133,7 @@ internal suspend fun LightningService.openChannel() { sync() val readyEvent = node.nextEventAsync() - check(readyEvent is Event.ChannelReady) + check(readyEvent is Event.ChannelReady) { "Expected ChannelReady event, got $readyEvent" } node.eventHandled() // wait for counterparty to pickup event: ChannelReady @@ -138,7 +145,7 @@ internal suspend fun LightningService.closeChannel(userChannelId: String, counte node.closeChannel(userChannelId, counterpartyNodeId) val event = node.nextEventAsync() - check(event is Event.ChannelClosed) + check(event is Event.ChannelClosed) { "Expected ChannelClosed event, got $event" } Log.i(LDK, "Channel closed: $userChannelId") node.eventHandled() @@ -155,22 +162,31 @@ internal suspend fun LightningService.payInvoice(invoice: String): Boolean { node.bolt11Payment().send(invoice) - val event = node.nextEventAsync() - if (event is Event.PaymentSuccessful) { - Log.i(LDK, "Payment successful for invoice: $invoice") - } else if (event is Event.PaymentFailed) { - Log.e(LDK, "Payment error: ${event.reason}") - return false + when (val event = node.nextEventAsync()) { + is Event.PaymentSuccessful -> { + Log.i(LDK, "Payment successful for invoice: $invoice") + } + + is Event.PaymentFailed -> { + Log.e(LDK, "Payment error: ${event.reason}") + return false + } + + else -> { + Log.e(LDK, "Expected PaymentSuccessful/PaymentFailed event, got $event") + return false + } } + node.eventHandled() return true } -internal fun warmupNode(cwd: String) { +internal fun warmupNode() { runCatching { LightningService.shared.apply { - init(cwd) + init() start() sync() } diff --git a/app/src/main/java/to/bitkit/ldk/MigrationService.kt b/app/src/main/java/to/bitkit/ldk/MigrationService.kt new file mode 100644 index 000000000..9bbf074a5 --- /dev/null +++ b/app/src/main/java/to/bitkit/ldk/MigrationService.kt @@ -0,0 +1,122 @@ +package to.bitkit.ldk + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext +import org.ldk.structs.KeysManager +import to.bitkit.Env +import to.bitkit.Tag.LDK +import to.bitkit.ext.hex +import java.io.File +import javax.inject.Inject +import kotlin.io.path.Path +import org.ldk.structs.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK as ChannelMonitorDecodeResultTuple +import org.ldk.structs.UtilMethods.C2Tuple_ThirtyTwoBytesChannelMonitorZ_read as read32BytesChannelMonitor + +class MigrationService @Inject constructor( + @ApplicationContext private val context: Context, +) { + fun migrate(seed: ByteArray, manager: ByteArray, monitors: List) { + Log.d(LDK, "Migrating LDK backup…") + + val file = Path(Env.LdkStorage.path, LDK_DB_NAME).toFile() + + // Skip if db already exists + if (file.exists()) { + Log.d(LDK, "Migration skipped: ldk-node db exists at: $file") + return + } + + val path = file.path + Log.d(LDK, "Creating ldk-node db at: $path") + Log.d(LDK, "Seeding ldk-node db with LDK backup data…") + + LdkNodeDataDbHelper(context, path).writableDatabase.use { + it.beginTransaction() + try { + it.insertManager(manager) + it.insertMonitors(seed, monitors) + + it.execSQL("DROP TABLE IF EXISTS android_metadata") + it.setTransactionSuccessful() + } finally { + it.endTransaction() + } + } + + File("$path-journal").delete() + + Log.i(LDK, "Migrated LDK backup to ldk-node db at: $path") + } + + private fun SQLiteDatabase.insertManager(manager: ByteArray) { + val values = ContentValues().apply { + put(PRIMARY_NAMESPACE, "") + put(SECONDARY_NAMESPACE, "") + put(KEY, "manager") + put(VALUE, manager) + } + insert(LDK_NODE_DATA, null, values) + } + + private fun SQLiteDatabase.insertMonitors(seed: ByteArray, monitors: List) { + val seconds = System.currentTimeMillis() / 1000L + val nanoSeconds = (seconds * 1000 * 1000).toInt() + + val keysManager = KeysManager.of(seed, seconds, nanoSeconds) + val (entropySource, signerProvider) = keysManager.as_EntropySource() to keysManager.as_SignerProvider() + + for (monitor in monitors) { + val channelMonitor = read32BytesChannelMonitor(monitor, entropySource, signerProvider).takeIf { it.is_ok } + ?.let { it as? ChannelMonitorDecodeResultTuple }?.res?._b + ?: throw Error("Could not read channel monitor using read32BytesChannelMonitor") + val fundingTx = channelMonitor._funding_txo._a._txid?.reversedArray()?.hex + ?: throw Error("Could not read txid from funding tx OutPoint of channel monitor") + val index = channelMonitor._funding_txo._a._index + val key = "${fundingTx}_$index" + + val values = ContentValues().apply { + put(PRIMARY_NAMESPACE, "monitors") + put(SECONDARY_NAMESPACE, "") + put(KEY, key) + put(VALUE, monitor) + } + insert(LDK_NODE_DATA, null, values) + + Log.d(LDK, "Inserted monitor: $key") + } + } + + companion object { + private const val LDK_NODE_DATA = "ldk_node_data" + private const val PRIMARY_NAMESPACE = "primary_namespace" + private const val SECONDARY_NAMESPACE = "secondary_namespace" + private const val KEY = "key" + private const val VALUE = "value" + private const val LDK_DB_NAME = "$LDK_NODE_DATA.sqlite" + } +} + +private class LdkNodeDataDbHelper(context: Context, name: String) : SQLiteOpenHelper(context, name, null, VERSION) { + override fun onCreate(db: SQLiteDatabase) { + val query = """ + |CREATE TABLE ldk_node_data ( + | primary_namespace TEXT NOT NULL, + | secondary_namespace TEXT DEFAULT "" NOT NULL, + | `key` TEXT NOT NULL CHECK (`key` <> ''), + | value BLOB, + | PRIMARY KEY (primary_namespace, secondary_namespace, `key`) + |); + """.trimMargin() + db.execSQL(query) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit + + companion object { + const val VERSION = 2 + } +} diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 7f7b99d0d..25407b49b 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -19,6 +20,7 @@ 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 @@ -61,6 +63,12 @@ class MainActivity : ComponentActivity() { AppThemeSurface { MainScreen(viewModel) { WalletScreen(viewModel) { + Row { + Button(onClick = viewModel::debugDb) { + Text(text = "Debug DB") + } + } + Peers(viewModel.peers, viewModel::togglePeerConnection) Channels(viewModel.channels, viewModel::closeChannel) } @@ -200,4 +208,3 @@ private fun NotificationButton() { ) } } - diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index ece3c75a9..66a37cb4a 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -1,5 +1,6 @@ package to.bitkit.ui +import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -12,7 +13,9 @@ import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.LnPeer import to.bitkit.SEED +import to.bitkit.Tag.DEV import to.bitkit.bdk.BitcoinService +import to.bitkit.data.AppDb import to.bitkit.di.BgDispatcher import to.bitkit.ext.syncTo import to.bitkit.ldk.LightningService @@ -26,6 +29,7 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val appDb: AppDb, ) : ViewModel() { val ldkNodeId = mutableStateOf("Loading…") val ldkBalance = mutableStateOf("Loading…") @@ -99,6 +103,14 @@ class MainViewModel @Inject constructor( sync() } } + + fun debugDb() { + viewModelScope.launch { + appDb.configDao().getAll().collect { + Log.d(DEV, "${it.count()} entities in DB: $it") + } + } + } } fun MainViewModel.refresh() { diff --git a/app/src/main/java/to/bitkit/ui/shared/Channels.kt b/app/src/main/java/to/bitkit/ui/shared/Channels.kt index 624783299..5ee93b876 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Channels.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Channels.kt @@ -109,8 +109,8 @@ private fun ChannelItem( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth(), ) { - Text(text = "$outbound sats", style = MaterialTheme.typography.labelSmall) - Text(text = "$inbound sats", style = MaterialTheme.typography.labelSmall) + Text(moneyString("$outbound"), style = MaterialTheme.typography.labelSmall) + Text(moneyString("$inbound"), style = MaterialTheme.typography.labelSmall) } Row(verticalAlignment = Alignment.CenterVertically) { val (icon, color) = Pair( diff --git a/app/src/main/java/to/bitkit/ui/shared/Peers.kt b/app/src/main/java/to/bitkit/ui/shared/Peers.kt index 4659e8076..dc26210ea 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Peers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Peers.kt @@ -45,23 +45,21 @@ internal fun Peers( style = MaterialTheme.typography.titleMedium, ) } - Column { - peers.sortedBy { it.isConnected }.forEachIndexed { i, it -> - if (i > 0 && peers.size > 1) { - HorizontalDivider() - } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - TogglePeerIcon(it.isConnected) { onToggle(it) } - Text( - text = it.nodeId, - style = MaterialTheme.typography.labelSmall, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } + peers.sortedByDescending { it.isConnected }.forEachIndexed { i, it -> + if (i > 0 && peers.size > 1) { + HorizontalDivider() + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + TogglePeerIcon(it.isConnected) { onToggle(it) } + Text( + text = it.nodeId, + style = MaterialTheme.typography.labelSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/shared/Text.kt b/app/src/main/java/to/bitkit/ui/shared/Text.kt index fd42847dc..55cae0a0f 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Text.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Text.kt @@ -8,13 +8,12 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import to.bitkit.R - @Composable internal fun moneyString( - ldkBalance: String, + value: String, currency: String = stringResource(R.string.sat), ) = buildAnnotatedString { - append("$ldkBalance ") + append("$value ") withStyle(SpanStyle(color = colorScheme.onBackground.copy(0.5f))) { append(currency) } diff --git a/build.gradle.kts b/build.gradle.kts index 0ea7d8f6d..0b22f4bc8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,16 +1,10 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - val kotlinVersion = "1.9.24" - val hiltVersion = "2.51.1" - id("com.android.application") version "8.5.1" apply false - id("org.jetbrains.kotlin.android") version kotlinVersion apply false - // id("org.jetbrains.kotlin.jvm") version kotlinVersion apply false // for room db - id("org.jetbrains.kotlin.plugin.serialization") version kotlinVersion apply false - id("com.google.gms.google-services") version "4.4.2" apply false - - // https://github.com/google/ksp/releases - id("com.google.devtools.ksp") version "$kotlinVersion-1.0.20" apply false - - // https://github.com/google/dagger/releases/ - id("com.google.dagger.hilt.android") version hiltVersion apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.hilt.android) apply false // https://github.com/google/dagger/releases/ + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.ksp) apply false // https://github.com/google/ksp/releases + alias(libs.plugins.room) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..23b55e5db --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,85 @@ +[versions] +activityCompose = "1.9.1" +agp = "8.5.2" +appcompat = "1.7.0" +bdk = "0.31.1" +composeBom = "2024.06.00" # https://developer.android.com/develop/ui/compose/bom/bom-mapping +coreKtx = "1.13.1" +espressoCore = "3.6.1" +firebaseBom = "33.1.2" +googleServices = "4.4.2" +guava = "33.2.1-android" +hilt = "2.51.1" +hiltCompiler = "1.2.0" +hiltNavigationCompose = "1.2.0" +hiltWork = "1.2.0" +junit = "4.13.2" +junitExt = "1.2.1" +kotlin = "1.9.24" +kotlinTestJunit = "1.9.21" +ksp = "1.9.24-1.0.20" +ktor = "2.3.12" +ldkNode = "0.3.0" +lifecycle = "2.8.4" +material = "1.12.0" +navCompose = "2.7.7" +room = "2.6.1" +slf4j = "1.7.36" +workRuntimeKtx = "2.9.1" + +[libraries] +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +bdk-android = { module = "org.bitcoindevkit:bdk-android", version.ref = "bdk" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltWork" } +junit = { module = "junit:junit", version.ref = "junit" } +junit-ext = { module = "androidx.test.ext:junit", version.ref = "junitExt" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinTestJunit" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version.ref = "ldkNode" } +lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } +lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } +lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } +material = { module = "com.google.android.material:material", version.ref = "material" } +material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +material3 = { module = "androidx.compose.material3:material3" } +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } +navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navCompose" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +room = { id = "androidx.room", version.ref = "room" } diff --git a/settings.gradle.kts b/settings.gradle.kts index dfd757ba4..0fad17cbb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ pluginManagement { gradlePluginPortal() } } +@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {