element-x-ada/BLOCKERS.md
Kayos adee67cf0d feat(wallet): payment card timeline item and raw event handling (Tasks 7+8)
Task 7: Timeline Payment Card
- TimelineItemPaymentView integration with TimelineItemEventContentView
- Payment card rendering for both sender and recipient perspectives
- Unit tests for TimelineItemPaymentContent

Task 8: Raw Event Handling
- Modified TimelineItemContentMessageFactory to intercept payment events
- Added isSentByMe parameter propagation through content factories
- FakePaymentEventSender for testing
- Unit tests for TimelineItemContentPaymentFactory

SDK Limitation Workaround:
Since matrix-rust-sdk doesn't expose raw event sending or UnknownContent
raw JSON, payment events are encoded as text messages with a marker:
[cardano-payment:v1]{...json...}

This falls back gracefully for non-wallet clients while enabling
rich payment card rendering for wallet-enabled clients.
2026-03-27 11:08:03 -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

Completed

  • PaymentEventSender.kt — Interface for sending payment events
  • DefaultPaymentEventSender.kt — Implementation
    • Sends payment as formatted text message with JSON payload
    • Format: [cardano-payment:v1]{...json...}\n💰 Sent X ADA
    • HTML body includes data-payment attribute for future parsing
    • Status updates use separate marker: [cardano-payment-status:v1]
  • TimelineItemContentPaymentFactory.kt — Parser for payment messages
    • isPaymentEvent(body) — Detects payment marker
    • isPaymentStatusUpdate(body) — Detects status update marker
    • createFromBody(body, isSentByMe) — Parses text message body
    • createFromRaw(json, isSentByMe) — Parses raw JSON (for future SDK extension)
    • Graceful error handling — returns null on malformed JSON
  • TimelineItemContentMessageFactory.kt — Modified to intercept payments
    • Added paymentFactory dependency
    • Added isSentByMe parameter to create()
    • TextMessageType checks for payment marker before creating text content
  • TimelineItemContentFactory.kt — Passes isSentByMe to message factory
  • FakePaymentEventSender.kt — Test fake
  • TimelineItemContentPaymentFactoryTest.kt — Unit tests

SDK Limitations & Approach

The Matrix Rust SDK does NOT expose:

  • Raw event sending (room.sendRawEvent())
  • Raw JSON access for UnknownContent

Workaround implemented: Instead of custom event types, we encode payment data in standard text messages:

[cardano-payment:v1]{"amount_lovelace":10000000,"to_address":"...","from_address":"...","tx_hash":"...","status":"pending","network":"testnet"}
💰 Sent 10 ADA

This approach:

  • Works with existing SDK (no fork needed)
  • Falls back gracefully (non-wallet clients see "💰 Sent 10 ADA")
  • Can be upgraded to proper custom events when SDK exposes raw event APIs

m.replace Status Updates

Decision: Due to SDK limitations (no direct access to m.replace relations), status updates are sent as new messages rather than event replacements.

Future improvement: When SDK exposes event relations, refactor to use m.replace for cleaner status update thread.

Potential Issues

  • ⚠️ Status updates create new timeline events (not ideal, but works)
  • ⚠️ Payment messages may be indexed by search (contains JSON)
  • ⚠️ Very long addresses in JSON may hit message length limits (unlikely in practice)

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