element-x-ada/BLOCKERS.md
Kayos f2b95d6b8a fix(wallet): replace text-marker hack with proper raw event API (room.sendRaw + MsgLikeKind.Other)
- Add Timeline.sendRaw() to send custom Matrix events
- Add CustomEventContent type for receiving custom events
- Update TimelineEventContentMapper to handle MsgLikeKind.Other
- Update TimelineItemContentFactory to intercept payment events
- Rewrite DefaultPaymentEventSender to use sendRaw instead of text markers
- Update TimelineItemContentPaymentFactory to parse raw JSON
- Remove text-marker detection from TimelineItemContentMessageFactory
- Update tests to use raw event API
- Mark raw event SDK blocker as RESOLVED in BLOCKERS.md

Event type: co.sulkta.payment.request (reverse-domain format)
Status updates: co.sulkta.payment.status

Benefits:
- Proper Matrix protocol compliance
- No JSON embedded in text messages
- Events won't be indexed by search
- Clean separation from regular messages
2026-03-27 11:45:12 -07:00

10 KiB

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<Long> — balance in lovelace
    • getUtxos(address: String): Result<List<Utxo>> — unspent outputs
    • submitTx(signedTxCbor: String): Result<String> — returns tx hash
    • getTxStatus(txHash: String): Result<TxStatus> — 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