Imported from jvsena42/mandacaru (
AGENTS.md). Install upstream withnpx skills add jvsena42/mandacaru. Copyright stays with the author.
AGENTS.md
This file provides guidance to coding agents (Claude Code, Codex, Cursor, Gemini CLI, and others) when working with code in this repository.
Project Overview
Mandacaru is an Android application that runs a lightweight Bitcoin validation node powered by Utreexo and Floresta. It bridges Rust-based Bitcoin node implementation with a modern Kotlin/Compose Android UI.
Build & Development Commands
Building the Project
# Debug build
./gradlew assembleDebug
# Release build
./gradlew assembleRelease
# Install debug build on connected device
./gradlew installDebug
Running Tests
# Run unit tests
./gradlew test
# Run instrumented tests (requires connected device/emulator)
./gradlew connectedAndroidTest
# Run specific test class
./gradlew test --tests "com.github.jvsena42.mandacaru.SpecificTestClass"
Code Quality
# Run static analysis (must pass before opening a PR)
./gradlew detekt
# Run Android Lint on the debug variant (must pass before opening a PR)
./gradlew lintDebug
# Clean build
./gradlew clean
# Check for updates
./gradlew dependencyUpdates
Lint configuration lives at app/lint.xml; detekt configuration lives at config/detekt/detekt.yml. When a rule fires on generated code or on an intentional project decision (e.g. shipping only arm64-v8a + x86_64, no 32-bit ABIs), prefer tuning the config over disabling the rule globally.
Kotlin Conventions
- Prefer
runCatching { ... }overtry/catchfor error handling. When the original code only swallowed a specific exception type, preserve that by re-throwing others inonFailure(e.g..onFailure { if (it !is IllegalStateException) throw it }). Reservetry/finallyfor guaranteed resource cleanup — that has norunCatchingequivalent. - Inside
suspendfunctions or coroutine builders (launch/withContext/flow), userunSuspendCatching { ... }(incom.github.jvsena42.mandacaru.common) instead ofrunCatchingor a generictry/catch— plainrunCatchingswallowsCancellationExceptionand breaks structured-concurrency cancellation.runSuspendCatchingrethrows it. - Import types and use the short name rather than fully-qualified names inline (e.g. import
androidx.annotation.OptIninstead of writing@androidx.annotation.OptIn(...)). When the simple name collides with an auto-imported one (androidx.annotation.OptInvskotlin.OptIn), use an import alias (e.g.import androidx.annotation.OptIn as AndroidXOptIn).
Development Setup
- Requires Android 10 (API 29) minimum
- ARM64 device (arm64-v8a) or x86_64 emulator - the Rust library is cross-compiled for both
arm64-v8aandx86_64(seeupdate-bindings.sh); 32-bit ABIs are not built - Java 11 language target
- Android Studio with Compose support recommended
Architecture
Layer Structure (Clean Architecture + MVVM)
presentation/ # UI layer with Jetpack Compose
├── ui/screens/ # Screen-specific ViewModels and Composables
│ ├── main/ # MainActivity and navigation
│ ├── node/ # Node status screen
│ ├── search/ # Transaction search screen
│ └── settings/ # Settings and configuration
├── ui/components/ # Reusable Compose components
└── utils/ # UI utilities (EventFlow, notifications)
domain/ # Business logic layer
├── floresta/ # Floresta daemon integration
│ ├── FlorestaDaemon.kt # Daemon lifecycle interface
│ ├── FlorestaDaemonImpl.kt # Rust/FFI integration
│ ├── FlorestaService.kt # Android foreground service
│ └── FlorestaRpcImpl.kt # RPC client implementation
└── model/ # Domain models and RPC DTOs
data/ # Data layer
├── FlorestaRpc.kt # RPC interface
└── PreferencesDataSource.kt # Preferences abstraction
Key Architectural Patterns
- MVVM: ViewModels expose
StateFlow<UiState>consumed by Compose UI - Repository Pattern:
FlorestaRpcinterface abstracts RPC communication - Dependency Injection: Koin manages singleton instances (configured in
MandacaruApplication.kt) - Coroutines + Flow: All async operations use
suspendfunctions andFlow<Result<T>> - Foreground Service:
FlorestaServicekeeps the Bitcoin node alive in background
Rust/Floresta Integration
FFI Bridge (UniFFI)
The app communicates with Rust code via UniFFI-generated bindings:
- Native Library:
libflorestad_ffi.so(built forarm64-v8aandx86_64) - Kotlin Bindings:
com.florestad.florestad.kt(auto-generated) - Main Classes:
Florestad: Rust daemon wrapper withstart()/stop()methodsConfig: Configuration object passed to Rust (network, dataDir, etc.)Network: Enum for Bitcoin/Testnet/Signet/Regtest
Daemon Lifecycle
FlorestaService.onStartCommand()triggersFlorestaDaemon.start()FlorestaDaemonImpl.start()reads network preference, createsConfig, calls FFI- Rust daemon initializes Bitcoin node with Utreexo, binds RPC server to
127.0.0.1:<port> - Android app communicates via JSON-RPC over HTTP
Configuration by Network
Network preferences stored in SharedPreferences:
- CURRENT_NETWORK: "Bitcoin", "Testnet", "Signet", or "Regtest"
- CURRENT_RPC_PORT: Automatically set based on network (see
Constants.kt)
Port mappings (defined in domain/model/Constants.kt):
- Bitcoin: 8332
- Testnet: 18332
- Signet: 38332
- Regtest: 18443
RPC System
JSON-RPC 2.0 Implementation
The app uses OkHttp3 to make HTTP POST requests to the Rust daemon's localhost RPC server.
Request Format:
{
"jsonrpc": "2.0",
"method": "getblockchaininfo",
"params": [],
"id": 1
}
Available RPC Methods (see domain/model/florestaRPC/RpcMethods.kt)
getblockchaininfo: Blockchain sync status, height, Utreexo forest stategetpeerinfo: Connected peer listgettransaction: Transaction lookup by txidloaddescriptor: Add wallet descriptor for address trackinglistdescriptors: List loaded descriptorsaddnode: Connect to specific Bitcoin noderescan: Rescan blockchain for wallet transactionsstop: Gracefully stop the node
RPC Response Models
Located in domain/model/florestaRPC/response/:
GetBlockchainInfoResponse: Contains Utreexo-specific fields (leafCount,rootCount,rootHashes)GetTransactionResponse: Full transaction details with inputs/outputsGetPeerInfoResponse: Peer connection infoAddNodeResponse: Node connection status
Error Handling
RPC methods return Flow<Result<T>>. Check for Result.isFailure to handle errors:
florestaRpc.getTransaction(txId).collect { result ->
result.onSuccess { tx -> /* handle success */ }
result.onFailure { error -> /* handle error */ }
}
State Management
ViewModel Pattern
Each screen has a ViewModel with:
private val _uiState = MutableStateFlow(UiState())val uiState = _uiState.asStateFlow()(read-only exposure)- Action handlers that update state via
_uiState.update { ... }
UI Observation
Compose screens observe state:
val state by viewModel.uiState.collectAsState()
Background Polling
NodeViewModel implements a 10-second polling loop (getInLoop()) to refresh blockchain info while the screen is active.
Important Implementation Notes
Network Changes Require App Restart
When the user changes network in Settings:
- New network is saved to SharedPreferences
SettingsEvents.OnNetworkChangedis broadcastMainActivityreceives event and callsrestartApplication()- App restarts with new
Configpassed to Rust daemon
Foreground Service Notifications
- Service notification is mandatory (Android O+)
- Channel: "floresta_service_channel"
- Notification actions: Open app, Stop service
- POST_NOTIFICATIONS permission required for Android 13+
Transaction Search Validation
The search field validates txid format (64-character hex string) before making RPC calls. Debouncing (500ms) prevents excessive API requests.
Descriptor Format
When loading wallet descriptors via Settings, use standard Bitcoin descriptor format (e.g., wpkh([fingerprint/path]xpub...)). The Rust daemon validates and derives addresses.
Testing Considerations
Unit Tests
- Test ViewModels with fake repositories
- Mock
FlorestaRpcinterface for predictable responses - Use Koin test utilities to inject test dependencies
Instrumented Tests
- Requires Android emulator or physical device
- Service tests need proper permission setup
- RPC tests require running Rust daemon
Agentic UI Tests (android-cli journeys)
- Journey tests live in
journeys/— natural-language XML steps an AI agent runs via theandroid-cliskill (android layout,adb shell input) against an installed debug build on an ARM64 device or x86_64 emulator. journeys/README.mdis the canonical testTag contract: it lists everytestTagand what it maps to. Keep it in sync when adding/renaming a tag.- Compose
testTags surface under theresource-idkey inandroid layoutbecause the root composable (MainActivity.MandacaruRoot) setstestTagsAsResourceId = true. Prefer targeting these stable ids over localized text or bounds. Two caveats learned from real runs: (1) atestTagonly surfaces on a node that already emits semantics (interactive controls, text, nav items) — a plain containerBoxwith only atestTagdoes not appear, so identify the active screen by the selected nav item's"state":["selected"]; (2) dropdowns/dialogs/bottom sheets render in a separate window outside the root subtree, so theirtestTags do not surface — target items inside them by text. - Tags are inline string literals at each call site (no shared constants file) to avoid cross-branch merge conflicts. When adding UI an agent must drive, tag it and document it in
journeys/README.md.
Common Development Scenarios
Adding a New RPC Method
- Add enum to
RpcMethods.kt - Create response model in
domain/model/florestaRPC/response/ - Add method to
FlorestaRpcinterface - Implement in
FlorestaRpcImpl.sendJsonRpcRequest() - Call from ViewModel, update UI state
Adding a New Screen
- Create package under
presentation/ui/screens/ - Create ViewModel with
UiStatedata class - Create Composable screen function
- Add route to
MainActivityNavHost - Register ViewModel in Koin
presentationModule
Modifying Rust Configuration
- Update
Configdata class inFlorestaDaemonImpl.kt - Ensure matching fields exist in Rust
Configstruct - Rebuild UniFFI bindings (handled by Rust build)
- Update
start()initialization logic
Dependencies
Key libraries (see gradle/libs.versions.toml):
- Compose BOM: 2025.09.01 (Material Design 3)
- Navigation Compose: 2.9.5
- Koin BOM: 4.1.1 (DI framework)
- OkHttp: 5.1.0 (HTTP client)
- Gson: 2.11.0 (JSON serialization)
- JNA: 5.14.0 (FFI bridge)
Pull request policy
When publishing changes that touch the Mandacaru build chain — Floresta-mandacaru, floresta-mandacaru-ffi, or mandacaru — open PRs against the fork's own default branch (jvsena42/<repo>:master or :main). Never open PRs against an upstream maintainer repo (e.g. vinteumorg/Floresta) without explicit per-task authorization from the user — "if necessary" or similar conditional phrasing does not count as authorization.
The personal forks carry Mandacaru-specific patches and the user reviews/lands changes there first; upstream PRs are a separate, deliberate decision.
Agent entry points
This file is the single source of guidance. Each tool that does not read AGENTS.md natively gets an entry point that is a symlink to it, so there is only ever one file to edit:
| Entry point | Tool |
|---|---|
CLAUDE.md |
Claude Code |
GEMINI.md |
Gemini CLI |
QWEN.md |
Qwen Code |
.clinerules |
Cline |
.github/copilot-instructions.md |
Copilot in-IDE |
Codex, Cursor, opencode, Zed, Aider, goose, Windsurf, Amp, Jules and GitHub's Copilot coding agent read AGENTS.md directly and need no entry point. .cursor/rules/agents.mdc is the one deliberate exception to the symlink rule: it is a real file because Cursor needs its own YAML frontmatter (alwaysApply: true) to auto-apply the rule, and a symlink would serve this file's frontmatter-less content instead.
On Windows, a clone without symlink support (needs Developer Mode or an elevated shell, plus git config core.symlinks true) materializes each symlink as a one-line text file containing the path AGENTS.md rather than the guidance itself. If an entry point above looks like a single stray filename, that is why — read AGENTS.md directly.
Related Resources
- Floresta Core - Underlying Rust implementation
- Utreexo - Accumulator design
- UniFFI - Rust FFI bindings generator