Files
amethyst/AGENTS.md
Nick Pulido 6cee6540ea
Create Release Assets / build-desktop (arm64, macos, macos-14, packageReleaseDmg) (push) Has been cancelled
Create Release Assets / build-desktop (x64, linux, ubuntu-latest, packageReleaseDeb packageReleaseRpm) (push) Has been cancelled
Create Release Assets / build-desktop (x64, linux-portable, ubuntu-latest, createReleaseAppImage createReleaseDistributable) (push) Has been cancelled
Create Release Assets / build-desktop (x64, windows, windows-latest, packageReleaseMsi createReleaseDistributable) (push) Has been cancelled
Create Release Assets / build-cli (arm64, macos, macos-14, amyImage) (push) Has been cancelled
Create Release Assets / build-cli (x64, linux, ubuntu-latest, amyImage jpackageDeb jpackageRpm) (push) Has been cancelled
Create Release Assets / deploy-android (push) Has been cancelled
feat: Add MoneroScreen UI and Account reactive flows
- 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
2026-05-22 18:22:11 -07:00

590 lines
25 KiB
Markdown

# 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 (`moneroTip` package)
- Created TipSplitSetup utility in quartz (`moneroTip` package)
- 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` (no `compose-material-icons-extended` needed)
- Uses `context.getString()` and `stringRes()` 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 `M3ActionDialog` from Amethyst components
- All icons use `MaterialSymbols` from commons (ArrowDownward, Info, Settings, CloudUpload, Warning, ContentCopy, ExpandLess, ExpandMore, Send, etc.)
- Uses `HostAndPort` via Account setter methods (`changeMoneroDaemonAddress`, `changeMoneroDaemonUsername`, `changeMoneroDaemonPassword`)
- **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**: `Divider` is deprecated → use `HorizontalDivider`
#### 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 assembleFdroidDebug` end-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:
1. Check current state:
```bash
./gradlew assembleFdroidDebug | tail -3
```
2. Verify MoneroScreen.kt exists and compiles:
```bash
find ~/projects/amethyst -name "MoneroScreen.kt" -not -path "*/build/*"
./gradlew compileFdroidDebugKotlin 2>&1 | grep -E "^e:|FAILED|SUCCESS"
```
3. 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.model` package
- **TextSpinner** is in `com.vitorpamplona.amethyst.ui.components.TextSpinner`
- **Deprecated Divider** - use `HorizontalDivider` instead in newer Compose
- **MaterialSymbols** - all icons must come from `commons.icons.symbols.MaterialSymbols` (not `androidx.compose.material.icons`). `MaterialSymbols.AutoMirrored.Send` for 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)
```kotlin
// Garnet's NostrSigner callback style
signer.sign(eventBuilder, onReady: (Event) -> Unit)
```
### New (coroutine, current)
```kotlin
// 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:
```kotlin
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**:
```kotlin
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:
```kotlin
// 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:
```kotlin
// 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()`:
```kotlin
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:
```kotlin
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:
```kotlin
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`
```kotlin
enum class TransactionPriority(val value: Int) {
UNIMPORTANT(0),
SLOW(1),
MEDIUM(2),
FAST(3),
ESTIMATED(4),
}
```
Used everywhere Monero transactions are created:
```kotlin
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`
```kotlin
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
1. Add `Monero` route in `Routes.kt` (sealed class Route)
2. Add composable mapping in `AppNavigation.kt`
3. 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
```bash
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
```
### Get the diff between Garnet and the fork point
```bash
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
```bash
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)
```bash
cd ~/projects/amethyst && ./gradlew compileFdroidDebugKotlin 2>&1 | grep -E "^e:|FAILED|SUCCESS"
```
### Count monero compilation errors
```bash
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" | grep -i monero | wc -l
```
### Watch monero-specific errors in real time
```bash
./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:
```bash
# 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 "
```