- 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
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
NETWORKtoCardanoNetwork.MAINNETfor 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 operationsetUserAuthenticationValidityDurationSeconds(-1)- no grace periodsetInvalidatedByBiometricEnrollment(true)- invalidate on biometric change- Methods:
generateWallet,importWallet,getMnemonic,getBaseAddress,getStakeAddress,deleteWallet
- Per-session wallet isolation (key alias:
-
✅ CardanoWalletManager (interface + implementation)
- Key derivation using CIP-1852 via cardano-client-lib's Account class
- Path
m/1852'/1815'/0'/0/0for external receiving address - Path
m/1852'/1815'/0'/2/0for 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_SECUREwhen 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
- Mnemonic never stored in plaintext - Always encrypted with Keystore key
- Key material cleared after use -
ByteArray.fill(0)called where possible - Per-session isolation - Different Matrix accounts cannot access each other's wallets
- Biometric invalidation - If user adds/removes fingerprints, wallet key becomes invalid
- 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 lovelacegetUtxos(address: String): Result<List<Utxo>>— unspent outputssubmitTx(signedTxCbor: String): Result<String>— returns tx hashgetTxStatus(txHash: String): Result<TxStatus>— PENDING/CONFIRMED/FAILED
-
✅ Data models in
api/:Utxo.kt— txHash, outputIndex, amount, addressTxStatus.kt— enum PENDING/CONFIRMED/FAILEDCardanoException.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.
- Uses
-
✅ 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.NETWORKto 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
- ⚠️
getTxStatusreturns 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 typesMsgLikeKind.OtherwitheventTypefield — Receives custom eventsTimelineItemDebugInfo.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
- Uses
- ✅ TimelineItemContentPaymentFactory.kt — Parser for payment events
isPaymentEventType(eventType)— Checks for payment event typeisStatusUpdateEventType(eventType)— Checks for status update typecreateFromRaw(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.OthertoCustomEventContent - ✅ TimelineItemContentFactory.kt — Handles
CustomEventContentfor payments- Gets raw JSON via
timelineItemDebugInfoProvider().originalJson - Delegates to paymentFactory for payment event types
- Gets raw JSON via
- ✅ 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