2026-03-23 01:41:39 +02:00
|
|
|
[versions]
|
build: unify the composite on AGP 9.4.0 so Android Studio can sync
Android Studio refused to sync the project after the lightning-kmp-app bump in 4e939c4:
Using multiple versions of the Android Gradle Plugin [9.1.1, 9.4.0] across Gradle builds is
not allowed.
Affected builds: [:, :lightning-kmp-app:lightning-kmp:bitcoin-kmp:secp256k1-kmp]
A composite build has to settle on a single AGP version across every build that applies the
plugin. Four builds in this tree do:
: 9.1.1 version catalog
:lightning-kmp-app 9.1.1 version catalog
:secp256k1-frost-kmp 9.1.1 version catalog
:lightning-kmp-app:lightning-kmp:bitcoin-kmp:secp256k1-kmp 9.4.0 hardcoded
lightning-kmp and bitcoin-kmp apply no android plugin at all, which is why the message names
only the two ends of the chain rather than everything between them.
secp256k1-kmp pins 9.4.0 twice -- a buildscript classpath entry, and a pluginManagement
resolutionStrategy that rewrites every requested com.android plugin to that version -- so it
cannot be talked out of it from here. The three catalogs move up to meet it. Pinning it down to
9.1.1 instead would mean editing a build four submodules deep and then committing in bitcoin-kmp
and lightning-kmp purely to carry the pointer, for no gain. Moving up is also the direction the
chain already set: bbba08b's f22c30a moved every build in it onto Gradle 9.7.1 for the same
reason, and 4e939c4 followed.
Submodule commits carried here:
lightning-kmp-app bbba08b -> ce1ed01 build: move to AGP 9.4.0, matching the version the
submodule chain pins
secp256k1-frost-kmp d3b294d -> 80a55b7 build: move to AGP 9.4.0 to match the rest of
mantra-kmp's composite
Both sit on branches (build/agp-9.4.0 and build/gate-ios-targets-on-macos) rather than their
masters, and neither is pushed yet, so a fresh clone cannot resolve these two pointers until
they are.
9.4.0 is a minor bump inside the AGP 9.x line, so the DSL is unchanged and every android.* flag
in gradle.properties -- newDsl, builtInKotlin, the r8 settings -- keeps its meaning.
`:composeApp:compileDebugKotlinAndroid` passes clean.
Note that this is only what the IDE needs to sync, not everything worth knowing about the
chain. bitcoin-kmp's settings.gradle.kts substitutes secp256k1-kmp, -jni-jvm and the three
-jni-jvm-{linux,darwin,mingw} coordinates, but not -jni-android. So the android app still
resolves secp256k1-kmp-jni-android from maven central at stock 0.24.0, without the
FROST/prefractal native modules the threshold branch exists to provide, and a call into those
from android would fail with UnsatisfiedLinkError. Fixing that needs a substitution rule in
bitcoin-kmp. Untouched here because it is a runtime concern, not a sync one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:58:32 +02:00
|
|
|
agp = "9.4.0"
|
2026-05-17 23:06:12 +02:00
|
|
|
android-compileSdk = "37"
|
2026-03-23 02:21:18 +02:00
|
|
|
android-minSdk = "26"
|
2026-05-17 23:06:12 +02:00
|
|
|
android-targetSdk = "37"
|
2026-03-30 13:21:14 +02:00
|
|
|
androidx-activity = "1.13.0"
|
2026-08-15 15:42:34 +02:00
|
|
|
androidx-appcompat = "1.8.0"
|
2026-06-05 23:55:44 +02:00
|
|
|
androidx-core = "1.19.0"
|
2026-03-23 01:41:39 +02:00
|
|
|
androidx-espresso = "3.7.0"
|
2026-08-15 15:42:34 +02:00
|
|
|
androidx-lifecycle = "2.11.0"
|
2026-03-23 01:41:39 +02:00
|
|
|
androidx-testExt = "1.3.0"
|
2026-08-15 15:42:34 +02:00
|
|
|
coilCompose = "3.5.0"
|
|
|
|
|
composeHotReload = "1.2.0"
|
|
|
|
|
composeMultiplatform = "1.11.1"
|
2026-04-18 21:58:32 +02:00
|
|
|
datastorePreferences = "1.2.1"
|
2026-03-23 01:41:39 +02:00
|
|
|
junit = "4.13.2"
|
2026-03-30 13:21:14 +02:00
|
|
|
kermit = "2.1.0"
|
2026-08-15 15:42:34 +02:00
|
|
|
kotlin = "2.4.10"
|
2026-05-17 23:12:49 +02:00
|
|
|
kotlinx-coroutines = "1.11.0"
|
|
|
|
|
kotlinx-datetime = "0.8.0"
|
2026-06-15 14:39:46 +02:00
|
|
|
kotlinxSerialization = "1.11.0"
|
2026-03-24 04:47:19 +02:00
|
|
|
ksp = "2.3.6"
|
2026-08-15 15:42:34 +02:00
|
|
|
ktor = "3.5.2"
|
2026-04-21 23:50:54 +02:00
|
|
|
lightningKmpCore = "1.11.5"
|
2026-03-23 01:41:39 +02:00
|
|
|
material3 = "1.10.0-alpha05"
|
|
|
|
|
materialIconsCore = "1.7.3"
|
|
|
|
|
materialIconsExtended = "1.7.3"
|
|
|
|
|
navigationCompose = "2.9.2"
|
2026-08-15 15:42:34 +02:00
|
|
|
okio = "3.18.1"
|
2026-07-01 15:11:31 +02:00
|
|
|
qrose = "1.1.2"
|
2026-08-29 16:22:56 +02:00
|
|
|
quartz = "1.14.0"
|
2026-08-15 15:42:34 +02:00
|
|
|
room3 = "3.0.1"
|
2026-06-15 14:39:46 +02:00
|
|
|
sqldelight = "2.3.2"
|
2026-08-15 15:42:34 +02:00
|
|
|
sqlite = "2.7.0"
|
2026-03-23 01:41:39 +02:00
|
|
|
|
|
|
|
|
[libraries]
|
|
|
|
|
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
|
|
|
|
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
|
|
|
|
|
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
|
|
|
|
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
|
|
|
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
2026-04-19 04:30:29 +02:00
|
|
|
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastorePreferences" }
|
2026-04-18 21:58:32 +02:00
|
|
|
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
2026-03-24 04:47:19 +02:00
|
|
|
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
|
2026-05-02 00:05:38 +02:00
|
|
|
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
|
|
|
|
|
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
|
2026-05-20 21:17:47 +02:00
|
|
|
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }
|
2026-03-23 01:41:39 +02:00
|
|
|
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
|
|
|
|
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
2026-04-21 23:49:54 +02:00
|
|
|
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
|
|
|
|
|
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coilCompose" }
|
2026-03-30 13:14:37 +02:00
|
|
|
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
2026-03-23 01:41:39 +02:00
|
|
|
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
|
|
|
|
|
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
|
|
|
|
|
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
|
2026-03-23 02:21:18 +02:00
|
|
|
compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "materialIconsCore" }
|
|
|
|
|
compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
|
2026-03-23 01:41:39 +02:00
|
|
|
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
|
2026-03-30 13:14:37 +02:00
|
|
|
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
2026-03-23 01:41:39 +02:00
|
|
|
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
|
2026-04-21 23:49:54 +02:00
|
|
|
junit = { module = "junit:junit", version.ref = "junit" }
|
2026-03-23 01:41:39 +02:00
|
|
|
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
2026-04-21 23:49:54 +02:00
|
|
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
|
|
|
|
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
2026-03-23 01:41:39 +02:00
|
|
|
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
test: cover the long-running sync, and open the seams needed to do it
The six commits that built the live chat sync added no tests. Everything they
touch fails silently by nature — a filter that drops messages, a subscription
that stops being replayed, a group whose id never reaches the `#h` tag — so the
symptom is always "some messages didn't arrive", days later, on someone else's
phone. 46 tests, in four files.
**What is covered**
RelayPoolSubscriptionTest (13) — the pool's half of surviving a dropped socket.
A query is retained and replayed on reconnect; a closed one is forgotten and
stops the socket reconnecting for it; closing one of two leaves the other alone;
a negentropy exchange is never replayed (its rounds are stateful, so resuming
one reconciles against a conversation the relay is no longer having); an update
to a live subscription replaces what gets replayed, including when the send
itself fails; dropping a relay or closing the pool forgets what they carried;
replay is scoped to the relay that reconnected. Plus the semantic the whole
change rests on, asserted in both directions: a live subscription keeps
delivering after EOSE, a one-shot query still ends at it.
LiveSubscriptionReconcileTest (12) — the requirement this all exists for: the
group filter follows group membership with nobody calling a subscribe function.
Joining widens the filter *in place* rather than reopening (a reopen would drop
the live tail of every other group in that chunk); leaving drops one; leaving
everything closes the subscription; churn inside the debounce window collapses
to one update; a NIP-17 room never becomes a group subscription. Then the
collect loop: events stored against the relay they came from, an event after
EOSE still stored, a CLOSED reopened once the back-off elapses and not before,
and a rate-limited CLOSED waiting far longer — but still coming back.
Backgrounding closes and foregrounding rebuilds, reconnects, and queues the
catch-up.
LiveSubscriptionPlanTest (11) — the filter and planning rules, led by the one
most likely to be "tidied up" later: the gift wrap filter carries no `since`,
because NIP-59 randomizes created_at into the past and a `since` near the
present silently drops new messages.
RelayBackPressureTest (4) and ReconnectBackoffTest (6) — the two pure decisions.
Which CLOSED reasons mean "ease off", and the backoff arithmetic including the
exponent clamp: 2.0.pow(4000) is Infinity and Duration * Double throws on it, so
without it a socket failing long enough turned its reconnect loop into a crash
loop, at the point the network was least likely to recover unaided.
**Seams opened to get there**, each a readability win on its own terms:
- NostrSocketClientFactory becomes an interface with DefaultNostrSocketClientFactory
behind it, so the pool can be driven by a fake socket.
- RelayPool takes its CoroutineScope, so the replay a reconnect triggers can be
observed rather than raced.
- LiveSubscriptionManager depends on a new LiveSubscriptionTransport (4
methods) rather than RelaysSocketManager, which observes the active wallet in
its init and cannot be stood up in a test at all.
- Its pure planning helpers move to the companion as `internal`, and its
launches inherit the caller's dispatcher instead of pinning Dispatchers.IO.
SynchronizationViewModel already launches observe() on IO, so nothing moves —
but a coroutine that picks its own dispatcher cannot be driven by a test
scheduler.
- reconnectDelay is extracted to ReconnectBackoff.kt with jitter as a
parameter, so the arithmetic can be pinned without randomness.
- endsLiveSubscription names the live-subscription termination rule next to
isTerminalFor, which is the one-shot rule. Having both named makes the
difference between them reviewable rather than implicit.
kotlinx-coroutines-test is added to commonTest: the pool's bookkeeping is all
suspend functions and there is no runBlocking in a common source set.
The tests were checked by mutation, not just by passing — reintroducing a
`since`, making EOSE terminal, dropping the leftGroupAt filter and removing
retention from query() each produce failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:31:11 +02:00
|
|
|
kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
2026-03-30 13:09:49 +02:00
|
|
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
2026-06-15 14:39:46 +02:00
|
|
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
|
|
|
|
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
|
2026-03-27 13:35:29 +02:00
|
|
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
2026-03-30 13:25:19 +02:00
|
|
|
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
2026-03-30 13:14:37 +02:00
|
|
|
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
|
|
|
|
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
2026-04-21 23:50:54 +02:00
|
|
|
lightning-kmp-core = { module = "fr.acinq.lightning:lightning-kmp-core", version.ref = "lightningKmpCore" }
|
2026-03-23 01:41:39 +02:00
|
|
|
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
2026-04-18 02:41:31 +02:00
|
|
|
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
2026-07-01 15:11:31 +02:00
|
|
|
qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
|
2026-06-15 14:39:46 +02:00
|
|
|
sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
|
|
|
|
sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
|
|
|
|
sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
|
|
|
|
|
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
|
2026-03-30 13:14:37 +02:00
|
|
|
vitorpamplona-quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
|
2026-03-23 01:41:39 +02:00
|
|
|
|
|
|
|
|
[plugins]
|
|
|
|
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
2026-05-02 00:05:38 +02:00
|
|
|
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }
|
2026-03-23 01:41:39 +02:00
|
|
|
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
|
|
|
|
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
|
|
|
|
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
|
|
|
|
|
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
2026-03-23 02:21:18 +02:00
|
|
|
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
2026-03-24 04:47:19 +02:00
|
|
|
kotlinPluginSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
2026-06-15 14:39:46 +02:00
|
|
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
|
|
|
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|