2026-03-23 01:41:39 +02:00
|
|
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
|
|
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
|
|
|
|
|
|
|
|
plugins {
|
|
|
|
|
alias(libs.plugins.androidApplication)
|
2026-05-02 00:05:38 +02:00
|
|
|
// alias(libs.plugins.androidx.room)
|
|
|
|
|
alias(libs.plugins.androidx.room3)
|
2026-03-24 04:47:19 +02:00
|
|
|
alias(libs.plugins.kotlinMultiplatform)
|
2026-03-23 01:41:39 +02:00
|
|
|
alias(libs.plugins.composeMultiplatform)
|
|
|
|
|
alias(libs.plugins.composeCompiler)
|
|
|
|
|
alias(libs.plugins.composeHotReload)
|
2026-03-23 02:21:18 +02:00
|
|
|
alias(libs.plugins.kotlinPluginSerialization)
|
2026-03-24 04:47:19 +02:00
|
|
|
alias(libs.plugins.ksp)
|
2026-06-15 14:39:46 +02:00
|
|
|
alias(libs.plugins.sqldelight)
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
kotlin {
|
2026-03-26 00:20:20 +02:00
|
|
|
compilerOptions {
|
|
|
|
|
freeCompilerArgs.add("-Xexpect-actual-classes")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 01:41:39 +02:00
|
|
|
androidTarget {
|
|
|
|
|
compilerOptions {
|
2026-06-23 13:59:17 +02:00
|
|
|
jvmTarget.set(JvmTarget.JVM_21)
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
build: bump lightning-kmp-app to bbba08b, building lightning-kmp from source
lightning-kmp-app moves 6745d88 -> bbba08b, four commits:
57353cf Minor updates
9f3cd72 Build lightning-kmp from the experimental submodule via a composite build
f22c30a Follow the submodule chain onto Gradle 9.7.1
bbba08b Declare the ios targets only on a mac, so the IDE can import the project
9f3cd72 is the substantive one. lightning-kmp no longer comes from maven central: the
submodule now carries its own experimental/lightning-kmp submodule on branch `threshold`
-- the branch with the FROST/prefractal signers -- and substitutes
fr.acinq.lightning:lightning-kmp-core for that build's project. That build includes
bitcoin-kmp, which includes secp256k1-kmp, which compiles the C library from a secp256k1-zkp
fork. So this repo's build tree is now four levels deep and compiles native code.
Cloning therefore needs `git submodule update --init --recursive`, and because each included
build resolves the android SDK from its own local.properties rather than inheriting the
root's, the three new nested builds each need a (gitignored) local.properties with sdk.dir.
57353cf changed two signatures that MantraApplication implements -- LightningApplication
gained getApplicationContext(), and BusinessManager.initialize now takes the application
rather than a Context. Neither needed a source change: MantraApplication extends
android.app.Application, which already supplies getApplicationContext(), and it was already
passing `this`, which satisfies the narrowed parameter type.
The rest of this commit is what the update forces on the outer build.
gradle-wrapper.properties, 9.3.1 -> 9.7.1: f22c30a moved every build in the chain onto
9.7.1. An included build does not use its own wrapper -- the root build's gradle version runs
the whole tree -- so this repo has to follow for the chain to build at all.
gradle.properties, configuration cache off: secp256k1-kmp's `:jni:generateHeaders` and
`:native:buildSecp256k1<target>` both hold gradle script object references and cannot be
serialized. The configuration cache covers a whole build tree and has no per-build opt-out,
so an included build's incompatibility is this build's problem. The comment records how to
undo this once those tasks are fixed upstream.
composeApp/build.gradle.kts, ios targets gated on the host: secp256k1-kmp declares a
libsecp256k1 cinterop, which makes gradle switch off klib cross compilation for apple
targets. On linux nothing in the tree then offers an ios variant of
fr.acinq.phoenix:lightning-kmp-app, and the ios compilations failed with "No matching variant
of project ':lightning-kmp-app:library'" -- not a warning, a build failure. The gate covers
the target declarations, the iosMain dependencies (the source set only exists when the
targets do) and the kspIos* configurations (likewise). This mirrors bbba08b, which applied
the same gate inside the submodule for the same reason.
secp256k1-frost-kmp d3b294d applies that gate there too. Substitution rules apply across a
whole build tree, so that project's own lightning-kmp-core coordinate started resolving to
the source project without it asking, and every apple source set stopped resolving. The
android build masked it -- ios compilations are not in its task graph -- but
kmpPartiallyResolvedDependenciesChecker reported it and compileAppleMainKotlinMetadata failed
outright, which would have broken IDE import.
Note that `:composeApp:compileCommonMainKotlinMetadata` no longer exists on a linux host.
With ios gated off, androidTarget is the only declared target (jvm() is still commented out),
and KMP does not generate a commonMain metadata compilation for a single-target project.
`:composeApp:compileDebugKotlinAndroid` is the check now; it passes clean, with none of the
resolution errors above.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:50:59 +02:00
|
|
|
// Declared only on a mac, mirroring the gate lightning-kmp-app's :library applies to its own ios
|
|
|
|
|
// targets. Down the composite chain secp256k1-kmp declares a libsecp256k1 cinterop, which makes
|
|
|
|
|
// gradle switch off klib cross compilation for apple targets, so on a linux/windows host nothing
|
|
|
|
|
// in the build tree offers an ios variant of fr.acinq.phoenix:lightning-kmp-app. Declaring these
|
|
|
|
|
// targets anyway leaves every ios compilation unable to resolve it and fails the build outright.
|
|
|
|
|
// Building an ios binary needs a mac regardless.
|
|
|
|
|
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) {
|
|
|
|
|
listOf(
|
|
|
|
|
iosArm64(),
|
|
|
|
|
iosSimulatorArm64()
|
|
|
|
|
).forEach { iosTarget ->
|
|
|
|
|
iosTarget.binaries.framework {
|
|
|
|
|
baseName = "ComposeApp"
|
|
|
|
|
isStatic = true
|
|
|
|
|
}
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: mantra compiles for the jvm
Phase 4. Declares jvm(), implements all 16 expects, and bumps the
submodule to the fork branch carrying phases 1-3.
:composeApp:compileKotlinJvm is green.
**The actuals were the small half. Room was the blocker.** The first jvm
compile failed with 58 copies of "Only suspend functions are allowed in
DAOs declared in source sets targeting non-Android platforms". Room
permits blocking query methods on android and nowhere else, so every @Dao
function that was neither suspend nor Flow-returning had to change --
58 of them across 24 files. KSP reports these in alphabetical batches, so
the count shrinks in stages and looks bottomless; scanning the dao package
directly for abstract funs with no suspend and no Flow return finds them
all at once.
It stops there, which is the only reason this is a 58-line change rather
than a refactor. Every one of the 15 call sites outside the dao package
was already inside a suspend function -- the repositories were written
that way throughout -- so nothing needed rewriting. One private helper,
DatabaseNostrRepository.matchNegentropicNostrEvents, had to become
suspend, and its single caller was already suspend, so the cascade
terminated immediately. Zero call-site edits.
**The cost lands on android, not on the jvm.** A blocking DAO method runs
on its caller's thread; a suspend one is dispatched to the query coroutine
context, which getRoomDatabase sets to Dispatchers.IO. That is the better
behaviour -- it is what stops a query running on the main thread -- but it
is a real change to the shipping platform, made for a target that does not
run yet. Hence the unit tests below rather than a compile alone.
**BusinessManager was not an expect**, so nothing warned about it. It is
now ported to the fork's jvmMain (05ce7eb); Phoenix.jvm.kt and
NavigationViewModel.jvm.kt are otherwise the ios actuals with one changed
import, since those files use no ios API.
**schedulePlatformLogic schedules nothing, and logs that it does not.**
Android starts two WorkManager jobs here, one of which is ChannelsWatcher
-- it wakes periodically to notice a channel force-closed while the app
was shut. A desktop application has no process once its window closes, so
there is nothing to wake, and running the watcher in-process would be
strictly worse than not running it: it would only fire while the app was
already open and watching. The exposure is real and belongs in release
notes rather than a comment -- a desktop wallet left closed past a
force-close deadline does not notice.
Smaller calls. PlatformContext carries an application directory, since
there is no Context to read one from, and PlatformDatabaseBuilder puts
aux.db under it rather than in java.io.tmpdir, which is what the abandoned
Aux implementation did behind a TODO and which most systems clear on
reboot. themeColorScheme ignores dynamicColor, which means Material You
and has no desktop counterpart. AppVersion reads the jar manifest that
compose.desktop writes, falling back when running from a class directory.
Verified: :composeApp:compileKotlinJvm green,
:composeApp:compileDebugKotlinAndroid green, and
:composeApp:testDebugUnitTest 52 passing -- the one that matters, since
this commit changes shared code every android query path goes through.
Not verified: nothing has run. No jvm entry point exists yet, so the
database has never been opened on this platform and no business has been
started. That is phase 5, which also has to unlock JvmKeyStore before the
wallet starts -- a passphrase prompt, not just a window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 01:23:42 +02:00
|
|
|
jvm()
|
2026-03-23 01:41:39 +02:00
|
|
|
|
|
|
|
|
sourceSets {
|
|
|
|
|
androidMain.dependencies {
|
build: package the android secp256k1 natives, with ChillDKG in them
The android app had no android secp256k1 natives at all, and had not had any for
as long as lightning-kmp-core has been a dependency. `dependencyInsight` on
debugRuntimeClasspath resolved every secp256k1 coordinate to
`:secp256k1-kmp:jni:jvm:{darwin,linux,mingw}` -- desktop .so/.dylib/.dll files,
loaded by extracting them from a jar, which cannot work on a device.
The cause is a chain of individually reasonable decisions. lightning-kmp-core
publishes no android variant, so an android consumer resolves it to the jvm one;
the jvm variant asks for `secp256k1-kmp-jni-jvm`; and nothing anywhere asks for
`secp256k1-kmp-jni-android`. lightning-kmp-app names it only in androidDeviceTest,
so consumers of the published library do not get it. It has to be named here.
Naming it alone would not have been enough. bitcoin-kmp's settings.gradle.kts
substituted five of secp256k1-kmp's coordinates to the fork's projects but not
-jni-android, so it would have resolved from Maven Central to stock 0.24.0 --
built from upstream libsecp256k1, with no ChillDKG module. Because the kotlin API
comes from the substituted root project, that combination type-checks and links
and then fails with UnsatisfiedLinkError at the first native call. The rule is
added in the submodule commits this carries.
Submodule commits carried here:
lightning-kmp-app ce1ed01 -> 6434282 build: carry the jni-android substitution
down from bitcoin-kmp
experimental/lightning-kmp 77be7b78 -> 5103b79e
experimental/bitcoin-kmp 196a479 -> 65c4aab build: substitute
secp256k1-kmp-jni-android to the
included build too
Verified through the artifact rather than the graph: composeApp-debug.apk now
carries lib/{arm64-v8a,armeabi-v7a,x86,x86_64}/libsecp256k1-jni.so, and `nm -D` on
the arm64 one exports all twelve Java_..._chilldkg_... JNI entry points --
hostpubkey_gen, params_hash, participant_step1/step2/finalize,
coordinator_step1/finalize, and the recover and investigate calls.
The three builds in the chain sit on `build/substitute-jni-android` branches
(lightning-kmp-app on `build/agp-9.4.0`, which now carries two commits) and none
are pushed, so a fresh clone cannot resolve these pointers yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 23:25:23 +02:00
|
|
|
// The android secp256k1 natives. Nothing pulls these in transitively: lightning-kmp-core
|
|
|
|
|
// publishes no android variant, so the android target resolves it to the jvm one, which
|
|
|
|
|
// asks for secp256k1-kmp-jni-jvm and gets desktop .so/.dylib/.dll files that cannot load
|
|
|
|
|
// on a device. The app has to name the android artifact itself.
|
|
|
|
|
//
|
|
|
|
|
// Resolved from source via the composite build, not from Maven Central -- bitcoin-kmp's
|
|
|
|
|
// settings.gradle.kts substitutes this coordinate for secp256k1-kmp's :jni:android. That
|
|
|
|
|
// matters: the stock artifact has no ChillDKG module, so ChillDKG would compile and then
|
|
|
|
|
// fail with UnsatisfiedLinkError. The version is never resolved; substitution matches on
|
|
|
|
|
// group:name.
|
|
|
|
|
implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.24.0")
|
|
|
|
|
|
2026-03-23 01:41:39 +02:00
|
|
|
implementation(libs.androidx.activity.compose)
|
2026-05-02 00:05:38 +02:00
|
|
|
// implementation(libs.androidx.room.sqlite.wrapper)
|
2026-03-30 13:14:37 +02:00
|
|
|
implementation(libs.compose.uiToolingPreview)
|
2026-06-15 14:39:46 +02:00
|
|
|
|
|
|
|
|
implementation(libs.sqldelight.android.driver)
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
commonMain.dependencies {
|
2026-04-19 04:30:29 +02:00
|
|
|
implementation(libs.androidx.datastore)
|
2026-04-18 21:58:32 +02:00
|
|
|
implementation(libs.androidx.datastore.preferences)
|
|
|
|
|
|
2026-03-30 13:14:37 +02:00
|
|
|
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
|
|
|
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
|
|
|
|
|
2026-05-02 00:05:38 +02:00
|
|
|
// implementation(libs.androidx.room.runtime)
|
|
|
|
|
implementation(libs.androidx.room3.runtime)
|
2026-03-30 13:14:37 +02:00
|
|
|
implementation(libs.androidx.sqlite.bundled)
|
|
|
|
|
|
2026-04-21 23:49:54 +02:00
|
|
|
implementation(libs.coil.compose)
|
|
|
|
|
implementation(libs.coil.network.ktor3)
|
2026-04-20 22:57:15 +02:00
|
|
|
|
2026-03-23 01:41:39 +02:00
|
|
|
implementation(libs.compose.runtime)
|
|
|
|
|
implementation(libs.compose.foundation)
|
|
|
|
|
implementation(libs.compose.material3)
|
|
|
|
|
implementation (libs.compose.material.icons.core)
|
|
|
|
|
implementation (libs.compose.material.icons.extended)
|
|
|
|
|
implementation(libs.compose.ui)
|
|
|
|
|
implementation(libs.compose.components.resources)
|
|
|
|
|
implementation(libs.compose.uiToolingPreview)
|
|
|
|
|
|
2026-04-18 21:58:32 +02:00
|
|
|
|
2026-03-23 01:41:39 +02:00
|
|
|
implementation(libs.kermit)
|
|
|
|
|
|
2026-03-30 13:09:49 +02:00
|
|
|
implementation(libs.kotlinx.datetime)
|
2026-03-30 13:14:37 +02:00
|
|
|
implementation(libs.kotlinx.serialization.json)
|
2026-06-15 14:39:46 +02:00
|
|
|
implementation(libs.kotlinx.serialization.cbor)
|
|
|
|
|
|
2026-03-30 13:09:49 +02:00
|
|
|
|
2026-03-27 13:35:29 +02:00
|
|
|
implementation(libs.ktor.client.core)
|
2026-03-30 13:25:19 +02:00
|
|
|
implementation(libs.ktor.client.cio)
|
2026-03-30 13:14:37 +02:00
|
|
|
implementation(libs.ktor.client.websockets)
|
|
|
|
|
implementation(libs.ktor.serialization.kotlinx.json)
|
|
|
|
|
|
2026-06-15 14:39:46 +02:00
|
|
|
|
|
|
|
|
|
2026-08-15 17:07:31 +02:00
|
|
|
// implementation(libs.lightning.kmp.core)
|
|
|
|
|
|
|
|
|
|
// resolved via the lightning-kmp-app composite build (see settings.gradle.kts)
|
|
|
|
|
implementation("fr.acinq.phoenix:lightning-kmp-app:1.0.0")
|
2026-06-15 14:39:46 +02:00
|
|
|
|
|
|
|
|
implementation(libs.navigation.compose)
|
|
|
|
|
|
2026-04-18 02:41:31 +02:00
|
|
|
implementation(libs.okio)
|
|
|
|
|
|
2026-07-01 15:11:31 +02:00
|
|
|
implementation(libs.qrose)
|
2026-06-15 14:39:46 +02:00
|
|
|
|
|
|
|
|
implementation(libs.sqldelight.runtime)
|
|
|
|
|
implementation(libs.sqldelight.coroutines.extensions)
|
|
|
|
|
|
2026-03-30 13:14:37 +02:00
|
|
|
implementation(libs.vitorpamplona.quartz)
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
commonTest.dependencies {
|
|
|
|
|
implementation(libs.kotlin.test)
|
feat: read a room's group events again when they arrived out of order
Relays impose no ordering, so a kind:445 can turn up before the group can
read it: an application message encrypted under an epoch whose commit has
not landed, or a commit for an epoch ahead of the local one. Both are
stored and then dropped -- MarmotInboundManager refuses an out-of-epoch
commit precisely so it does not half-mutate the group -- and nothing goes
back for them once the missing event fills the gap. The message is on
disk, readable, and never read. A "Reindex Events" button at the bottom
of the group's detail screen is that second look.
Only events with nothing to show for them are replayed: no chat line at
all, or one of the two placeholder types. A room where nothing went wrong
is left exactly as it was, which is what makes the button safe to press
on a hunch. Passes repeat while a pass recovers something, because
created_at order is not epoch order and a commit recovered by one pass is
what lets the next read the messages that were waiting on it.
**Replaying was not safe as it stood.** Every row the path writes is keyed
on an event id and upserts in place -- MarmotGroupEvent, MarmotInnerEvent,
and the nip30303 entities -- with one exception. ChatMessage's primary key
is autogenerated, so writing a freshly built line always inserts, and a
re-read would have left the room showing each recovered message twice,
once as "Undecryptable Message" and once as itself.
ChatMessage.reconcileMarmotLine matches on the group event id instead, so
a re-read is an update, and refuses to let a placeholder overwrite a line
that says something. That last rule is what protects the line this device
wrote on the way out for a message it sent: our own kind:445 cannot be
read back, since the sender ratchet has consumed the generation, and
without the rule a replay would have replaced our words with
"Undecryptable Message".
The MLS group itself was already safe to replay against, which is worth
saying because it is the part that looks dangerous: a commit behind the
current epoch is rejected as a duplicate before it touches the group, one
ahead is refused, and a consumed ratchet generation throws before
mutating anything. The exception was quartz's EpochCommitTracker, which
does not dedupe and only empties when a commit applies -- so replaying a
held commit just grew the list and left it pending forever.
forgetPendingCommits drops the room's entries first, and the sweep feeds
the events back in the order CommitOrdering picks a winner in, so a
contested epoch resolves the same way it would have on every other
device.
**What is testable, and what is not.** The DAO is not: testDebugUnitTest
is plain JVM and Room's in-memory builder wants an Android Context. So
the two pieces carrying decisions are lifted out where they can be run
without one -- MarmotReindexSweep for the stopping rule, and
reconcileMarmotLine for which of two lines wins -- and the DAO is left as
query, sweep, write. The filter tests pin why the query's `tags LIKE` is a
prefilter and not a test: an event belonging to another room can mention
this one in a q tag, and its own h tag is what rejects it.
**Not recovered by any of this.** A message whose key is gone -- one the
ratchet has already advanced past, or one from an epoch predating this
device's join. And events that never reached disk at all: storeNostrEvent
is a single transaction, so a kind:445 arriving before its room exists
rolls back its own insert along with the failed indexing, and only a
re-sync brings it back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 01:49:57 +02:00
|
|
|
// runTest: the DAO, model and relay layers are all suspending, and there is no
|
|
|
|
|
// runBlocking in a common source set — so anything worth asserting about them
|
|
|
|
|
// needs a coroutine, and a scheduler, to assert it in.
|
|
|
|
|
implementation(libs.kotlinx.coroutinesTest)
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
jvmMain.dependencies {
|
|
|
|
|
implementation(compose.desktop.currentOs)
|
|
|
|
|
implementation(libs.kotlinx.coroutinesSwing)
|
feat: mantra compiles for the jvm
Phase 4. Declares jvm(), implements all 16 expects, and bumps the
submodule to the fork branch carrying phases 1-3.
:composeApp:compileKotlinJvm is green.
**The actuals were the small half. Room was the blocker.** The first jvm
compile failed with 58 copies of "Only suspend functions are allowed in
DAOs declared in source sets targeting non-Android platforms". Room
permits blocking query methods on android and nowhere else, so every @Dao
function that was neither suspend nor Flow-returning had to change --
58 of them across 24 files. KSP reports these in alphabetical batches, so
the count shrinks in stages and looks bottomless; scanning the dao package
directly for abstract funs with no suspend and no Flow return finds them
all at once.
It stops there, which is the only reason this is a 58-line change rather
than a refactor. Every one of the 15 call sites outside the dao package
was already inside a suspend function -- the repositories were written
that way throughout -- so nothing needed rewriting. One private helper,
DatabaseNostrRepository.matchNegentropicNostrEvents, had to become
suspend, and its single caller was already suspend, so the cascade
terminated immediately. Zero call-site edits.
**The cost lands on android, not on the jvm.** A blocking DAO method runs
on its caller's thread; a suspend one is dispatched to the query coroutine
context, which getRoomDatabase sets to Dispatchers.IO. That is the better
behaviour -- it is what stops a query running on the main thread -- but it
is a real change to the shipping platform, made for a target that does not
run yet. Hence the unit tests below rather than a compile alone.
**BusinessManager was not an expect**, so nothing warned about it. It is
now ported to the fork's jvmMain (05ce7eb); Phoenix.jvm.kt and
NavigationViewModel.jvm.kt are otherwise the ios actuals with one changed
import, since those files use no ios API.
**schedulePlatformLogic schedules nothing, and logs that it does not.**
Android starts two WorkManager jobs here, one of which is ChannelsWatcher
-- it wakes periodically to notice a channel force-closed while the app
was shut. A desktop application has no process once its window closes, so
there is nothing to wake, and running the watcher in-process would be
strictly worse than not running it: it would only fire while the app was
already open and watching. The exposure is real and belongs in release
notes rather than a comment -- a desktop wallet left closed past a
force-close deadline does not notice.
Smaller calls. PlatformContext carries an application directory, since
there is no Context to read one from, and PlatformDatabaseBuilder puts
aux.db under it rather than in java.io.tmpdir, which is what the abandoned
Aux implementation did behind a TODO and which most systems clear on
reboot. themeColorScheme ignores dynamicColor, which means Material You
and has no desktop counterpart. AppVersion reads the jar manifest that
compose.desktop writes, falling back when running from a class directory.
Verified: :composeApp:compileKotlinJvm green,
:composeApp:compileDebugKotlinAndroid green, and
:composeApp:testDebugUnitTest 52 passing -- the one that matters, since
this commit changes shared code every android query path goes through.
Not verified: nothing has run. No jvm entry point exists yet, so the
database has never been opened on this platform and no business has been
started. That is phase 5, which also has to unlock JvmKeyStore before the
wallet starts -- a passphrase prompt, not just a window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 01:23:42 +02:00
|
|
|
|
|
|
|
|
// The jvm counterpart to sqldelight-android-driver / -native-driver above.
|
|
|
|
|
implementation(libs.sqldelight.sqlite.driver)
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
build: bump lightning-kmp-app to bbba08b, building lightning-kmp from source
lightning-kmp-app moves 6745d88 -> bbba08b, four commits:
57353cf Minor updates
9f3cd72 Build lightning-kmp from the experimental submodule via a composite build
f22c30a Follow the submodule chain onto Gradle 9.7.1
bbba08b Declare the ios targets only on a mac, so the IDE can import the project
9f3cd72 is the substantive one. lightning-kmp no longer comes from maven central: the
submodule now carries its own experimental/lightning-kmp submodule on branch `threshold`
-- the branch with the FROST/prefractal signers -- and substitutes
fr.acinq.lightning:lightning-kmp-core for that build's project. That build includes
bitcoin-kmp, which includes secp256k1-kmp, which compiles the C library from a secp256k1-zkp
fork. So this repo's build tree is now four levels deep and compiles native code.
Cloning therefore needs `git submodule update --init --recursive`, and because each included
build resolves the android SDK from its own local.properties rather than inheriting the
root's, the three new nested builds each need a (gitignored) local.properties with sdk.dir.
57353cf changed two signatures that MantraApplication implements -- LightningApplication
gained getApplicationContext(), and BusinessManager.initialize now takes the application
rather than a Context. Neither needed a source change: MantraApplication extends
android.app.Application, which already supplies getApplicationContext(), and it was already
passing `this`, which satisfies the narrowed parameter type.
The rest of this commit is what the update forces on the outer build.
gradle-wrapper.properties, 9.3.1 -> 9.7.1: f22c30a moved every build in the chain onto
9.7.1. An included build does not use its own wrapper -- the root build's gradle version runs
the whole tree -- so this repo has to follow for the chain to build at all.
gradle.properties, configuration cache off: secp256k1-kmp's `:jni:generateHeaders` and
`:native:buildSecp256k1<target>` both hold gradle script object references and cannot be
serialized. The configuration cache covers a whole build tree and has no per-build opt-out,
so an included build's incompatibility is this build's problem. The comment records how to
undo this once those tasks are fixed upstream.
composeApp/build.gradle.kts, ios targets gated on the host: secp256k1-kmp declares a
libsecp256k1 cinterop, which makes gradle switch off klib cross compilation for apple
targets. On linux nothing in the tree then offers an ios variant of
fr.acinq.phoenix:lightning-kmp-app, and the ios compilations failed with "No matching variant
of project ':lightning-kmp-app:library'" -- not a warning, a build failure. The gate covers
the target declarations, the iosMain dependencies (the source set only exists when the
targets do) and the kspIos* configurations (likewise). This mirrors bbba08b, which applied
the same gate inside the submodule for the same reason.
secp256k1-frost-kmp d3b294d applies that gate there too. Substitution rules apply across a
whole build tree, so that project's own lightning-kmp-core coordinate started resolving to
the source project without it asking, and every apple source set stopped resolving. The
android build masked it -- ios compilations are not in its task graph -- but
kmpPartiallyResolvedDependenciesChecker reported it and compileAppleMainKotlinMetadata failed
outright, which would have broken IDE import.
Note that `:composeApp:compileCommonMainKotlinMetadata` no longer exists on a linux host.
With ios gated off, androidTarget is the only declared target (jvm() is still commented out),
and KMP does not generate a commonMain metadata compilation for a single-target project.
`:composeApp:compileDebugKotlinAndroid` is the check now; it passes clean, with none of the
resolution errors above.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:50:59 +02:00
|
|
|
// Only exists when the ios targets above were declared; the default hierarchy template
|
|
|
|
|
// creates this source set from them.
|
|
|
|
|
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) {
|
|
|
|
|
iosMain.dependencies {
|
|
|
|
|
implementation(libs.sqldelight.native.driver)
|
|
|
|
|
}
|
2026-03-27 13:35:29 +02:00
|
|
|
}
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
android {
|
2026-07-15 01:14:46 +02:00
|
|
|
namespace = "press.mantra.android"
|
2026-03-23 01:41:39 +02:00
|
|
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
|
|
|
|
|
2026-06-15 14:39:46 +02:00
|
|
|
buildFeatures {
|
|
|
|
|
buildConfig = true
|
|
|
|
|
}
|
2026-03-23 01:41:39 +02:00
|
|
|
defaultConfig {
|
2026-07-15 01:14:46 +02:00
|
|
|
applicationId = "press.mantra.android"
|
2026-03-23 01:41:39 +02:00
|
|
|
minSdk = libs.versions.android.minSdk.get().toInt()
|
|
|
|
|
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
|
|
|
|
versionCode = 1
|
2026-04-22 22:31:03 +02:00
|
|
|
versionName = "0.1.0"
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
packaging {
|
|
|
|
|
resources {
|
|
|
|
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
2026-03-23 02:21:18 +02:00
|
|
|
excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
buildTypes {
|
|
|
|
|
getByName("release") {
|
|
|
|
|
isMinifyEnabled = false
|
|
|
|
|
}
|
2026-03-23 02:21:18 +02:00
|
|
|
getByName("debug") {
|
|
|
|
|
applicationIdSuffix = ".debug"
|
|
|
|
|
versionNameSuffix = "-DEBUG"
|
|
|
|
|
}
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
compileOptions {
|
2026-06-23 13:59:17 +02:00
|
|
|
sourceCompatibility = JavaVersion.VERSION_21
|
|
|
|
|
targetCompatibility = JavaVersion.VERSION_21
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
feat: add a SubmissionEvent that carries a nip30303 event as its payload
Every nip30303 kind so far describes a thing: an artifact, a dialect, a
chapter, a translated chunk. None of them describes the act of putting
one in front of a group, and until now nothing needed to -- a group
event's sender was the author of the event inside it, so the two
questions had one answer by construction.
That construction is also the limit. It means a group can only ever hold
work written by its own members under their own keys. A translation
lifted from a public archive, a chapter transcribed by an outside
contributor, an artifact somebody published years ago: none of it can go
in without a member re-authoring it and taking the byline.
Kind 30312 is the envelope that separates them. Its content is the
payload event's JSON, whole -- same id, same pubKey, same signature,
nothing rewritten to look like the submitter's work. The submitter signs
for the envelope; the author still signs for the event. Two tags name
what is inside so a client can decide whether it can apply a submission
without parsing the content first:
payloadKind the payload's kind
payloadId the payload's id, with the author slot carrying the
payload's author -- who, unusually for an id tag in
this package, is often not the event's sender
Kinds 30300-30311 are taken (30305 and 30307 by contributor lists), so
30312 is the next free one.
A submission is not an endorsement and grants nothing. Who may submit is
the group's business; this only makes the question expressible.
The test covers the property the whole thing rests on: an event written
by an outsider goes into an envelope, comes out of a JSON round trip
with its id, author and signature intact, and does not acquire the
submitter as its author. It also pins payload() returning null rather
than something empty when the content will not parse -- which needed
android.util.Log stubbing, since quartz logs on that path and unmocked
Log methods throw, failing the test on the log line rather than on what
it came to check.
Nothing sends or reads one yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 20:21:00 +02:00
|
|
|
testOptions {
|
|
|
|
|
unitTests {
|
|
|
|
|
// Quartz logs through android.util.Log on the error paths -- parsing a
|
|
|
|
|
// malformed event, for one. Unmocked, those methods throw, so a test
|
|
|
|
|
// covering such a path fails on the log line rather than on what it
|
|
|
|
|
// came to check. Default values let the code under test carry on.
|
|
|
|
|
isReturnDefaultValues = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dependencies {
|
|
|
|
|
debugImplementation(libs.compose.uiTooling)
|
2026-05-02 00:05:38 +02:00
|
|
|
// ksp(libs.androidx.room3.compiler)
|
|
|
|
|
add("kspAndroid", libs.androidx.room3.compiler)
|
build: bump lightning-kmp-app to bbba08b, building lightning-kmp from source
lightning-kmp-app moves 6745d88 -> bbba08b, four commits:
57353cf Minor updates
9f3cd72 Build lightning-kmp from the experimental submodule via a composite build
f22c30a Follow the submodule chain onto Gradle 9.7.1
bbba08b Declare the ios targets only on a mac, so the IDE can import the project
9f3cd72 is the substantive one. lightning-kmp no longer comes from maven central: the
submodule now carries its own experimental/lightning-kmp submodule on branch `threshold`
-- the branch with the FROST/prefractal signers -- and substitutes
fr.acinq.lightning:lightning-kmp-core for that build's project. That build includes
bitcoin-kmp, which includes secp256k1-kmp, which compiles the C library from a secp256k1-zkp
fork. So this repo's build tree is now four levels deep and compiles native code.
Cloning therefore needs `git submodule update --init --recursive`, and because each included
build resolves the android SDK from its own local.properties rather than inheriting the
root's, the three new nested builds each need a (gitignored) local.properties with sdk.dir.
57353cf changed two signatures that MantraApplication implements -- LightningApplication
gained getApplicationContext(), and BusinessManager.initialize now takes the application
rather than a Context. Neither needed a source change: MantraApplication extends
android.app.Application, which already supplies getApplicationContext(), and it was already
passing `this`, which satisfies the narrowed parameter type.
The rest of this commit is what the update forces on the outer build.
gradle-wrapper.properties, 9.3.1 -> 9.7.1: f22c30a moved every build in the chain onto
9.7.1. An included build does not use its own wrapper -- the root build's gradle version runs
the whole tree -- so this repo has to follow for the chain to build at all.
gradle.properties, configuration cache off: secp256k1-kmp's `:jni:generateHeaders` and
`:native:buildSecp256k1<target>` both hold gradle script object references and cannot be
serialized. The configuration cache covers a whole build tree and has no per-build opt-out,
so an included build's incompatibility is this build's problem. The comment records how to
undo this once those tasks are fixed upstream.
composeApp/build.gradle.kts, ios targets gated on the host: secp256k1-kmp declares a
libsecp256k1 cinterop, which makes gradle switch off klib cross compilation for apple
targets. On linux nothing in the tree then offers an ios variant of
fr.acinq.phoenix:lightning-kmp-app, and the ios compilations failed with "No matching variant
of project ':lightning-kmp-app:library'" -- not a warning, a build failure. The gate covers
the target declarations, the iosMain dependencies (the source set only exists when the
targets do) and the kspIos* configurations (likewise). This mirrors bbba08b, which applied
the same gate inside the submodule for the same reason.
secp256k1-frost-kmp d3b294d applies that gate there too. Substitution rules apply across a
whole build tree, so that project's own lightning-kmp-core coordinate started resolving to
the source project without it asking, and every apple source set stopped resolving. The
android build masked it -- ios compilations are not in its task graph -- but
kmpPartiallyResolvedDependenciesChecker reported it and compileAppleMainKotlinMetadata failed
outright, which would have broken IDE import.
Note that `:composeApp:compileCommonMainKotlinMetadata` no longer exists on a linux host.
With ios gated off, androidTarget is the only declared target (jvm() is still commented out),
and KMP does not generate a commonMain metadata compilation for a single-target project.
`:composeApp:compileDebugKotlinAndroid` is the check now; it passes clean, with none of the
resolution errors above.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:50:59 +02:00
|
|
|
// These configurations only exist when the ios targets are declared, which the kotlin block
|
|
|
|
|
// above does only on a mac.
|
|
|
|
|
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) {
|
|
|
|
|
add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
|
|
|
|
|
// add("kspIosX64", libs.androidx.room.compiler)
|
|
|
|
|
add("kspIosArm64", libs.androidx.room3.compiler)
|
|
|
|
|
}
|
feat: mantra compiles for the jvm
Phase 4. Declares jvm(), implements all 16 expects, and bumps the
submodule to the fork branch carrying phases 1-3.
:composeApp:compileKotlinJvm is green.
**The actuals were the small half. Room was the blocker.** The first jvm
compile failed with 58 copies of "Only suspend functions are allowed in
DAOs declared in source sets targeting non-Android platforms". Room
permits blocking query methods on android and nowhere else, so every @Dao
function that was neither suspend nor Flow-returning had to change --
58 of them across 24 files. KSP reports these in alphabetical batches, so
the count shrinks in stages and looks bottomless; scanning the dao package
directly for abstract funs with no suspend and no Flow return finds them
all at once.
It stops there, which is the only reason this is a 58-line change rather
than a refactor. Every one of the 15 call sites outside the dao package
was already inside a suspend function -- the repositories were written
that way throughout -- so nothing needed rewriting. One private helper,
DatabaseNostrRepository.matchNegentropicNostrEvents, had to become
suspend, and its single caller was already suspend, so the cascade
terminated immediately. Zero call-site edits.
**The cost lands on android, not on the jvm.** A blocking DAO method runs
on its caller's thread; a suspend one is dispatched to the query coroutine
context, which getRoomDatabase sets to Dispatchers.IO. That is the better
behaviour -- it is what stops a query running on the main thread -- but it
is a real change to the shipping platform, made for a target that does not
run yet. Hence the unit tests below rather than a compile alone.
**BusinessManager was not an expect**, so nothing warned about it. It is
now ported to the fork's jvmMain (05ce7eb); Phoenix.jvm.kt and
NavigationViewModel.jvm.kt are otherwise the ios actuals with one changed
import, since those files use no ios API.
**schedulePlatformLogic schedules nothing, and logs that it does not.**
Android starts two WorkManager jobs here, one of which is ChannelsWatcher
-- it wakes periodically to notice a channel force-closed while the app
was shut. A desktop application has no process once its window closes, so
there is nothing to wake, and running the watcher in-process would be
strictly worse than not running it: it would only fire while the app was
already open and watching. The exposure is real and belongs in release
notes rather than a comment -- a desktop wallet left closed past a
force-close deadline does not notice.
Smaller calls. PlatformContext carries an application directory, since
there is no Context to read one from, and PlatformDatabaseBuilder puts
aux.db under it rather than in java.io.tmpdir, which is what the abandoned
Aux implementation did behind a TODO and which most systems clear on
reboot. themeColorScheme ignores dynamicColor, which means Material You
and has no desktop counterpart. AppVersion reads the jar manifest that
compose.desktop writes, falling back when running from a class directory.
Verified: :composeApp:compileKotlinJvm green,
:composeApp:compileDebugKotlinAndroid green, and
:composeApp:testDebugUnitTest 52 passing -- the one that matters, since
this commit changes shared code every android query path goes through.
Not verified: nothing has run. No jvm entry point exists yet, so the
database has never been opened on this platform and no business has been
started. That is phase 5, which also has to unlock JvmKeyStore before the
wallet starts -- a passphrase prompt, not just a window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 01:23:42 +02:00
|
|
|
add("kspJvm", libs.androidx.room3.compiler)
|
2026-03-24 04:47:19 +02:00
|
|
|
// Add any other platform target you use in your project, for example kspDesktop
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 00:05:38 +02:00
|
|
|
room3 {
|
2026-03-24 04:47:19 +02:00
|
|
|
schemaDirectory("$projectDir/schemas")
|
2026-03-23 01:41:39 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 14:39:46 +02:00
|
|
|
sqldelight {
|
|
|
|
|
databases {
|
|
|
|
|
create("ChannelsDatabase") {
|
|
|
|
|
packageName.set("fr.acinq.phoenix.db.sqldelight")
|
|
|
|
|
srcDirs.from("src/commonMain/sqldelight/channelsdb")
|
|
|
|
|
}
|
|
|
|
|
create("PaymentsDatabase") {
|
|
|
|
|
packageName.set("fr.acinq.phoenix.db.sqldelight")
|
|
|
|
|
srcDirs.from("src/commonMain/sqldelight/paymentsdb")
|
|
|
|
|
}
|
|
|
|
|
create("AppDatabase") {
|
|
|
|
|
packageName.set("fr.acinq.phoenix.db.sqldelight")
|
|
|
|
|
srcDirs.from("src/commonMain/sqldelight/appdb")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 01:41:39 +02:00
|
|
|
compose.desktop {
|
|
|
|
|
application {
|
2026-07-15 01:14:46 +02:00
|
|
|
mainClass = "press.mantra.desktop.MainKt"
|
2026-03-23 01:41:39 +02:00
|
|
|
|
|
|
|
|
nativeDistributions {
|
|
|
|
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
2026-07-15 01:14:46 +02:00
|
|
|
packageName = "press.mantra.desktop"
|
2026-03-23 01:41:39 +02:00
|
|
|
packageVersion = "1.0.0"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|