Add Element X Android technical review for Cardano wallet integration
This commit is contained in:
parent
10e73d484b
commit
046a254e17
1 changed files with 581 additions and 0 deletions
581
ELEMENT-X-ANDROID-REVIEW.md
Normal file
581
ELEMENT-X-ANDROID-REVIEW.md
Normal file
|
|
@ -0,0 +1,581 @@
|
||||||
|
# Element X Android — Cardano Wallet Integration Technical Review
|
||||||
|
|
||||||
|
**Date:** 2026-03-26
|
||||||
|
**Author:** Kayos (Technical Review Subagent)
|
||||||
|
**Target:** Integrating a built-in Cardano wallet with `/pay` slash command
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Element X Android is well-architected for extension. The codebase uses a clean feature module pattern with clear separation between API, implementation, and test modules. Key findings:
|
||||||
|
|
||||||
|
1. **Slash commands don't exist yet** — `/` is recognized but returns empty suggestions. This is a clean integration point.
|
||||||
|
2. **Android Keystore infrastructure exists** — `SecretKeyRepository` is already used for crypto operations; we can extend it.
|
||||||
|
3. **Timeline rendering is pluggable** — Adding a payment card bubble is straightforward via the factory pattern.
|
||||||
|
4. **Account data APIs are NOT exposed** — The Rust SDK has these but they're not surfaced to Kotlin yet. This is the main blocker.
|
||||||
|
5. **Widget infrastructure is solid** — Element Call uses WebView + postMessage; could inform a payment confirmation UI.
|
||||||
|
|
||||||
|
**Honest Effort Estimate:** 3-4 weeks for a competent Android developer familiar with Matrix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Slash Command System
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
|
||||||
|
The slash command infrastructure exists but is **intentionally empty**. Element X recognizes `/` as a command trigger but returns no suggestions.
|
||||||
|
|
||||||
|
**Key Files:**
|
||||||
|
|
||||||
|
```
|
||||||
|
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Suggestion.kt
|
||||||
|
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Suggestion.kt** defines the types:
|
||||||
|
```kotlin
|
||||||
|
sealed interface SuggestionType {
|
||||||
|
data object Mention : SuggestionType // @
|
||||||
|
data object Command : SuggestionType // /
|
||||||
|
data object Room : SuggestionType // #
|
||||||
|
data object Emoji : SuggestionType // :
|
||||||
|
data class Custom(val pattern: String) : SuggestionType
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SuggestionsProcessor.kt** handles suggestions but explicitly returns empty for commands:
|
||||||
|
```kotlin
|
||||||
|
SuggestionType.Command,
|
||||||
|
SuggestionType.Emoji,
|
||||||
|
is SuggestionType.Custom -> {
|
||||||
|
// Clear suggestions
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to Add `/pay`
|
||||||
|
|
||||||
|
1. **Create a new slash command data model:**
|
||||||
|
```kotlin
|
||||||
|
// features/wallet/api/src/main/kotlin/io/.../SlashCommand.kt
|
||||||
|
sealed interface SlashCommand {
|
||||||
|
data class Pay(val recipient: UserId?, val amount: String?) : SlashCommand
|
||||||
|
// Future: /balance, /receive, etc.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Extend SuggestionsProcessor to handle commands:**
|
||||||
|
```kotlin
|
||||||
|
SuggestionType.Command -> {
|
||||||
|
val commands = listOf(
|
||||||
|
ResolvedSuggestion.Command("/pay", "Send ADA to someone"),
|
||||||
|
// ... other commands
|
||||||
|
)
|
||||||
|
commands.filter { it.command.contains(suggestion.text, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add `ResolvedSuggestion.Command` type:**
|
||||||
|
```kotlin
|
||||||
|
// In libraries/textcomposer/impl/.../mentions/ResolvedSuggestion.kt
|
||||||
|
data class Command(val command: String, val description: String) : ResolvedSuggestion
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Intercept message sending in MessageComposerPresenter:**
|
||||||
|
```kotlin
|
||||||
|
// In sendMessage(), before sending to timeline:
|
||||||
|
val slashCommand = parseSlashCommand(message.markdown)
|
||||||
|
if (slashCommand != null) {
|
||||||
|
when (slashCommand) {
|
||||||
|
is SlashCommand.Pay -> {
|
||||||
|
// Navigate to payment flow instead of sending message
|
||||||
|
navigator.navigateToPayment(slashCommand.recipient, slashCommand.amount)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `SuggestionsProcessor.kt` | Add command filtering logic |
|
||||||
|
| `ResolvedSuggestion.kt` | Add `Command` type |
|
||||||
|
| `MessageComposerPresenter.kt` | Intercept `/pay` before send |
|
||||||
|
| New: `SlashCommandParser.kt` | Parse `/pay @user 100` syntax |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Key Storage (Android Keystore)
|
||||||
|
|
||||||
|
### Current Infrastructure
|
||||||
|
|
||||||
|
Element X already uses Android Keystore for AES encryption. The infrastructure is **solid and reusable**.
|
||||||
|
|
||||||
|
**Key Files:**
|
||||||
|
```
|
||||||
|
libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt
|
||||||
|
libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt
|
||||||
|
libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFile.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
**SecretKeyRepository interface:**
|
||||||
|
```kotlin
|
||||||
|
interface SecretKeyRepository {
|
||||||
|
fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey
|
||||||
|
fun deleteKey(alias: String)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**KeyStoreSecretKeyRepository implementation:**
|
||||||
|
```kotlin
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class KeyStoreSecretKeyRepository(
|
||||||
|
private val keyStore: KeyStore,
|
||||||
|
) : SecretKeyRepository {
|
||||||
|
override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey {
|
||||||
|
// Uses Android Keystore with AES-GCM-128
|
||||||
|
val generator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM, ANDROID_KEYSTORE)
|
||||||
|
val keyGenSpec = KeyGenParameterSpec.Builder(alias, ...)
|
||||||
|
.setUserAuthenticationRequired(requiresUserAuthentication)
|
||||||
|
.build()
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### What We Need to Add
|
||||||
|
|
||||||
|
1. **Cardano-specific key storage module:**
|
||||||
|
```kotlin
|
||||||
|
// libraries/wallet/impl/src/.../CardanoKeyStorage.kt
|
||||||
|
@Inject
|
||||||
|
class CardanoKeyStorage(
|
||||||
|
private val secretKeyRepository: SecretKeyRepository,
|
||||||
|
private val encryptionService: EncryptionDecryptionService,
|
||||||
|
private val sessionStore: SessionStore,
|
||||||
|
) {
|
||||||
|
private val KEY_ALIAS_PREFIX = "cardano_wallet_"
|
||||||
|
|
||||||
|
fun storeSpendingKey(sessionId: SessionId, encryptedKey: ByteArray): Result<Unit>
|
||||||
|
fun getSpendingKey(sessionId: SessionId): Result<ByteArray>
|
||||||
|
fun hasWallet(sessionId: SessionId): Boolean
|
||||||
|
fun deleteWallet(sessionId: SessionId): Result<Unit>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Biometric/PIN protection option:**
|
||||||
|
```kotlin
|
||||||
|
secretKeyRepository.getOrCreateKey(
|
||||||
|
alias = "${KEY_ALIAS_PREFIX}${sessionId.value}",
|
||||||
|
requiresUserAuthentication = true // Requires biometric/PIN
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Leverage EncryptedFile pattern for key backup:**
|
||||||
|
```kotlin
|
||||||
|
val encryptedFile = EncryptedFile(context, walletKeyFile)
|
||||||
|
encryptedFile.openFileOutput().use { it.write(encryptedSpendingKey) }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
- Keys should be **per-session** (each Matrix account has its own wallet)
|
||||||
|
- Consider `setUserAuthenticationRequired(true)` for spending keys
|
||||||
|
- Use existing `RandomSecretPassphraseProvider` pattern for derived keys
|
||||||
|
- **DO NOT** store raw spending keys — always encrypt with Keystore-backed key
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Widget Infrastructure (Element Call Pattern)
|
||||||
|
|
||||||
|
### How Element Call Works
|
||||||
|
|
||||||
|
Element Call is embedded as a WebView-based widget with bidirectional message passing.
|
||||||
|
|
||||||
|
**Key Files:**
|
||||||
|
```
|
||||||
|
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
|
||||||
|
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
|
||||||
|
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ CallScreenView │
|
||||||
|
│ ┌───────────────────────────────────┐ │
|
||||||
|
│ │ WebView │ │
|
||||||
|
│ │ (Element Call HTML/JS) │ │
|
||||||
|
│ └───────────────────────────────────┘ │
|
||||||
|
│ ▲ │ │
|
||||||
|
│ │ postMessage │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌───────────────────────────────────┐ │
|
||||||
|
│ │ WebViewWidgetMessageInterceptor │ │
|
||||||
|
│ └───────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────────────┐ │
|
||||||
|
│ │ MatrixWidgetDriver │ │
|
||||||
|
│ │ (SDK ↔ Widget communication) │ │
|
||||||
|
│ └───────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**WidgetMessage structure:**
|
||||||
|
```kotlin
|
||||||
|
@Serializable
|
||||||
|
data class WidgetMessage(
|
||||||
|
@SerialName("api") val direction: Direction,
|
||||||
|
@SerialName("widgetId") val widgetId: String,
|
||||||
|
@SerialName("requestId") val requestId: String,
|
||||||
|
@SerialName("action") val action: Action,
|
||||||
|
@SerialName("data") val data: JsonElement? = null,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relevance for Wallet
|
||||||
|
|
||||||
|
We could use this pattern for:
|
||||||
|
1. **Payment confirmation UI** — A WebView showing tx details before signing
|
||||||
|
2. **dApp browser** — Future widget-based dApp integration
|
||||||
|
3. **Cross-device signing** — If we need to coordinate with desktop wallets
|
||||||
|
|
||||||
|
For MVP `/pay`, we probably **don't need widgets** — a native Compose UI is simpler.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Message Rendering (Payment Card Bubble)
|
||||||
|
|
||||||
|
### Current Architecture
|
||||||
|
|
||||||
|
Timeline items are rendered through a factory + sealed class pattern.
|
||||||
|
|
||||||
|
**Key Files:**
|
||||||
|
```
|
||||||
|
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
|
||||||
|
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt
|
||||||
|
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
**TimelineItemEventContent** is a sealed interface with multiple implementations:
|
||||||
|
```kotlin
|
||||||
|
sealed interface TimelineItemEventContent {
|
||||||
|
val type: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing types:
|
||||||
|
data class TimelineItemTextBasedContent(...) : TimelineItemEventContent
|
||||||
|
data class TimelineItemImageContent(...) : TimelineItemEventContent
|
||||||
|
data class TimelineItemPollContent(...) : TimelineItemEventContent
|
||||||
|
data class TimelineItemLocationContent(...) : TimelineItemEventContent
|
||||||
|
// ... etc
|
||||||
|
```
|
||||||
|
|
||||||
|
**TimelineItemContentFactory** maps SDK `EventContent` → UI content:
|
||||||
|
```kotlin
|
||||||
|
suspend fun create(itemContent: EventContent, ...): TimelineItemEventContent {
|
||||||
|
return when (itemContent) {
|
||||||
|
is MessageContent -> messageFactory.create(...)
|
||||||
|
is PollContent -> pollFactory.create(...)
|
||||||
|
is UnknownContent -> TimelineItemUnknownContent
|
||||||
|
// ... etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**TimelineItemEventContentView** dispatches rendering:
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemEventContentView(content: TimelineItemEventContent, ...) {
|
||||||
|
when (content) {
|
||||||
|
is TimelineItemTextBasedContent -> TimelineItemTextView(...)
|
||||||
|
is TimelineItemPollContent -> TimelineItemPollView(...)
|
||||||
|
// ... etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Payment Card
|
||||||
|
|
||||||
|
1. **Create content model:**
|
||||||
|
```kotlin
|
||||||
|
// features/wallet/impl/src/.../timeline/TimelineItemPaymentContent.kt
|
||||||
|
data class TimelineItemPaymentContent(
|
||||||
|
val transactionId: String?,
|
||||||
|
val senderAddress: String,
|
||||||
|
val recipientAddress: String,
|
||||||
|
val amount: String, // e.g., "100 ADA"
|
||||||
|
val status: PaymentStatus, // Pending, Confirmed, Failed
|
||||||
|
val timestamp: Long,
|
||||||
|
val txHash: String?, // Once confirmed
|
||||||
|
) : TimelineItemEventContent {
|
||||||
|
override val type: String = "m.payment" // Custom event type
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PaymentStatus { PENDING, CONFIRMED, FAILED }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create factory:**
|
||||||
|
```kotlin
|
||||||
|
// features/wallet/impl/src/.../timeline/TimelineItemContentPaymentFactory.kt
|
||||||
|
@Inject
|
||||||
|
class TimelineItemContentPaymentFactory {
|
||||||
|
fun create(content: PaymentEventContent): TimelineItemPaymentContent {
|
||||||
|
return TimelineItemPaymentContent(
|
||||||
|
transactionId = content.transactionId,
|
||||||
|
// ... map fields
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create view:**
|
||||||
|
```kotlin
|
||||||
|
// features/wallet/impl/src/.../timeline/TimelineItemPaymentView.kt
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemPaymentView(
|
||||||
|
content: TimelineItemPaymentContent,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(modifier = modifier) {
|
||||||
|
Column {
|
||||||
|
// Icon + "Payment" header
|
||||||
|
Row {
|
||||||
|
Icon(Icons.Default.Payment)
|
||||||
|
Text("Payment")
|
||||||
|
}
|
||||||
|
// Amount
|
||||||
|
Text(
|
||||||
|
text = content.amount,
|
||||||
|
style = MaterialTheme.typography.headlineMedium
|
||||||
|
)
|
||||||
|
// Status indicator
|
||||||
|
StatusChip(content.status)
|
||||||
|
// View on explorer link (if confirmed)
|
||||||
|
if (content.txHash != null) {
|
||||||
|
TextButton(onClick = { openExplorer(content.txHash) }) {
|
||||||
|
Text("View on CardanoScan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Register in TimelineItemContentFactory:**
|
||||||
|
```kotlin
|
||||||
|
// Add to existing factory
|
||||||
|
is PaymentContent -> paymentFactory.create(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Register in TimelineItemEventContentView:**
|
||||||
|
```kotlin
|
||||||
|
// Add to existing when block
|
||||||
|
is TimelineItemPaymentContent -> TimelineItemPaymentView(content = content, modifier = modifier)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Event Type
|
||||||
|
|
||||||
|
We need a custom Matrix event type for payments. Options:
|
||||||
|
|
||||||
|
1. **`m.payment.cardano`** — Custom event type (requires SDK extension)
|
||||||
|
2. **`m.room.message` with `msgtype: "m.payment"`** — Easier but hacky
|
||||||
|
3. **Room state event** — For persistent payment records
|
||||||
|
|
||||||
|
The SDK currently handles `UnknownContent` for unrecognized events, so we'd need to:
|
||||||
|
- Either extend the Rust SDK to recognize `m.payment.*`
|
||||||
|
- Or use the raw event JSON and parse it ourselves
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Account Data APIs
|
||||||
|
|
||||||
|
### Current State: **BLOCKER**
|
||||||
|
|
||||||
|
Account data (`m.account_data`) is the natural place to store:
|
||||||
|
- Encrypted wallet seed (per-account)
|
||||||
|
- Wallet addresses (for backup/sync)
|
||||||
|
- Payment preferences
|
||||||
|
|
||||||
|
**The Matrix Rust SDK has account data APIs, but Element X Android does NOT expose them.**
|
||||||
|
|
||||||
|
The `MatrixClient` interface has no:
|
||||||
|
- `getAccountData(type: String)`
|
||||||
|
- `setAccountData(type: String, content: JsonObject)`
|
||||||
|
- Room-level account data access
|
||||||
|
|
||||||
|
### Workarounds
|
||||||
|
|
||||||
|
1. **Local-only storage (MVP approach):**
|
||||||
|
- Store wallet keys in Android Keystore
|
||||||
|
- Store addresses in DataStore/preferences
|
||||||
|
- **Con:** No cross-device sync
|
||||||
|
|
||||||
|
2. **Use a custom room as wallet state:**
|
||||||
|
- Create a self-DM or special room
|
||||||
|
- Store state as room events
|
||||||
|
- **Con:** Awkward, discoverable
|
||||||
|
|
||||||
|
3. **Extend the SDK (proper solution):**
|
||||||
|
- Add account data bindings to `RustMatrixClient`
|
||||||
|
- Expose `setAccountData` / `getAccountData`
|
||||||
|
- **Time:** 2-3 days for SDK extension
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
For MVP: **Local-only storage.** The user's wallet exists only on that device.
|
||||||
|
|
||||||
|
For V2: Extend SDK to use account data for encrypted seed backup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Recommended File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
features/
|
||||||
|
└── wallet/
|
||||||
|
├── api/
|
||||||
|
│ └── src/main/kotlin/io/element/android/features/wallet/api/
|
||||||
|
│ ├── WalletEntryPoint.kt # Feature entry point
|
||||||
|
│ ├── WalletState.kt # Wallet balance/state
|
||||||
|
│ ├── PaymentRequest.kt # /pay parsing
|
||||||
|
│ └── slash/
|
||||||
|
│ └── SlashCommand.kt # Slash command models
|
||||||
|
│
|
||||||
|
├── impl/
|
||||||
|
│ └── src/main/kotlin/io/element/android/features/wallet/impl/
|
||||||
|
│ ├── di/
|
||||||
|
│ │ └── WalletModule.kt # DI bindings
|
||||||
|
│ ├── storage/
|
||||||
|
│ │ ├── CardanoKeyStorage.kt # Keystore wrapper
|
||||||
|
│ │ └── WalletPreferences.kt # DataStore prefs
|
||||||
|
│ ├── cardano/
|
||||||
|
│ │ ├── CardanoWalletManager.kt # Wallet operations
|
||||||
|
│ │ ├── TransactionBuilder.kt # Tx construction
|
||||||
|
│ │ └── BlockfrostClient.kt # Chain queries
|
||||||
|
│ ├── payment/
|
||||||
|
│ │ ├── PaymentFlowNode.kt # Navigation node
|
||||||
|
│ │ ├── PaymentPresenter.kt # MVP logic
|
||||||
|
│ │ ├── PaymentState.kt # UI state
|
||||||
|
│ │ └── PaymentView.kt # Compose UI
|
||||||
|
│ ├── timeline/
|
||||||
|
│ │ ├── TimelineItemPaymentContent.kt
|
||||||
|
│ │ ├── TimelineItemContentPaymentFactory.kt
|
||||||
|
│ │ └── TimelineItemPaymentView.kt
|
||||||
|
│ └── slash/
|
||||||
|
│ ├── SlashCommandParser.kt
|
||||||
|
│ └── SlashCommandHandler.kt
|
||||||
|
│
|
||||||
|
└── test/
|
||||||
|
└── src/main/kotlin/io/element/android/features/wallet/test/
|
||||||
|
├── FakeCardanoWalletManager.kt
|
||||||
|
└── FakeBlockfrostClient.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Blockers & Gotchas
|
||||||
|
|
||||||
|
### Hard Blockers
|
||||||
|
|
||||||
|
1. **Account Data API not exposed**
|
||||||
|
- Impact: No cross-device wallet sync
|
||||||
|
- Mitigation: Local storage for MVP; SDK extension for V2
|
||||||
|
- Effort: 2-3 days to extend SDK
|
||||||
|
|
||||||
|
2. **Custom event types not recognized**
|
||||||
|
- Impact: Payment events show as "Unknown event"
|
||||||
|
- Mitigation: Extend SDK's event parsing or use raw JSON
|
||||||
|
- Effort: 1-2 days
|
||||||
|
|
||||||
|
### Soft Blockers
|
||||||
|
|
||||||
|
3. **No existing Cardano library for Android/Kotlin**
|
||||||
|
- We need: Key derivation (BIP39/CIP1852), transaction building
|
||||||
|
- Options: cardano-serialization-lib (Rust via JNI), or port to Kotlin
|
||||||
|
- Effort: 3-5 days for JNI wrapper OR 1-2 weeks for native Kotlin
|
||||||
|
|
||||||
|
4. **Blockfrost dependency**
|
||||||
|
- Need API key management
|
||||||
|
- Rate limiting concerns
|
||||||
|
- Effort: 1 day
|
||||||
|
|
||||||
|
### Things That Are Easier Than Expected
|
||||||
|
|
||||||
|
- **Keystore integration** — Existing infrastructure is excellent
|
||||||
|
- **Timeline rendering** — Factory pattern makes it trivial
|
||||||
|
- **Feature module structure** — Well-documented pattern
|
||||||
|
- **Slash command hook** — Clean insertion point exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Effort Estimate
|
||||||
|
|
||||||
|
### MVP: `/pay` Command End-to-End
|
||||||
|
|
||||||
|
| Task | Days | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| Slash command parser + suggestions | 2 | Extend existing infrastructure |
|
||||||
|
| Payment flow UI (recipient, amount, confirm) | 3 | Compose screens |
|
||||||
|
| Cardano key storage (Keystore) | 2 | Extend SecretKeyRepository |
|
||||||
|
| Cardano transaction builder | 5 | Depends on Rust lib or Kotlin port |
|
||||||
|
| Blockfrost integration | 1 | Submit tx, query balance |
|
||||||
|
| Payment card timeline item | 2 | New content type + view |
|
||||||
|
| Testing & polish | 3 | Unit tests, UI tests, edge cases |
|
||||||
|
| **Total MVP** | **18 days** | ~3.5 weeks |
|
||||||
|
|
||||||
|
### V2: Cross-Device Sync
|
||||||
|
|
||||||
|
| Task | Days | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| Extend SDK for account data | 3 | Rust bindings + Kotlin wrapper |
|
||||||
|
| Encrypted seed backup | 2 | Store in m.account_data |
|
||||||
|
| Receive flow + QR | 2 | Address display, sharing |
|
||||||
|
| **Total V2** | **7 days** | ~1.5 weeks |
|
||||||
|
|
||||||
|
### Full Feature Set
|
||||||
|
|
||||||
|
| Task | Days | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| MVP | 18 | As above |
|
||||||
|
| V2 | 7 | Cross-device |
|
||||||
|
| Balance widget | 2 | Room-level balance display |
|
||||||
|
| Transaction history | 3 | Query + display |
|
||||||
|
| Multi-asset support | 3 | Native tokens |
|
||||||
|
| **Total Full** | **33 days** | ~6.5 weeks |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Recommendations
|
||||||
|
|
||||||
|
1. **Start with local-only MVP** — Don't block on SDK extensions
|
||||||
|
2. **Use cardano-serialization-lib via JNI** — Don't reinvent wheel
|
||||||
|
3. **Store wallet per-session** — Each Matrix account has its own wallet
|
||||||
|
4. **Use Blockfrost for MVP** — Move to own node later if needed
|
||||||
|
5. **Custom `m.payment.cardano` event** — Proper event type, extend SDK
|
||||||
|
6. **Fork Element X for initial dev** — Easier than patches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Files Quick Reference
|
||||||
|
|
||||||
|
| Purpose | Location |
|
||||||
|
|---------|----------|
|
||||||
|
| Slash command suggestions | `features/messages/impl/.../suggestions/SuggestionsProcessor.kt` |
|
||||||
|
| Message composer | `features/messages/impl/.../messagecomposer/MessageComposerPresenter.kt` |
|
||||||
|
| Keystore | `libraries/cryptography/impl/.../KeyStoreSecretKeyRepository.kt` |
|
||||||
|
| Timeline factory | `features/messages/impl/.../factories/event/TimelineItemContentFactory.kt` |
|
||||||
|
| Timeline content view | `features/messages/impl/.../components/event/TimelineItemEventContentView.kt` |
|
||||||
|
| Content models | `features/messages/impl/.../model/event/TimelineItem*.kt` |
|
||||||
|
| Widget system | `features/call/impl/.../utils/DefaultCallWidgetProvider.kt` |
|
||||||
|
| Encrypted DB | `libraries/encrypted-db/src/.../crypto/EncryptedFile.kt` |
|
||||||
|
| Session storage | `libraries/session-storage/impl/.../DatabaseSessionStore.kt` |
|
||||||
|
| Matrix client | `libraries/matrix/impl/.../RustMatrixClient.kt` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*End of Technical Review*
|
||||||
Loading…
Add table
Add a link
Reference in a new issue