- 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
221 lines
10 KiB
Markdown
221 lines
10 KiB
Markdown
# 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*
|