- MoneroScreen.kt: complete ~800-line UI with send, receive, daemon settings, transaction list, backup seed dialog - Account.kt: add 7 reactive StateFlows and updateMoneroState() - strings.xml: add monero-related string resources - AGENTS.md: update with actual progress state - GARNET_README.md: rewrite merge progress section - Uses MaterialSymbols from commons, no compose-material-icons-extended - compileFdroidDebugKotlin: 0 errors
25 KiB
Garnet Integration into Amethyst - Project Notes & Learnings
Quick Reference
| Item | Value |
|---|---|
| Garnet repo | https://github.com/retrnull/garnet |
| Amethyst repo | https://github.com/vitorpamplona/amethyst |
| Garnet base amethyst commit | 175b79b29 (April 2024) |
| Garnet fork date | ~July 10, 2024 |
| Latest amethyst commit | a4310317a (May 2026) |
| Commits since fork | ~9,330 |
| Garnet version | v0.3.0 |
| Monero source | https://github.com/retrnull/monero (branch: release-v0.18.3.3-garnet) |
| Current status | Phase 1-5 compiled. MoneroScreen UI ported, Account reactive flows added. UI ready, needs navigation wiring. |
What was accomplished (this session)
Previous session accomplishments (Phase 1-4)
- Extracted all monero model classes from Garnet Kotlin code
- Adapted imports for new KMP quartz package structure (
nip01Core.*) - Created wallet model classes (Wallet, PendingTransaction, Subaddress, etc.)
- Created WalletService (fully implemented from Garnet source, ~700 lines)
- Created MoneroWalletListener interface
- Created MoneroTipEvent in quartz (
moneroTippackage) - Created TipSplitSetup utility in quartz (
moneroTippackage) - Created TransactionPriority enum (UNIMPORTANT, SLOW, MEDIUM, FAST, ESTIMATED)
- Created TipHandler and TipEventDataSource (stubs)
- Created MoneroDataSource (stub with flows)
- Added monero wallet fields to Account.kt (spend key, seed, restore height, password flows)
- Added monero wallet methods to Account.kt (startMonero, stopMonero, balance, address, generateSpendKey)
- Added
moneroAddress()method to User model in commons - Added
moneroAddressFromAbout()method to UserMetadataCache in commons - Added
moneroAddressIsValid()utility function in commons - Added EventFactory registration for MoneroTipEvent (kind 1814)
- Added monero.xml icon to resources
- Added monero-specific strings to strings.xml
- Added monero JNI bridge files (monerujo.cpp, monerujo.h, wallet2_api.h)
- Updated amethyst/build.gradle.kts for monero dependencies
This session accomplishments (Phase 5)
- Wired up MoneroDataSource.setMoneroService() - forwards WalletService StateFlows into MoneroServiceFlows
- Added missing Account monero methods - sendMonero, listMoneroAddresses, newSubaddress, setSubaddressLabel, seedWithPassphrase, getMoneroHistory, getMoneroRestoreHeight, getMoneroDaemonStatus
- Added Account monero settings properties - moneroDaemonAddress, moneroDaemonUsername, moneroDaemonPassword, moneroRestoreHeight, moneroPassword, moneroSeed, isMoneroSeedBackedUp, defaultMoneroTransactionPriority (+ setters)
- Added MoneroUtils.kt - showMoneroAmount and decToPiconero helper functions
- Restored TipEventDataSource.getEvent() - simplified from Garnet Flow-based pattern to LocalCache.getOrCreateNote
- Kotlin compilation passes with 0 errors - verified with
compileFdroidDebugKotlin
UI Port accomplishments (MoneroScreen)
- Created MoneroScreen.kt (~795 lines) adapted to Amethyst patterns:
- Uses Amethyst's MaterialSymbols from
commons.icons.symbols(nocompose-material-icons-extendedneeded) - Uses
context.getString()andstringRes()for string resources (not Garnet's Flow-based pattern) - All composables implemented:
MoneroScreen,MoneroSendDialog,ReceiveDialog,EditDaemonDialog,DaemonInfo,Balance,TipInfo,ActionsRow,BackupSeedDialog,EphemeralWalletWarning,TransactionList,TransactionRow - All buttons use
M3ActionDialogfrom Amethyst components - All icons use
MaterialSymbolsfrom commons (ArrowDownward, Info, Settings, CloudUpload, Warning, ContentCopy, ExpandLess, ExpandMore, Send, etc.) - Uses
HostAndPortvia Account setter methods (changeMoneroDaemonAddress,changeMoneroDaemonUsername,changeMoneroDaemonPassword)
- Uses Amethyst's MaterialSymbols from
- Added 12 new string resources for MoneroScreen UI (unconfirmed, monero_tipping_info, no_transactions, transaction_history, custom_restore_height, restoration_height, monero_amount, edit_address_label, info, close, no_results_found, transaction_received, transaction_sent)
- Compiled with 0 errors - verified with
compileFdroidDebugKotlin - Warning:
Divideris deprecated → useHorizontalDivider
Account Reactive State Flows (Session)
- Added 8 reactive StateFlows to Account.kt:
moneroStatus,moneroBalance,moneroLockedBalance,moneroWalletHeight,moneroDaemonHeight,moneroConnectionStatus,moneroTransactions,moneroAddress - Added
updateMoneroState()method to sync wallet service state into reactive flows
What's remaining (next session)
- Wire MoneroScreen into app navigation (Routes.kt + AppNavigation.kt)
- Run
./gradlew assembleFdroidDebugend-to-end after MoneroScreen addition - Add NIP-69 monero address support
- Test monero wallet functionality end-to-end
Next Session Quick Start
To resume work on the Monero integration:
-
Check current state:
./gradlew assembleFdroidDebug | tail -3 -
Verify MoneroScreen.kt exists and compiles:
find ~/projects/amethyst -name "MoneroScreen.kt" -not -path "*/build/*" ./gradlew compileFdroidDebugKotlin 2>&1 | grep -E "^e:|FAILED|SUCCESS" -
The next step: add MoneroScreen navigation route.
Relevant files:
amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MoneroScreen.kt(main UI file)amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt(add route)amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt(wire screen)amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt(reactive flows)
Known Issues & Gotchas
- TransactionDirection enum is in
com.vitorpamplona.amethyst.modelpackage - TextSpinner is in
com.vitorpamplona.amethyst.ui.components.TextSpinner - Deprecated Divider - use
HorizontalDividerinstead in newer Compose - MaterialSymbols - all icons must come from
commons.icons.symbols.MaterialSymbols(notandroidx.compose.material.icons).MaterialSymbols.AutoMirrored.Sendfor the Send icon.
What Worked ✓
21 New Files Created
| # | File | Description |
|---|---|---|
| 1 | amethyst/src/main/java/.../model/MoneroWalletListener.kt |
JNI wallet listener callback interface |
| 2 | amethyst/src/main/java/.../model/PendingTransaction.kt |
JNI pending transaction wrapper |
| 3 | amethyst/src/main/java/.../model/ProofInfo.kt |
Proof verification data class |
| 4 | amethyst/src/main/java/.../model/Subaddress.kt |
Monero subaddress model |
| 5 | amethyst/src/main/java/.../model/Transaction.kt |
Monero transaction model |
| 6 | amethyst/src/main/java/.../model/TransactionInfo.kt |
Transaction info wrapper |
| 7 | amethyst/src/main/java/.../model/TransactionHistory.kt |
Transaction history model |
| 8 | amethyst/src/main/java/.../model/Transfer.kt |
Transfer data class |
| 9 | amethyst/src/main/java/.../model/Wallet.kt |
JNI Wallet wrapper (all native methods) |
| 10 | amethyst/src/main/java/.../model/WalletManager.kt |
Wallet factory + native bridge utilities |
| 11 | amethyst/src/main/java/.../service/MoneroDataSource.kt |
Data source stub with Flow wrappers |
| 12 | amethyst/src/main/java/.../service/TipEventDataSource.kt |
Tip event lifecycle stubs |
| 13 | amethyst/src/main/java/.../service/TipHandler.kt |
Tip processing logic (partial) |
| 14 | amethyst/src/main/java/.../service/WalletService.kt |
Android Service + all wallet operations |
| 15 | amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt |
Transaction priority enum |
| 16 | amethyst/src/main/res/drawable/monero.xml |
Monero icon resource |
| 17 | quartz/src/commonMain/kotlin/.../moneroTip/MoneroTipEvent.kt |
Nostr tip event (kind 1814) |
| 18 | quartz/src/commonMain/kotlin/.../moneroTip/TipSplitSetup.kt |
Tip split utilities + monero tag parsing |
| 19 | amethyst/src/main/cpp/monerujo.cpp |
JNI bridge C++ implementation |
| 20 | amethyst/src/main/cpp/monerujo.h |
JNI bridge header |
| 21 | amethyst/src/main/cpp/wallet2_api.h |
Monero wallet API header |
6 Modified Files
| # | File | Changes |
|---|---|---|
| 1 | amethyst/src/main/java/.../model/Account.kt |
Added monero wallet fields + methods |
| 2 | amethyst/src/main/res/values/strings.xml |
Added monero string resources |
| 3 | commons/src/commonMain/kotlin/.../commons/model/User.kt |
Added moneroAddress(), moneroAddressIsValid() |
| 4 | commons/src/commonMain/kotlin/.../commons/model/nip01Core/UserMetadataCache.kt |
Added moneroAddressFromAbout() |
| 5 | quartz/src/commonMain/kotlin/.../utils/EventFactory.kt |
Registered MoneroTipEvent (kind 1814), import |
| 6 | amethyst/build.gradle.kts |
Added monero module/dependencies |
Current File Structure with Exact Import Paths
This is the most critical section. All Garnet-era import paths were different because the quartz module was not yet Kotlin Multiplatform. Here are the exact current paths every imported type should use:
Quartz (MoneroEvent) Import Paths
CRITICAL: These are DIFFERENT from Garnet source.
| Type | New import path |
|---|---|
| Event base class | com.vitorpamplona.quartz.nip01Core.core.Event |
| HexKey | com.vitorpamplona.quartz.nip01Core.core.HexKey |
| IEvent | com.vitorpamplona.quartz.nip01Core.core.IEvent |
| AddressableEvent | com.vitorpamplona.quartz.nip01Core.core.AddressableEvent |
| JsonMapper | com.vitorpamplona.quartz.nip01Core.core.JsonMapper |
| OptimizedSerializable | com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable |
| TagArray | com.vitorpamplona.quartz.nip01Core.core.TagArray |
| KeyPair | com.vitorpamplona.quartz.nip01Core.crypto.KeyPair (was com.vitorpamplona.quartz.nip01Core.enc.KeyPair in garnet) |
| NostrSignerInternal | com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal (was ...signs.NostrSignerInternal) |
| NostrSigner | com.vitorpamplona.quartz.nip01Core.signers.NostrSigner |
| EventTemplate | com.vitorpamplona.quartz.nip01Core.signers.EventTemplate |
| ETag | com.vitorpamplona.quartz.nip01Core.tags.events.ETag |
| PTag | com.vitorpamplona.quartz.nip01Core.tags.people.PTag |
| EventHintProvider | com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider |
| PubKeyHintProvider | com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider |
| NormalizedRelayUrl | com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl |
Package Locations
| Type | Location |
|---|---|
| Note class | com.vitorpamplona.amethyst.commons.model.Note (was amethyst-only before, now in commons) |
| User class | com.vitorpamplona.amethyst.commons.model.User (was amethyst-only before, now in commons) |
| LocalCache | com.vitorpamplona.amethyst.model.LocalCache (stays in amethyst) |
| MoneroTipEvent | com.vitorpamplona.quartz.moneroTip.MoneroTipEvent |
| TipSplitSetup | com.vitorpamplona.quartz.moneroTip.TipSplitSetup |
Old Garnet → New Amethyst Mapping
Many Garnet-era packages have been renamed. Common mappings:
| Old (Garnet/2024) | New (2026) |
|---|---|
com.vitorpamplona.quartz.events.EncryptedContentUtils |
com.vitorpamplona.quartz.nip01Core.crypto.EventHasher |
com.vitorpamplona.quartz.events.* |
com.vitorpamplona.quartz.nip01Core.* (various subpackages) |
com.vitorpamplona.quartz.events.signs.* |
com.vitorpamplona.quartz.nip01Core.signers.* |
com.vitorpamplona.quartz.events.enc.* |
com.vitorpamplona.quartz.nip01Core.crypto.* |
com.vitorpamplona.quartz.events.crypto.* |
com.vitorpamplona.quartz.nip01Core.crypto.* |
commons.model.* (amethyst) |
commons.model.* (commons-commonMain) |
NostrSigner API Change (CRITICAL)
The signing API changed significantly. Garnet-era code uses callback-based signing. Current Amethyst uses coroutine-based signing.
Old (callback, Garnet-era)
// Garnet's NostrSigner callback style
signer.sign(eventBuilder, onReady: (Event) -> Unit)
New (coroutine, current)
// Current Amethyst coroutine style
val event = signer.sign(template)
// or
val event = signer.sign(createdAt, kind, tags, content)
NostrSignerInternal signing with createdAt
In current code, NostrSignerInternal.sign() with createdAt uses the vararg tags variant:
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
val event: MoneroTipEvent = NostrSignerInternal(KeyPair())
.sign(createdAt = TimeUtils.now(), kind = MoneroTipEvent.KIND, tags = tags.toTypedArray(), content = content)
See LnZapRequestEvent.kt:130 for a real example of creating an anonymous signer for private zaps.
LocalCache.consume() Signature
The justConsume() method in LocalCache takes 3 parameters:
fun justConsume(event: Event, relay: NormalizedRelayUrl?, isOwn: Boolean): Boolean
| Parameter | Description |
|---|---|
event |
The Nostr event to consume |
relay |
The relay it came from (null if from client) |
isOwn |
Whether this is the current account's own event |
Common usage patterns:
// Consume from relay, not own event
cache.justConsume(event, relay, false)
// Consume own event (no relay)
cache.justConsumeMyOwnEvent(event) // convenience wrapper, same as justConsume(event, null, true)
// Consume gift wrap / inner event from push
cache.justConsume(innerGift, null, false)
// Consume decrypted inner event from Marmot
cache.justConsume(innerEvent, null, true)
User Metadata Access Pattern
User metadata in commons.model.User is accessed via lazy-initialized caches:
// UserMetadataCache
val cache = user.metadata() // lazy init
val flow: StateFlow<UserInfo?> = cache.flow
val name = cache.bestName() // String?
val picture = cache.profilePicture() // String?
val lnAddress = cache.lnAddress() // String?
// For monero:
val moneroAddr = cache.moneroAddressFromAbout() // String?
In User model, the moneroAddress() method tries nip69Address() first (not yet implemented), then falls back to moneroAddressFromAbout():
fun User.moneroAddress(): String? {
val nip69 = nip69Address() // NIP-69 type-0 event, stub returns null
if (nip69 != null) return nip69
return metadataOrNull()?.moneroAddressFromAbout() // regex from about field
}
The regex in moneroAddressFromAbout(): \b[48]\w{93}\b — matches standard Monero addresses (4xxxx or 8xxxx, 95 chars total).
Amethyst.instance Reference Pattern
The global Amethyst singleton uses these accessors:
import com.vitorpamplona.amethyst.Amethyst
val context = Amethyst.instance.appContext // NOT applicationContext
val customScope = Amethyst.instance.customScope // UI coroutine scope
val ioScope = Amethyst.instance.applicationIOScope // IO coroutine scope
Important: Use appContext, NOT applicationContext. Garnet-era code used applicationContext which no longer compiles.
See WalletService.kt:68-80 for the correct pattern:
Amethyst.instance.appContext.getString(R.string.wallet_status_opening)
Amethyst.instance.applicationIOScope // used for scope.launch in wallet listener
TransactionPriority Enum
Defined at: amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt
enum class TransactionPriority(val value: Int) {
UNIMPORTANT(0),
SLOW(1),
MEDIUM(2),
FAST(3),
ESTIMATED(4),
}
Used everywhere Monero transactions are created:
wallet.createTransaction(
destination = address,
amount = amount,
priority = TransactionPriority.UNIMPORTANT // passes priority.ordinal to JNI
)
WalletService Public API
WalletService (Android Service) implements the full wallet. All public methods:
| Method | Return | Description |
|---|---|---|
loadWallet(name, password, spendKey="", daemonAddress, ...) |
Wallet |
Open or create wallet |
load(name, password, spendKey="", daemonAddress, ...) |
Wallet |
Same, with checkNotInMainThread |
connectToDaemon(address, username, password, proxy, restoreHeight, walletPassword) |
Wallet.Status? |
Connect to daemon |
connect(address, username, password, proxy, isCreation, restoreHeight, walletPassword) |
Wallet.Status? |
Same, check not on main thread |
sendTransaction(destination, amount, priority) |
PendingTransaction.Status? |
Send to single address |
sendTransactionMultDest(destinations[], amounts[], priority) |
PendingTransaction? |
Multi-destination |
getTxProof(txId, destination, message) |
Proof? |
Generate transaction proof |
checkTxProof(txId, address, message, signature) |
ProofInfo? |
Verify a transaction proof |
setProxy(proxy) |
Boolean? |
Set network proxy |
storeWallet() |
Wallet.Status? |
Persist wallet to disk |
closeWallet() |
Unit | Close the current wallet |
newSubaddress(accountIndex, label) |
Subaddress? |
Generate new subaddress |
lastSubaddress(accountIndex) |
Subaddress? |
Get last subaddress |
listAddresses() |
List<Subaddress>? |
List all subaddresses |
setSubaddressLabel(index, label) |
Wallet.Status? |
Label a subaddress |
seedWithPassphrase(passphrase) |
String? |
Get wallet seed |
isAddressValid(address) |
Boolean? |
Validate address format |
getRestoreHeight() |
Long? |
Get restore height |
getHistory() |
TransactionHistory? |
Get transaction history |
setUserNote(txId, note) |
Boolean? |
Attach user note to transaction |
estimateTransactionFee(destinations[], amounts[], priority) |
Long? |
Estimate fee in picoMonero |
getBalance |
Long |
Property: total balance |
getLockedBalance |
Long |
Property: locked (unconfirmed) balance |
walletHeight |
Long |
Property: wallet blockchain height |
daemonHeight |
Long |
Property: daemon blockchain height |
address |
String? |
Property: primary address |
connectionStatus |
Wallet.ConnectionStatus |
Property: DISCONNECTED/CONNECTED/WRONG_VERSION |
State flows (MutableStateFlow → StateFlow):
statusStateFlow: StateFlow<WalletStatus>balanceStateFlow: StateFlow<Long>lockedBalanceStateFlow: StateFlow<Long>walletHeightStateFlow: StateFlow<Long>daemonHeightStateFlow: StateFlow<Long>connectionStatusStateFlow: StateFlow<Wallet.ConnectionStatus>transactions: StateFlow<List<TransactionInfo>>
MoneroDataSource — Current State
File: amethyst/src/main/java/.../service/MoneroDataSource.kt
object MoneroDataSource {
@Volatile
private var flows: MoneroServiceFlows = MoneroServiceFlows()
fun status(): Flow<*> = flows.status
fun connectionStatus(): Flow<*> = flows.connectionStatus
fun walletHeight(): Flow<Long> = flows.walletHeight
fun daemonHeight(): Flow<Long> = flows.daemonHeight
fun balance(): Flow<Long> = flows.balance
fun lockedBalance(): Flow<Long> = flows.lockedBalance
fun transactions(): Flow<List<TransactionInfo>> = flows.transactions
fun setMoneroService(service: WalletService) {
val newFlows =
MoneroServiceFlows(
status = service.statusStateFlow,
connectionStatus = service.connectionStatusStateFlow,
walletHeight = service.walletHeightStateFlow,
daemonHeight = service.daemonHeightStateFlow,
balance = service.balanceStateFlow,
lockedBalance = service.lockedBalanceStateFlow,
transactions = service.transactions,
)
flows = newFlows
}
}
The setMoneroService() forwards all 7 WalletService StateFlows into MoneroServiceFlows. It must be called from AccountViewModel or service binding to wire the flows.
What Next Session Must Do
Priority A: Wire MoneroScreen into Navigation
- Add
Moneroroute inRoutes.kt(sealed class Route) - Add composable mapping in
AppNavigation.kt - Wire from settings screen or top navigation drawer
Priority B: Add NIP-69 Monero Address Support
NIP-69 defines a peer-to-peer way to share Monero addresses via Nostr events. Consider implementing a type-0 NIP-69 event for monero address sharing.
Priority C: End-to-End Testing
- Test wallet creation and daemon connection
- Test sending and receiving Monero
- Test subaddress generation
- Test tip events with monero tags
Build Commands
Verify baseline builds
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
Get the diff between Garnet and the fork point
cd ~/projects/garnet
git diff 99614f07..bfaf92e5 --stat # 103 files
git diff 99614f07..bfaf92e5 --name-status # file list with A/M/D status
Extract native libs from garnet releases
curl -L -o /tmp/garnet.apk "https://github.com/retrnull/garnet/releases/download/v0.3.0/app-mainnet-fdroid-universal-release.apk"
unzip -o /tmp/garnet.apk -d /tmp/garnet/
# libmonerujo.so is in extracted-libs/lib/<abi>/
Compile only Kotlin (fast check for monero-related errors)
cd ~/projects/amethyst && ./gradlew compileFdroidDebugKotlin 2>&1 | grep -E "^e:|FAILED|SUCCESS"
Count monero compilation errors
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" | grep -i monero | wc -l
Watch monero-specific errors in real time
./gradlew compileFdroidDebugKotlin 2>&1 | tee /tmp/build.log | grep "^e:" | grep -i monero
Common Error Patterns We Fixed
These are specific compile/lint errors encountered during the port, with fixes applied:
1. No such file or directory: 'com.vitorpamplona.quartz.events.*'
Cause: Quartz module restructured from main/java/com/vitorpamplona/quartz/events/ to commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/
Fix: Replace all old imports with new package paths. Mapping table above.
2. Cannot access 'com.vitorpamplona.quartz.nip01Core.enc.KeyPair'
Cause: KeyPair moved from enc.* to crypto.*
Fix: com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
3. Unresolved reference: NostrSignerInternal from signs.*
Cause: Signers package renamed from signs to signers
Fix: com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
4. Val cannot be reassigned / Property setter not found
Cause: Garnet used var properties; current codebase uses val with delegation
Fix: Use cache.update(...) pattern or MutableStateFlow.update { } instead of direct assignment.
5. Cannot inline class: KeyPair
Cause: KeyPair was a regular class in Garnet; now it may be a value class or internal class
Fix: Use KeyPair constructor properly or access through NostrSignerInternal factory.
6. Unresolved reference: applicationContext on Amethyst.instance
Cause: Amethyst renamed applicationContext to appContext
Fix: Amethyst.instance.appContext
7. Function 'justConsume' doesn't have the expected number of parameters
Cause: Garnet used 1-parameter justConsume(event), new version uses 3: justConsume(event, relay, isOwn)
Fix: Add relay and isOwn parameters: cache.justConsume(event, null, true) for own events.
8. Type mismatch: inferred type is commons.model.User but ...
Cause: User moved from amethyst.model.User to commons.model.User
Fix: Update all imports: com.vitorpamplona.amethyst.commons.model.User
9. No parameter with name 'autoCorrect' in TextField
Cause: Material3 TextField removed autoCorrect parameter in the new Compose BOM
Fix: Remove autoCorrect = false parameter from TextField calls.
10. Unresolved reference: rememberRipple
Cause: Material3 ripple API changed/removed
Fix: Use rememberRipple(color = ...) or remove the ripple parameter from composables.
11. Cannot find heading or HeadingStyle
Cause: HeadingStyle enum moved or was renamed during Compose upgrade
Fix: Check current richtext package for the correct heading/composable type.
12. No parameterless constructor found on sealed interfaces
Cause: Some sealed hierarchies moved from sealed class to sealed interface
Fix: Create instances via companion object create() methods or object expressions, not constructors.
Quick Commands for New Session
All paths use ~/ or $HOME — do NOT use absolute /home/ paths:
# Verify amethyst builds
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
# Count remaining monero errors
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" -i monero | wc -l
# Get garnet diff for reference
cd ~/projects/garnet && git diff 99614f07..bfaf92e5 --stat
# Check current monero files in amethyst
find ~/projects/amethyst -name "*.kt" -path "*monero*" -not -path "*/build/*"
# Find all new files added this session
git status --short | grep "^A "
# Find all modified files
git status --short | grep "^M "