dff41d417d8553b0aee9d79c84907e3d625397ca
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
adf1f03817 |
feat: a desktop entry point, and the first code here that runs
Phase 5. `press.mantra.desktop.MainKt` has been named by the compose.desktop block since before this work started and did not exist; now it does, and `./gradlew :composeApp:run` opens a window. **The window opens onto a passphrase gate, not onto the app.** That is phase 3 landing here rather than there, and it was not in the plan. keyStoreEncryption(keyName, plainText) takes no secret, because on android the OS keystore serves keys without asking anybody anything -- so a passphrase scheme needs an unlock the expect signature cannot express. MainKt calls JvmKeyStore.unlock before MantraApp is composed, off the ui thread, because Argon2id at 64 MiB is deliberately slow enough to stop the window painting. The gate says on its face that this build is not for real funds. One application directory is handed to both the mantra and the phoenix context, so a single install keeps a single place on disk rather than two named after different projects. **MantraDatabaseJvmTest is the part worth keeping.** Running the app proves the window paints; it proves nothing about Room, because the gate stops before anything touches the database. Six tests now open it: the schema is created, a profile survives a write and a read, upsert replaces rather than duplicates, the @Transaction relation query behind findChatRoomById reads back, a soft-deleted room stops being found, and the on-disk builder writes under the context directory rather than java.io.tmpdir. This is the first time this database has been opened anywhere but android, and it covers exactly what the compiler cannot see -- that Room's ksp output for this target is usable, that the *host* SQLite native loads where the android artifact's would not, and that the 58 queries forced from blocking to suspend still return what they stored. Both of that test's first drafts were wrong in ways worth keeping the scars of. Every write failed with SQLite error 787 because Profile has a foreign key onto NostrEvent and the test never created the parent row -- which is evidence rather than an annoyance, since a schema whose constraints were quietly off would have let all of it pass. And Kind is a typealias for Int, not a constructor. Window sizing is 480x900: a starting size that does not immediately misrepresent layouts only ever exercised at phone widths, not a considered desktop layout. That, along with back handling and any ui offering an nfc affordance, is the shakeout this phase names and does not do. Verified, all five green: :composeApp:compileKotlinJvm, :composeApp:compileDebugKotlinAndroid, :composeApp:testDebugUnitTest (52), :composeApp:jvmTest (6), and the fork's :library:jvmTest (97). Not verified: nothing past the gate. No seed has been written, no business started, no relay contacted. A gradle `run` killed with SIGTERM reports BUILD FAILED with exit value 143 -- that is the signal, not the app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bf4041a5b2 |
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> |
||
|
|
670f87a609 |
docs: record what phase 3 turned out to require
Phase 3 is implemented in the fork on claude/jvm-target-actuals (ce49657). The security analysis in the plan held up; three practical constraints around it did not appear until the code was written. **A passphrase-derived KEK is not a drop-in.** The plan treated the choice between a passphrase, an OS keychain and a key file as the whole decision. But keyStoreEncryption(keyName, plainText) takes no context and no secret -- on android the OS holds the key, so none is needed -- which means any passphrase scheme needs an out-of-band unlock the expect cannot express. That is a change to application startup, not just to the actual, so it is now called out against phase 5: the desktop entry point has to prompt and unlock before the wallet starts. **The iv must be 16 bytes.** EncryptedSeed.V2.serialize in commonMain throws on anything else, which rules out a conventional 96-bit GCM nonce -- worth knowing before designing around one. It turns out to help: with randomly generated nonces the risk is a repeat under one key, and 128 bits makes that vanishingly unlikely where 96 merely makes it unlikely. **Argon2id costs a dependency.** The jdk has PBKDF2 and no memory-hard KDF at all, so it means bouncycastle. Recorded with the reason to pay it: if the build is dev-only because it lacks hardware backing, weakening the KDF too gets the trade backwards. Also recorded: wrap a per-name data key under the KEK rather than encrypting the seed with it directly, so a passphrase change rewraps 32 bytes; throw java.security.KeyStoreException when locked, since the graceful* wrappers already map it to DecryptSeedResult.Failure.KeyStoreFailure; and a verification section naming the properties that fail quietly, plus the two limits worth writing down rather than fixing -- the first unlock on a new store accepts any passphrase, and zeroing the key is best effort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
506dfea802 |
docs: correct phase 2 against what the jdbc drivers actually needed
Phase 2 is implemented and verified in the fork on claude/jvm-target-actuals (d1a82ea). Three corrections and one omission. **Schema handling does not need hand-rolling.** The plan said "you call Schema.create(driver) and the migration path explicitly, and you have to track the applied version yourself". SQLDelight 2.x ships a factory function that shadows the constructor -- JdbcSqliteDriver(url, properties, schema, migrateEmptySchema, vararg callbacks) -- which does all three, user_version included. The same-named constructor does none of it, which is the trap worth naming rather than the work that was budgeted for. **Foreign keys were the actual work, and the plan never mentioned them.** Off by default in SQLite, and the pragma is per connection while JdbcSqliteDriver opens one per thread, so it has to go through the connection Properties rather than be issued once against the driver. Recorded along with why that needs no compile dependency on org.xerial:sqlite-jdbc, which arrives at runtime scope only. **commonTest has an expect too.** The 23 counted at the top of this document are commonMain's. Declaring jvm() also creates jvmTest, which inherits commonTest, so `connect` in ElectrumServersTest blocks every jvm test from compiling. Noted along with the reason not to stub it empty the way ios does: the class is @Ignore'd everywhere, so an empty body looks harmless right up until somebody removes the @Ignore and connect_to_mainnet_servers starts passing without connecting to anything. **Phase 2 is the first phase that can be run, and the plan told you not to bother.** It said "none of this is exercisable until Phase 4. Write the SQLDelight schema-creation path against a scratch main() if you want feedback sooner." That was wrong twice: library/src/jvmTest/ already exists, and the two properties worth checking are exactly the ones a compiler cannot see. Schema creation and the foreign-key pragma both fail silently in production -- a missing table only shows up at first query, and foreign keys being off means cascading deletes quietly do not happen. The phase now carries a real exit condition, and DbFactoryJvmTest meets it with five passing tests. Recorded with it: the two KeyStoreFunctions actuals have to exist before phase 3 decides anything, because nothing jvm compiles without them, and they should throw rather than do something plausible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0757e50dc5 |
docs: correct the jvm plan against what phase 1 actually did
Phase 1 is implemented and verified in the lightning-kmp-app fork on claude/jvm-target-actuals (27a0054). Four things in the plan were wrong, and doing the work is what surfaced them. **jvm() belongs at the start of phase 1, not phase 4 -- for the library.** The plan said leave it off in both builds until phase 4. That is right for mantra and wrong for the fork: library/src/jvmMain/ is an orphan source set until the library declares the target, so phases 1-3 would all have been written blind. Declared first, `:library:compileKotlinJvm` names the remaining expects, and that list beats grepping for `expect ` -- it shrinks by exactly what you implement and cannot drift from the truth. The build stays red across phases 1-3 by design. That checklist is now recorded as the phase 1 exit condition: exactly eight expects should remain, and exactly which eight. Anything else means something in the phase is wrong. **Phase 3 is two decisions, not four.** gracefulSingleSeedDecryption and gracefulMultiSeedDecryption are pure exception mapping into a DecryptSeedResult, and the exception they branch on is java.security.KeyStoreException -- a plain JCA type that exists on the jvm. Both are near-copies of the android actuals and need nothing settled first, so they move alongside phase 2. Only keyStoreEncryption and keyStoreDecryption are the security decision, and that part of the analysis stands. **The Fibonacci template must not be deleted.** The plan said to drop it "assuming nothing references them". Things do: generateFibi is exercised by template tests in commonTest, androidHostTest, iosTest, jvmTest and linuxX64Test, and JvmFibiTest asserts a value that depends on precisely the two properties fibiprops.jvm.kt defines. That file already satisfies two of the 25 expects, which is why the count was 23 missing rather than 25. Removing the template is five test files plus four fibiprops.* actuals, and it is a separate cleanup. **Phase 1 is fifteen actuals, not fourteen**, and two of them are not copies of android -- platformElectrumRegtestConf (10.0.2.2 is the emulator's alias for the host loopback; a jvm process is already on the host) and phoenixLogWriters (android routes kermit into slf4j because android tooling reads that back). Also recorded, because it cost time: a worktree cannot run gradle at all until the submodules are checked out *and* local.properties exists at five levels. Neither is version controlled, so a fresh worktree has neither, and the failure surfaces four builds down at :...:secp256k1-kmp:jni:android as "SDK location not found" rather than anywhere obviously related. Both builds were run: `:library:compileKotlinJvm` fails only on the known eight, and `:composeApp:compileDebugKotlinAndroid` still passes with the library's jvm target declared -- the check that matters, since a new variant must not change how the android target resolves the library. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2aaa7b99a6 |
build: phase 0 of the jvm target -- clear the ground, correct the plan
First phase of docs/jvm-target.md. Nothing here turns the target on; it
removes what would break the moment it is turned on, and stages the two
catalog entries that cannot be derived automatically. Two of the four
steps as written in the doc turned out to be wrong, and implementing them
is how that surfaced -- both are corrected in the doc in this commit.
**Deleted the stale jvmMain tree.** Six files under
composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux
project this codebase grew out of. They have gone unnoticed because
`jvmMain` is currently an orphan source set -- the accessor creates it,
no target compiles it -- so the wrong package, the Room 2 imports
(androidx.room, not androidx.room3), and the references to a long-gone
AuxDatabase and AuxGlobal have never had to resolve. They would all become
compile errors in phase 4.
They are not lost: they are the closest thing to a skeleton for five of
the six platform actuals phase 4 needs, and main.kt is a reasonable
starting shape for the phase 5 desktop entry point. `git show HEAD~1` has
them.
**Added two catalog entries, not four.** sqlite-bundled-jvm and
sqldelight-sqlite-driver. Both earn their place by being unreachable
otherwise: sqlite-bundled-jvm has to be named explicitly because
variant-aware resolution hands the *android* artifact to anything running
on the host, and sqldelight-sqlite-driver is the jvm counterpart to the
android-driver and native-driver entries already there.
The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither
is right. Once jvm() exists, commonMain's existing androidx-room3-runtime
resolves to the -jvm variant on its own, so an explicit entry is
redundant and would drift. And the SQLDelight drivers phase 2 needs are
for DbFactory, which lives in lightning-kmp-app -- a separate gradle build
with its own version catalog, where an entry here is simply not visible.
**kspJvm cannot be wired yet, and the build file already said so.** The
doc's phase 0 told you to uncomment
composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is
where jvm() gets turned on. The comment three lines above it states the
rule:
These configurations only exist when the ios targets are declared,
which the kotlin block above does only on a mac.
The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws
UnknownConfigurationException until a jvm() target creates the
configuration. So it moves into phase 4, into the same edit that declares
the target. composeApp/build.gradle.kts is deliberately untouched by this
commit.
**Also documented: gradle does not run in a worktree here at all** until
the submodule is checked out, which worktrees do not do automatically.
`lightning-kmp-app/` is empty and configuration fails with "Project with
path ':library' not found in build ':lightning-kmp-app'". Recorded in the
phase 0 verification section along with the caveat that a linked worktree
shares .git/modules/ with the main checkout, so both trees end up on one
submodule git dir.
**Not verified by a build.** For that reason. The deletion is an orphan
source set and the additions are unreferenced catalog lines, so neither
can change a build's outcome -- but that is an argument, not a green
check, and it is the second commit in a row on this branch that has not
compiled anything. Phase 4 is the first phase that genuinely cannot be
done without a working gradle invocation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5abc37e463 |
docs: scope the jvm target, and separate it from testing the daos
Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |