# BLOCKERS.md - Phase 1 Implementation Status ## Task 1: Module Scaffolding ✅ COMPLETE ### Completed - ✅ Module structure created (api/impl/test) - ✅ Metro DI setup following Element X patterns - ✅ WalletEntryPoint and WalletState APIs defined - ✅ PaymentFlowNode placeholder with Appyx navigation - ✅ FakeWalletEntryPoint for testing - ✅ Cardano client library dependencies added - ✅ ProGuard rules configured - ✅ Basic unit tests added - ✅ Pushed to Gitea phase1-dev branch --- ## Task 2: Key Generation + Storage ✅ COMPLETE ### Completed - ✅ **CardanoNetworkConfig.kt** - Single object for testnet/mainnet config swap - Currently configured for TESTNET (preprod) - Change `NETWORK` to `CardanoNetwork.MAINNET` for production - All derived values (Koios URL, explorer URL, address prefix) auto-switch - ✅ **CardanoKeyStorage** (interface + implementation) - Per-session wallet isolation (key alias: `cardano_wallet_{sessionId}`) - 24-word BIP-39 mnemonic generation using cardano-client-lib - AES-GCM-256 encryption with Android Keystore-backed key - `setUserAuthenticationRequired(true)` - biometric/PIN for every operation - `setUserAuthenticationValidityDurationSeconds(-1)` - no grace period - `setInvalidatedByBiometricEnrollment(true)` - invalidate on biometric change - Methods: `generateWallet`, `importWallet`, `getMnemonic`, `getBaseAddress`, `getStakeAddress`, `deleteWallet` - ✅ **CardanoWalletManager** (interface + implementation) - Key derivation using CIP-1852 via cardano-client-lib's Account class - Path `m/1852'/1815'/0'/0/0` for external receiving address - Path `m/1852'/1815'/0'/2/0` for staking key - Shelley base address generation (payment + staking key hash) - Uses CardanoNetworkConfig for network selection - Exposes: `getAddress(sessionId)`, `getStakeAddress(sessionId)`, `getSpendingKey(sessionId)` - ✅ **SeedPhraseManager** (interface + implementation) - 24-word mnemonic generation (256-bit entropy) - Support for 12/15/18/21/24 word counts - BIP-39 validation (checksum + wordlist) - Word suggestions for autocomplete - Normalization (whitespace, case) - ⚠️ UI must apply `FLAG_SECURE` when displaying seed phrases (documented) - ✅ **FakeCardanoKeyStorage** for testing - ✅ Unit tests for SeedPhraseManager, CardanoNetworkConfig, CardanoWalletManager ### Decisions Made (per instructions) - Wallet scope: **PER SESSION** (each Matrix account has its own wallet) - Biometric change: **INVALIDATE** key + require wallet re-import/creation - Network: **TESTNET** (preprod) - single config constant for easy mainnet swap ### Not Verified (No Android SDK in build environment) - ⚠️ Compilation with `./gradlew :features:wallet:impl:assemble` - ⚠️ Unit tests with `./gradlew :features:wallet:impl:test` - ⚠️ ktlint compliance - ⚠️ Actual Android Keystore behavior (requires device/emulator) - ⚠️ Biometric prompt integration (requires Activity context) ### Security Notes 1. **Mnemonic never stored in plaintext** - Always encrypted with Keystore key 2. **Key material cleared after use** - `ByteArray.fill(0)` called where possible 3. **Per-session isolation** - Different Matrix accounts cannot access each other's wallets 4. **Biometric invalidation** - If user adds/removes fingerprints, wallet key becomes invalid 5. **No screenshots** - UI must apply FLAG_SECURE when showing seed phrase --- ## Task 3: Koios Client ✅ COMPLETE ### Completed - ✅ **CardanoClient.kt** interface in `api/` module: - `getBalance(address: String): Result` — balance in lovelace - `getUtxos(address: String): Result>` — unspent outputs - `submitTx(signedTxCbor: String): Result` — returns tx hash - `getTxStatus(txHash: String): Result` — PENDING/CONFIRMED/FAILED - ✅ **Data models** in `api/`: - `Utxo.kt` — txHash, outputIndex, amount, address - `TxStatus.kt` — enum PENDING/CONFIRMED/FAILED - `CardanoException.kt` — typed exceptions (NetworkException, RateLimitException, InvalidAddressException, TransactionNotFoundException, SubmissionFailedException, InsufficientFundsException, ApiException) - ✅ **KoiosCardanoClient.kt** implementation: - Uses `BackendFactory.getKoiosBackendService()` from cardano-client-lib - Testnet URL: `https://preprod.koios.rest/api/v1` (via CardanoNetworkConfig) - Mainnet URL: `https://api.koios.rest/api/v1` (via CardanoNetworkConfig) - 3 retries with exponential backoff (1s → 2s → 4s, max 10s) - Basic rate limiting (100ms min between requests for Koios 100 req/10s limit) - DI: `@ContributesBinding(SessionScope::class)` - Error parsing: 429 → RateLimitException, 5xx → NetworkException, etc. - ✅ **FakeCardanoClient.kt** for testing: - Configurable balances, UTxOs, transaction statuses - Error simulation (network errors, rate limits, submit failures) - Transaction lifecycle simulation (pending → confirmed → failed) - Call counters for test verification - Helper: `setupWallet(address, balance)` creates realistic UTxO set - ✅ **KoiosCardanoClientTest.kt** — 15+ unit tests: - getBalance success, unknown address, network error, rate limit - getUtxos success, empty result - submitTx success, failure - getTxStatus pending, confirmed, failed - reset/state management - ✅ **CardanoWalletManager updated** to use CardanoClient: - `refreshBalance()` now fetches real balance via Koios - Updates WalletState with lovelace + formatted ADA string ### Design Notes - **No API key required** — Koios public API is free - **Network config centralized** — Change `CardanoNetworkConfig.NETWORK` to swap testnet/mainnet - **Hex CBOR for submitTx** — Accepts hex-encoded signed transaction bytes - **UTxO pagination** — Limited to first 100 UTxOs (sufficient for typical wallets) ### Potential Issues - ⚠️ `getTxStatus` returns PENDING for unknown hashes (could be never-submitted or truly pending) - ⚠️ Koios rate limit (100 req/10s) may need adjustment for heavy usage patterns - ⚠️ No getProtocolParameters yet (needed for Task 4 fee calculation) --- ## Task 4-6: See PHASE1-PLAN.md --- ## Task 7: Timeline Payment Card ✅ COMPLETE ### Completed - ✅ **PaymentCardStatus.kt** — Enum for PENDING/CONFIRMED/FAILED states - ✅ **TimelineItemPaymentContent.kt** — Data class implementing TimelineItemEventContent - amountLovelace, addresses, txHash, status, network, isSentByMe - Computed properties: amountAda, isTestnet, truncatedTxHash, explorerUrl - Companion formatAda() helper - ✅ **TimelineItemPaymentView.kt** — Compose UI for payment card - Cardano icon (₳ symbol) - Amount in ADA (formatted from lovelace) - Status chip with spinner (pending), checkmark (confirmed), X (failed) - Testnet badge when applicable - Truncated tx hash (tappable → CardanoScan) - View on explorer link for confirmed transactions - @PreviewsDayNight with multiple preview states - ✅ **TimelineItemPaymentContentTest.kt** — Unit tests for content model - ✅ **Integration with TimelineItemEventContentView.kt** ### Design Notes - Payment cards use different colors for sent (primary) vs received (surface) - Explorer URLs: preprod.cardanoscan.io for testnet, cardanoscan.io for mainnet - Tx hash truncated to first 8 + last 8 chars for display --- ## Task 8: Raw Event Handling ✅ COMPLETE (UPGRADED) ### ✅ RESOLVED: SDK Raw Event API **Previous blocker:** Matrix Rust SDK did not expose raw event sending or raw JSON access. **Resolution:** The SDK (version 26.03.24) now provides: - `Timeline.sendRaw(eventType: String, content: String)` — Sends custom event types - `MsgLikeKind.Other` with `eventType` field — Receives custom events - `TimelineItemDebugInfo.originalJson` — Access to raw event JSON via debug info provider **Implementation updated to use proper raw events instead of text markers.** ### Completed - ✅ **PaymentEventSender.kt** — Interface for sending payment events - ✅ **DefaultPaymentEventSender.kt** — Implementation using raw events - Uses `timeline.sendRaw(eventType, content)` to send custom events - Event type: `co.sulkta.payment.request` (reverse-domain format) - Status updates: `co.sulkta.payment.status` - No text marker hack — proper Matrix custom events - ✅ **TimelineItemContentPaymentFactory.kt** — Parser for payment events - `isPaymentEventType(eventType)` — Checks for payment event type - `isStatusUpdateEventType(eventType)` — Checks for status update type - `createFromRaw(json, isSentByMe)` — Parses raw JSON from custom events - Supports both camelCase and snake_case field names - Graceful error handling — returns null on malformed JSON - ✅ **TimelineEventContentMapper.kt** — Maps `MsgLikeKind.Other` to `CustomEventContent` - ✅ **TimelineItemContentFactory.kt** — Handles `CustomEventContent` for payments - Gets raw JSON via `timelineItemDebugInfoProvider().originalJson` - Delegates to paymentFactory for payment event types - ✅ **CustomEventContent.kt** — New EventContent type for custom events - ✅ **Timeline.sendRaw()** — Added to Timeline interface and RustTimeline implementation - ✅ **FakePaymentEventSender.kt** — Test fake - ✅ **TimelineItemContentPaymentFactoryTest.kt** — Updated unit tests ### m.replace Status Updates **Decision:** Status updates are sent as separate events of type `co.sulkta.payment.status`. **Future improvement:** When SDK exposes event relations, refactor to use m.replace for cleaner status update thread. ### Benefits of Raw Event Approach - ✅ Proper Matrix protocol compliance (custom event types, not hacked text) - ✅ Non-wallet clients see "Unknown event" instead of JSON-in-text - ✅ Clean separation of payment events from regular messages - ✅ Events won't be indexed by message search - ✅ No message length limits concern --- ## Known Issues ### Issue 1: Biometric Prompt Activity Context The `CardanoKeyStorageImpl` uses `setUserAuthenticationRequired(true)` which will cause `UserNotAuthenticatedException` when accessing the key. The biometric prompt UI must be triggered from an Activity/Fragment context before calling `getMnemonic()`, `getSpendingKey()`, etc. **Solution:** Task 6 (Payment Flow UI) must call BiometricPrompt before invoking storage operations. ### Issue 2: KeyPermanentlyInvalidatedException If user changes biometric enrollment, the Keystore key is invalidated. Current behavior: throws exception, user must delete and recreate wallet. **Enhancement (future):** Show user-friendly message explaining why wallet became invalid and offer to re-import. --- *Last updated: 2026-03-27 - Task 2 complete*