# BitChat + Solana Android MVP Implementation Plan
**Version**: 1.0
**Date**: February 2026
**Platform**: Android (Kotlin + Jetpack Compose)
**Approach**: Offline Transaction Creation + Delayed Broadcasting (Approach 1)
**Base Repository**: https://github.com/permissionlesstech/bitchat-android
---
## Executive Summary
This document outlines a detailed implementation plan for integrating Solana blockchain transfers into the BitChat Android application. The MVP will enable users to:
1. Create and sign Solana transactions offline
2. Transfer signed transactions via Bluetooth mesh
3. Queue transactions for broadcasting when internet is available
4. Confirm on-chain settlement and notify users
**Core Value Proposition**: The only Android solution enabling offline Solana transaction creation with secure peer-to-peer transfer via Bluetooth mesh networking.
---
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Technology Stack](#technology-stack)
3. [Core Components](#core-components)
4. [Implementation Phases](#implementation-phases)
5. [File Structure](#file-structure)
6. [Detailed Component Specifications](#detailed-component-specifications)
7. [Data Flow & Protocols](#data-flow--protocols)
8. [Security Implementation](#security-implementation)
9. [Testing Strategy](#testing-strategy)
10. [Deployment Plan](#deployment-plan)
11. [Success Metrics](#success-metrics)
12. [Risk Mitigation](#risk-mitigation)
---
## Architecture Overview
### System Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ UI Layer (Jetpack Compose) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Wallet │ │ Transaction │ │ Message │ │
│ │ Screen │ │ Screen │ │ Screen │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ ViewModel Layer (MVVM Pattern) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ChatViewModel (Extended) │ │
│ │ - StateFlow for reactive UI updates │ │
│ │ - Wallet state management │ │
│ │ - Transaction lifecycle coordination │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Solana │ │ Transaction │ │ Broadcast │ │
│ │ Wallet │ │ Protocol │ │ Service │ │
│ │ Service │ │ Service │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Existing BitChat Infrastructure │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Bluetooth │ │ Encryption │ │ Binary │ │
│ │ Mesh │ │ Service │ │ Protocol │ │
│ │ Service │ │ (BouncyCastle)│ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ External Systems │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Solana │ │ Encrypted │ │ Bluetooth │ │
│ │ RPC │ │ Shared │ │ LE │ │
│ │ │ │ Preferences │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Key Architectural Principles
1. **MVVM Pattern**: Clear separation between UI (Compose), ViewModel (state), and Services (business logic)
2. **Kotlin Coroutines**: Async operations using coroutines and Flow
3. **Dependency Injection**: Hilt for service management
4. **Protocol Extension**: Extend BitChat's binary protocol without breaking existing functionality
5. **Security First**: EncryptedSharedPreferences + Android Keystore for secure key storage
6. **Modern Solana Patterns**: Use Solana Kotlin SDK with modern patterns
---
## Technology Stack
### Android Requirements
- **Minimum SDK**: API 26 (Android 8.0 Oreo) - Same as BitChat
- **Target SDK**: API 34 (Android 14)
- **Compile SDK**: API 34
- **Kotlin**: 1.9.0+
- **Gradle**: 8.0+
- **Java Version**: 17
---
### Core Dependencies
```kotlin
// app/build.gradle.kts
dependencies {
// ===== EXISTING BITCHAT DEPENDENCIES =====
// Jetpack Compose
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.0")
implementation("androidx.activity:activity-compose:1.8.2")
// Lifecycle & ViewModel
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// Bluetooth
implementation("no.nordicsemi.android:ble:2.7.0")
implementation("no.nordicsemi.android:ble-ktx:2.7.0")
// Cryptography
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
// Encrypted Storage
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// DataStore
implementation("androidx.datastore:datastore-preferences:1.0.0")
// ===== NEW SOLANA DEPENDENCIES =====
// Solana Kotlin SDK (choose one based on availability)
// Option 1: Solana Mobile Kotlin (if available)
implementation("com.solanamobile:solana-kotlin:0.2.5")
// Option 2: Solana4k (community library)
implementation("com.github.metaplex-foundation:solana4k:1.0.0")
// Option 3: Web3j for Solana (if needed)
implementation("org.web3j:core:5.0.0")
// For this plan, we'll use Solana Mobile's Kotlin SDK
// Base58 encoding
implementation("com.github.komputing:kbase58:0.1")
// Ed25519 signing (if not in Solana SDK)
implementation("net.i2p.crypto:eddsa:0.3.0")
// BIP39 Mnemonic
implementation("cash.z.ecc.android:kotlin-bip39:1.0.7")
// QR Code generation
implementation("com.google.zxing:core:3.5.2")
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
// JSON serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Network monitoring
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// Hilt for Dependency Injection
implementation("com.google.dagger:hilt-android:2.50")
kapt("com.google.dagger:hilt-compiler:2.50")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.0")
}
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization")
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
```
---
## Core Components
### 1. SolanaWalletService
**Responsibility**: Manage Solana keypairs, balance tracking, and wallet state
**Key Functions:**
- Generate new Solana keypair
- Import/export seed phrase (BIP39)
- Securely store private key in Android Keystore
- Fetch SOL balance from RPC
- Derive wallet address (base58 encoded public key)
---
### 2. TransactionProtocolService
**Responsibility**: Extend BitChat's binary protocol for Solana transactions
**New Message Types:**
- `0x30`: Solana Transaction Packet
- `0x31`: Transaction Delivery Acknowledgment
- `0x32`: Transaction Broadcast Confirmation
- `0x33`: Transaction Status Update
**Packet Structure:**
```kotlin
data class SolanaTransactionPacket(
val version: Byte = 0x01,
val transactionType: TransactionType,
val serializedTransaction: ByteArray,
val senderAddress: String,
val recipientAddress: String,
val amount: Long, // lamports
val timestamp: Int,
val flags: TransactionFlags,
val signature: ByteArray // Ed25519 64 bytes
)
enum class TransactionType(val value: Byte) {
SOL_TRANSFER(0x00),
TOKEN_TRANSFER(0x01),
NFT_TRANSFER(0x02)
}
@JvmInline
value class TransactionFlags(val value: Byte) {
companion object {
const val REQUIRES_BLOCKHASH_UPDATE: Byte = 0b00000001
const val PRIORITY_FEE_ENABLED: Byte = 0b00000010
}
}
```
---
### 3. BroadcastService
**Responsibility**: Queue and broadcast transactions to Solana network
**Key Functions:**
- Queue transactions for broadcasting
- Monitor internet connectivity
- Fetch recent blockhash when broadcasting
- Submit transaction to RPC endpoint
- Poll for confirmation
- Retry failed transactions with exponential backoff
- Emit status updates via Bluetooth
**Queue Persistence:**
- Store queued transactions in Room database
- Survive app restarts
- TTL: 24 hours default
---
### 4. Transaction UI Components (Jetpack Compose)
**Screens:**
- `WalletScreen` - Balance, address, transaction history
- `SendTransactionScreen` - Amount input, recipient selection
- `ReceiveScreen` - QR code, address display
- `TransactionHistoryScreen` - List of transactions with status
---
## Implementation Phases
### Phase 0: Setup & Foundation (Week 1)
**Goals:**
- Set up development environment
- Install Solana dependencies
- Create feature branch
- Design database schema extensions
**Deliverables:**
1. Feature branch: `feature/solana-integration`
2. Updated `build.gradle.kts` with Solana dependencies
3. Room database entities for wallet and transactions
4. Development RPC endpoint configuration (Devnet)
**Tasks:**
#### Task 0.1: Update Dependencies
```kotlin
// app/build.gradle.kts
android {
namespace = "com.bitchat.android"
compileSdk = 34
defaultConfig {
applicationId = "com.bitchat.android"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0.0-solana-beta"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
```
#### Task 0.2: Create Database Schema
```kotlin
// data/local/SolanaDatabase.kt
package com.bitchat.android.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(
entities = [
WalletEntity::class,
QueuedTransactionEntity::class
],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class SolanaDatabase : RoomDatabase() {
abstract fun walletDao(): WalletDao
abstract fun transactionDao(): TransactionDao
}
// data/local/entities/WalletEntity.kt
@Entity(tableName = "wallets")
data class WalletEntity(
@PrimaryKey val publicKey: String,
val createdAt: Long,
val lastBalanceUpdate: Long,
val balanceLamports: Long
)
// data/local/entities/QueuedTransactionEntity.kt
@Entity(tableName = "queued_transactions")
data class QueuedTransactionEntity(
@PrimaryKey val id: String,
val serializedTransaction: ByteArray,
val senderAddress: String,
val recipientAddress: String,
val amount: Long,
val timestamp: Long,
val status: String,
val requiresBlockhashUpdate: Boolean,
val retryCount: Int = 0,
val signature: String? = null
)
```
#### Task 0.3: Set Up Hilt Dependency Injection
```kotlin
// BitchatApplication.kt
package com.bitchat.android
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class BitchatApplication : Application() {
override fun onCreate() {
super.onCreate()
// Existing initialization
}
}
// di/SolanaModule.kt
package com.bitchat.android.di
import android.content.Context
import androidx.room.Room
import com.bitchat.android.data.local.SolanaDatabase
import com.bitchat.android.solana.SolanaWalletService
import com.bitchat.android.solana.SolanaRpcService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SolanaModule {
@Provides
@Singleton
fun provideSolanaDatabase(@ApplicationContext context: Context): SolanaDatabase {
return Room.databaseBuilder(
context,
SolanaDatabase::class.java,
"solana_database"
).build()
}
@Provides
@Singleton
fun provideSolanaRpcService(): SolanaRpcService {
return SolanaRpcService(cluster = Cluster.DEVNET)
}
@Provides
@Singleton
fun provideSolanaWalletService(
@ApplicationContext context: Context,
rpcService: SolanaRpcService
): SolanaWalletService {
return SolanaWalletService(context, rpcService)
}
}
```
**Phase 0 Deliverables:**
- [ ] Updated `build.gradle.kts` with dependencies
- [ ] Room database schema created
- [ ] Hilt modules configured
- [ ] Development environment verified
- [ ] Devnet RPC endpoint tested
---
### Phase 1: Wallet Foundation (Week 2)
**Goals:**
- Implement basic wallet generation and storage
- Display wallet address and balance
- Test secure key storage
**Components to Build:**
#### 1.1 SolanaWalletService.kt
```kotlin
// solana/SolanaWalletService.kt
package com.bitchat.android.solana
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.solanamobile.seedvault.WalletContractV1
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import org.bitcoinj.crypto.MnemonicCode
import org.bitcoinj.crypto.HDKeyDerivation
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
class SolanaWalletService(
private val context: Context,
private val rpcService: SolanaRpcService
) {
companion object {
private const val KEYSTORE_ALIAS = "bitchat_solana_wallet"
private const val PREFS_NAME = "bitchat_solana_prefs"
private const val KEY_PUBLIC_ADDRESS = "public_address"
private const val KEY_ENCRYPTED_SEED = "encrypted_seed"
private const val KEY_MNEMONIC = "mnemonic"
}
init {
// Add BouncyCastle provider if not already added
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(BouncyCastleProvider())
}
}
private val _walletState = MutableStateFlow<WalletState>(WalletState.Uninitialized)
val walletState: StateFlow<WalletState> = _walletState.asStateFlow()
private val _balance = MutableStateFlow(0L)
val balance: StateFlow<Long> = _balance.asStateFlow()
private val encryptedPrefs by lazy {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
context,
PREFS_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
private val keyStore: KeyStore by lazy {
KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
}
/**
* Generate a new Solana wallet
*/
suspend fun generateWallet(): Result<WalletInfo> {
return try {
// 1. Generate BIP39 mnemonic (24 words)
val entropy = ByteArray(32).apply {
java.security.SecureRandom().nextBytes(this)
}
val mnemonic = MnemonicCode.INSTANCE.toMnemonic(entropy)
val mnemonicPhrase = mnemonic.joinToString(" ")
// 2. Derive seed from mnemonic
val seed = MnemonicCode.toSeed(mnemonic, "")
// 3. Derive Solana keypair (BIP44: m/44'/501'/0'/0')
val masterKey = HDKeyDerivation.createMasterPrivateKey(seed)
val purposeKey = HDKeyDerivation.deriveChildKey(masterKey, 44 or HDKeyDerivation.HARDENED_BIT)
val coinTypeKey = HDKeyDerivation.deriveChildKey(purposeKey, 501 or HDKeyDerivation.HARDENED_BIT)
val accountKey = HDKeyDerivation.deriveChildKey(coinTypeKey, 0 or HDKeyDerivation.HARDENED_BIT)
val changeKey = HDKeyDerivation.deriveChildKey(accountKey, 0)
val addressKey = HDKeyDerivation.deriveChildKey(changeKey, 0)
// 4. Extract private key (32 bytes for Ed25519)
val privateKeyBytes = addressKey.privKeyBytes
// 5. Derive public key using Ed25519
val publicKeyBytes = derivePublicKey(privateKeyBytes)
// 6. Convert to base58 address
val publicAddress = Base58.encode(publicKeyBytes)
// 7. Encrypt and store private key
val encryptedSeed = encryptSeed(privateKeyBytes)
encryptedPrefs.edit().apply {
putString(KEY_PUBLIC_ADDRESS, publicAddress)
putString(KEY_ENCRYPTED_SEED, encryptedSeed)
putString(KEY_MNEMONIC, mnemonicPhrase)
}.apply()
// 8. Update state
val walletInfo = WalletInfo(
publicKey = publicAddress,
mnemonic = mnemonicPhrase
)
_walletState.value = WalletState.Initialized(walletInfo)
// 9. Start balance monitoring
startBalanceMonitoring()
Result.success(walletInfo)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Load existing wallet from storage
*/
suspend fun loadWallet(): Result<WalletInfo> {
return try {
val publicAddress = encryptedPrefs.getString(KEY_PUBLIC_ADDRESS, null)
?: return Result.failure(WalletException.NoWalletFound)
val mnemonic = encryptedPrefs.getString(KEY_MNEMONIC, null)
?: return Result.failure(WalletException.NoMnemonicFound)
val walletInfo = WalletInfo(
publicKey = publicAddress,
mnemonic = mnemonic
)
_walletState.value = WalletState.Initialized(walletInfo)
startBalanceMonitoring()
Result.success(walletInfo)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Get current SOL balance
*/
suspend fun refreshBalance() {
val currentState = _walletState.value
if (currentState !is WalletState.Initialized) return
try {
val balanceLamports = rpcService.getBalance(currentState.walletInfo.publicKey)
_balance.value = balanceLamports
} catch (e: Exception) {
// Log error but don't crash
e.printStackTrace()
}
}
/**
* Sign a Solana transaction
*/
suspend fun signTransaction(transaction: ByteArray): Result<ByteArray> {
return try {
val encryptedSeed = encryptedPrefs.getString(KEY_ENCRYPTED_SEED, null)
?: return Result.failure(WalletException.NoKeypairFound)
val privateKeyBytes = decryptSeed(encryptedSeed)
// Sign with Ed25519
val signature = signWithEd25519(transaction, privateKeyBytes)
Result.success(signature)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Export seed phrase (requires user authentication)
*/
fun exportSeedPhrase(): Result<String> {
return try {
val mnemonic = encryptedPrefs.getString(KEY_MNEMONIC, null)
?: return Result.failure(WalletException.NoMnemonicFound)
Result.success(mnemonic)
} catch (e: Exception) {
Result.failure(e)
}
}
// MARK: - Private Methods
private fun derivePublicKey(privateKey: ByteArray): ByteArray {
// Use BouncyCastle for Ed25519 key derivation
val keyFactory = java.security.KeyFactory.getInstance("Ed25519", "BC")
val privateKeySpec = org.bouncycastle.jce.spec.ECPrivateKeySpec(
java.math.BigInteger(1, privateKey),
org.bouncycastle.jce.ECNamedCurveTable.getParameterSpec("Ed25519")
)
// ... implementation details for Ed25519 public key derivation
// For production, use a library like TweetNaCl or Solana SDK's key derivation
return ByteArray(32) // Placeholder
}
private fun encryptSeed(seed: ByteArray): String {
// Get or create encryption key in Android Keystore
val secretKey = getOrCreateSecretKey()
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encryptedBytes = cipher.doFinal(seed)
// Combine IV + encrypted data
val combined = iv + encryptedBytes
return android.util.Base64.encodeToString(combined, android.util.Base64.DEFAULT)
}
private fun decryptSeed(encryptedData: String): ByteArray {
val secretKey = getOrCreateSecretKey()
val combined = android.util.Base64.decode(encryptedData, android.util.Base64.DEFAULT)
// Extract IV (first 12 bytes for GCM)
val iv = combined.sliceArray(0 until 12)
val encryptedBytes = combined.sliceArray(12 until combined.size)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
return cipher.doFinal(encryptedBytes)
}
private fun getOrCreateSecretKey(): SecretKey {
return if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
keyStore.getKey(KEYSTORE_ALIAS, null) as SecretKey
} else {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
KEYSTORE_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false) // Change to true for biometric
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
}
}
private fun signWithEd25519(message: ByteArray, privateKey: ByteArray): ByteArray {
// Use BouncyCastle or TweetNaCl for Ed25519 signing
// Placeholder implementation
return ByteArray(64) // Ed25519 signatures are 64 bytes
}
private suspend fun startBalanceMonitoring() {
// Poll balance every 10 seconds
kotlinx.coroutines.GlobalScope.launch {
while (true) {
refreshBalance()
kotlinx.coroutines.delay(10_000)
}
}
}
}
// MARK: - Data Classes
data class WalletInfo(
val publicKey: String,
val mnemonic: String
)
sealed class WalletState {
object Uninitialized : WalletState()
data class Initialized(val walletInfo: WalletInfo) : WalletState()
data class Error(val exception: Exception) : WalletState()
}
sealed class WalletException : Exception() {
object NoWalletFound : WalletException()
object NoMnemonicFound : WalletException()
object NoKeypairFound : WalletException()
}
```
#### 1.2 SolanaRpcService.kt
```kotlin
// solana/SolanaRpcService.kt
package com.bitchat.android.solana
import com.google.gson.Gson
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit
enum class Cluster(val url: String, val wsUrl: String) {
MAINNET(
"https://api.mainnet-beta.solana.com",
"wss://api.mainnet-beta.solana.com"
),
DEVNET(
"https://api.devnet.solana.com",
"wss://api.devnet.solana.com"
),
TESTNET(
"https://api.testnet.solana.com",
"wss://api.testnet.solana.com"
)
}
class SolanaRpcService(
private val cluster: Cluster = Cluster.DEVNET
) {
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val gson = Gson()
private val mediaType = "application/json".toMediaType()
/**
* Get SOL balance for a public key
*/
suspend fun getBalance(publicKey: String): Long = withContext(Dispatchers.IO) {
val requestBody = buildJsonRpcRequest(
method = "getBalance",
params = listOf(publicKey)
)
val response = executeRequest(requestBody)
val result = response.getAsJsonObject("result")
result.get("value").asLong
}
/**
* Get recent blockhash
*/
suspend fun getRecentBlockhash(): String = withContext(Dispatchers.IO) {
val requestBody = buildJsonRpcRequest(
method = "getLatestBlockhash",
params = listOf(
mapOf("commitment" to "confirmed")
)
)
val response = executeRequest(requestBody)
val value = response.getAsJsonObject("result").getAsJsonObject("value")
value.get("blockhash").asString
}
/**
* Send a signed transaction
*/
suspend fun sendTransaction(
signedTransaction: String,
encoding: String = "base64"
): String = withContext(Dispatchers.IO) {
val requestBody = buildJsonRpcRequest(
method = "sendTransaction",
params = listOf(
signedTransaction,
mapOf(
"encoding" to encoding,
"preflightCommitment" to "confirmed"
)
)
)
val response = executeRequest(requestBody)
response.get("result").asString
}
/**
* Confirm transaction
*/
suspend fun confirmTransaction(
signature: String,
timeout: Long = 30_000
): Boolean = withContext(Dispatchers.IO) {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val requestBody = buildJsonRpcRequest(
method = "getSignatureStatuses",
params = listOf(listOf(signature))
)
val response = executeRequest(requestBody)
val value = response.getAsJsonObject("result").getAsJsonArray("value")
if (value.size() > 0 && !value[0].isJsonNull) {
val status = value[0].asJsonObject
val confirmationStatus = status.get("confirmationStatus")?.asString
if (confirmationStatus == "confirmed" || confirmationStatus == "finalized") {
return@withContext true
}
}
kotlinx.coroutines.delay(1000) // Poll every 1 second
}
false
}
// MARK: - Private Methods
private fun buildJsonRpcRequest(method: String, params: List<Any>): String {
val request = JsonObject().apply {
addProperty("jsonrpc", "2.0")
addProperty("id", 1)
addProperty("method", method)
add("params", gson.toJsonTree(params))
}
return gson.toJson(request)
}
private fun executeRequest(jsonBody: String): JsonObject {
val requestBody = jsonBody.toRequestBody(mediaType)
val request = Request.Builder()
.url(cluster.url)
.post(requestBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw RpcException("RPC request failed: ${response.code}")
}
val responseBody = response.body?.string()
?: throw RpcException("Empty response body")
val jsonResponse = gson.fromJson(responseBody, JsonObject::class.java)
if (jsonResponse.has("error")) {
val error = jsonResponse.getAsJsonObject("error")
throw RpcException("RPC error: ${error.get("message").asString}")
}
return jsonResponse
}
}
}
class RpcException(message: String) : Exception(message)
```
#### 1.3 WalletScreen.kt (Jetpack Compose UI)
```kotlin
// ui/screens/WalletScreen.kt
package com.bitchat.android.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.bitchat.android.solana.WalletState
import com.bitchat.android.viewmodels.WalletViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WalletScreen(
viewModel: WalletViewModel = hiltViewModel()
) {
val walletState by viewModel.walletState.collectAsState()
val balance by viewModel.balance.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Solana Wallet") },
actions = {
IconButton(onClick = { /* Settings */ }) {
Icon(Icons.Default.Settings, "Settings")
}
}
)
}
) { paddingValues ->
when (val state = walletState) {
is WalletState.Uninitialized -> {
WalletOnboarding(
onCreateWallet = { viewModel.createWallet() },
onImportWallet = { /* TODO */ },
modifier = Modifier.padding(paddingValues)
)
}
is WalletState.Initialized -> {
WalletDashboard(
walletInfo = state.walletInfo,
balance = balance,
onSend = { /* Navigate to send screen */ },
onReceive = { /* Navigate to receive screen */ },
modifier = Modifier.padding(paddingValues)
)
}
is WalletState.Error -> {
ErrorScreen(
error = state.exception,
modifier = Modifier.padding(paddingValues)
)
}
}
}
}
@Composable
fun WalletOnboarding(
onCreateWallet: () -> Unit,
onImportWallet: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.AccountBalanceWallet,
contentDescription = null,
modifier = Modifier.size(100.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Create Your Solana Wallet",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Your wallet will be secured with your device's encryption",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onCreateWallet,
modifier = Modifier.fillMaxWidth()
) {
Text("Create Wallet")
}
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(
onClick = onImportWallet,
modifier = Modifier.fillMaxWidth()
) {
Text("Import Existing Wallet")
}
}
}
@Composable
fun WalletDashboard(
walletInfo: com.bitchat.android.solana.WalletInfo,
balance: Long,
onSend: () -> Unit,
onReceive: () -> Unit,
modifier: Modifier = Modifier
) {
val clipboardManager = LocalClipboardManager.current
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
// Balance Card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Balance",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = formatBalance(balance),
style = MaterialTheme.typography.displayMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "SOL",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(16.dp))
// Address
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(walletInfo.publicKey))
}
) {
Text(
text = truncateAddress(walletInfo.publicKey),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace
),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedButton(
onClick = onReceive,
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.QrCode, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Receive")
}
Button(
onClick = onSend,
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Send, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Send")
}
}
Spacer(modifier = Modifier.height(24.dp))
// Transaction History
Text(
text = "Recent Transactions",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
item {
Card(
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "No transactions yet",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
private fun formatBalance(lamports: Long): String {
val sol = lamports / 1_000_000_000.0
return String.format("%.4f", sol)
}
private fun truncateAddress(address: String): String {
return if (address.length > 16) {
"${address.take(8)}...${address.takeLast(8)}"
} else {
address
}
}
```
#### 1.4 WalletViewModel.kt
```kotlin
// viewmodels/WalletViewModel.kt
package com.bitchat.android.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitchat.android.solana.SolanaWalletService
import com.bitchat.android.solana.WalletState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class WalletViewModel @Inject constructor(
private val walletService: SolanaWalletService
) : ViewModel() {
val walletState: StateFlow<WalletState> = walletService.walletState
val balance: StateFlow<Long> = walletService.balance
init {
// Try to load existing wallet on init
viewModelScope.launch {
walletService.loadWallet()
}
}
fun createWallet() {
viewModelScope.launch {
walletService.generateWallet()
}
}
fun refreshBalance() {
viewModelScope.launch {
walletService.refreshBalance()
}
}
fun exportSeedPhrase(): Result<String> {
return walletService.exportSeedPhrase()
}
}
```
**Phase 1 Deliverables:**
- [ ] `SolanaWalletService.kt` - Wallet management
- [ ] `SolanaRpcService.kt` - RPC communication
- [ ] `WalletScreen.kt` - Wallet UI (Jetpack Compose)
- [ ] `WalletViewModel.kt` - State management
- [ ] `ReceiveScreen.kt` - QR code for receiving
- [ ] Unit tests for wallet generation and key storage
**Phase 1 Success Criteria:**
- Can generate new wallet and store securely
- Can display wallet address and balance
- Can copy address to clipboard
- Balance refreshes automatically
- Seed phrase can be exported
---
### Phase 2: Transaction Creation (Week 3)
**Goals:**
- Build UI for creating transactions
- Implement transaction signing
- Serialize transactions for Bluetooth transfer
**Components to Build:**
#### 2.1 TransactionBuilder.kt
```kotlin
// solana/TransactionBuilder.kt
package com.bitchat.android.solana
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class TransactionBuilder(
private val rpcService: SolanaRpcService
) {
/**
* Create a SOL transfer transaction
*/
suspend fun createTransferTransaction(
from: String,
to: String,
amountLamports: Long
): ByteArray = withContext(Dispatchers.Default) {
// Note: This is a simplified version
// In production, use Solana SDK's transaction builder
// Get recent blockhash (will be updated before broadcast)
val blockhash = rpcService.getRecentBlockhash()
// Build transaction structure
val transaction = SolanaTransaction(
recentBlockhash = blockhash,
feePayer = from,
instructions = listOf(
SystemProgramInstruction.transfer(
from = from,
to = to,
lamports = amountLamports
)
)
)
// Serialize to bytes
transaction.serialize()
}
/**
* Create transaction with priority fee
*/
suspend fun createTransferTransactionWithPriorityFee(
from: String,
to: String,
amountLamports: Long,
priorityFeeMicroLamports: Long = 5000
): ByteArray = withContext(Dispatchers.Default) {
val blockhash = rpcService.getRecentBlockhash()
val transaction = SolanaTransaction(
recentBlockhash = blockhash,
feePayer = from,
instructions = listOf(
ComputeBudgetInstruction.setComputeUnitPrice(priorityFeeMicroLamports),
SystemProgramInstruction.transfer(from, to, amountLamports)
)
)
transaction.serialize()
}
}
// Simplified transaction structure
data class SolanaTransaction(
val recentBlockhash: String,
val feePayer: String,
val instructions: List<TransactionInstruction>
) {
fun serialize(): ByteArray {
// Implement Solana transaction serialization
// This would use the actual Solana SDK in production
return ByteArray(0) // Placeholder
}
}
interface TransactionInstruction
object SystemProgramInstruction {
fun transfer(from: String, to: String, lamports: Long): TransactionInstruction {
return object : TransactionInstruction {}
}
}
object ComputeBudgetInstruction {
fun setComputeUnitPrice(microLamports: Long): TransactionInstruction {
return object : TransactionInstruction {}
}
}
```
#### 2.2 SendTransactionScreen.kt
```kotlin
// ui/screens/SendTransactionScreen.kt
package com.bitchat.android.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.bitchat.android.viewmodels.SendTransactionViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SendTransactionScreen(
onNavigateBack: () -> Unit,
viewModel: SendTransactionViewModel = hiltViewModel()
) {
var recipientAddress by remember { mutableStateOf("") }
var amount by remember { mutableStateOf("") }
var usePriorityFee by remember { mutableStateOf(false) }
var showConfirmDialog by remember { mutableStateOf(false) }
val transactionStatus by viewModel.transactionStatus.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Send SOL") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, "Back")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Recipient Section
Text(
text = "Recipient",
style = MaterialTheme.typography.titleMedium
)
OutlinedButton(
onClick = { /* Show peer selection */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Select BitChat Peer")
}
Text(
text = "Or enter address manually:",
style = MaterialTheme.typography.bodySmall
)
OutlinedTextField(
value = recipientAddress,
onValueChange = { recipientAddress = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("Solana Address") },
singleLine = true
)
Divider()
// Amount Section
Text(
text = "Amount",
style = MaterialTheme.typography.titleMedium
)
OutlinedTextField(
value = amount,
onValueChange = { amount = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("SOL") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
trailingIcon = { Text("SOL") }
)
// Quick amount buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
listOf(0.1, 0.5, 1.0).forEach { value ->
FilledTonalButton(
onClick = { amount = value.toString() },
modifier = Modifier.weight(1f)
) {
Text("$value SOL")
}
}
}
Divider()
// Fee Section
Text(
text = "Network Fee: ~0.000005 SOL",
style = MaterialTheme.typography.bodyMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Priority Fee")
Switch(
checked = usePriorityFee,
onCheckedChange = { usePriorityFee = it }
)
}
Spacer(modifier = Modifier.weight(1f))
// Send Button
Button(
onClick = { showConfirmDialog = true },
modifier = Modifier.fillMaxWidth(),
enabled = recipientAddress.isNotEmpty() && amount.toDoubleOrNull() != null
) {
Text("Review Transaction")
}
}
// Confirmation Dialog
if (showConfirmDialog) {
TransactionConfirmationDialog(
recipient = recipientAddress,
amount = amount.toDoubleOrNull() ?: 0.0,
onConfirm = {
viewModel.createAndSendTransaction(
recipient = recipientAddress,
amount = amount.toDoubleOrNull() ?: 0.0,
usePriorityFee = usePriorityFee
)
showConfirmDialog = false
},
onDismiss = { showConfirmDialog = false }
)
}
// Status snackbar
LaunchedEffect(transactionStatus) {
// Show status updates as snackbars
}
}
}
@Composable
fun TransactionConfirmationDialog(
recipient: String,
amount: Double,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Confirm Transaction") },
text = {
Column {
Text("Sending to:")
Text(recipient, style = MaterialTheme.typography.bodySmall)
Spacer(modifier = Modifier.height(8.dp))
Text("Amount: $amount SOL")
Text("Fee: ~0.000005 SOL")
Spacer(modifier = Modifier.height(8.dp))
Text("Total: ${amount + 0.000005} SOL", style = MaterialTheme.typography.titleMedium)
}
},
confirmButton = {
Button(onClick = onConfirm) {
Text("Confirm & Send")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
```
**Phase 2 Deliverables:**
- [ ] `TransactionBuilder.kt` - Transaction creation
- [ ] `SendTransactionScreen.kt` - Send UI
- [ ] `SendTransactionViewModel.kt` - Transaction state management
- [ ] `PeerSelectionScreen.kt` - Select BitChat peer as recipient
- [ ] Unit tests for transaction creation and signing
**Phase 2 Success Criteria:**
- Can create SOL transfer transactions
- Can sign transactions with wallet keypair
- Transaction data is properly serialized
- UI shows clear transaction status
- Can select BitChat peers as recipients
---
### Phase 3: Protocol Extension (Week 4)
Same as iOS plan, but implemented in Kotlin for Android.
**Key Files:**
- Extend `BinaryProtocol.kt`
- `TransactionProtocolService.kt`
- Integration with existing `BluetoothMeshService.kt`
---
### Phase 4: Broadcast Queue (Week 5)
**Components:**
- `BroadcastQueue.kt` using Room database
- `NetworkMonitor.kt` using ConnectivityManager
- Background WorkManager tasks
---
### Phase 5: Testing & Polish (Week 6)
**Testing:**
- Unit tests with JUnit
- UI tests with Compose Testing
- Integration tests
- Manual testing on physical devices
---
## File Structure
```
bitchat-android/
├── app/src/main/
│ ├── kotlin/com/bitchat/android/
│ │ ├── BitchatApplication.kt # Hilt entry point
│ │ │
│ │ ├── solana/ # NEW: Solana integration
│ │ │ ├── SolanaWalletService.kt
│ │ │ ├── SolanaRpcService.kt
│ │ │ ├── TransactionBuilder.kt
│ │ │ ├── TransactionProtocolService.kt
│ │ │ ├── BroadcastQueue.kt
│ │ │ └── NetworkMonitor.kt
│ │ │
│ │ ├── data/
│ │ │ ├── local/
│ │ │ │ ├── SolanaDatabase.kt
│ │ │ │ ├── WalletDao.kt
│ │ │ │ ├── TransactionDao.kt
│ │ │ │ └── entities/
│ │ │ │ ├── WalletEntity.kt
│ │ │ │ └── QueuedTransactionEntity.kt
│ │ │ │
│ │ │ └── models/
│ │ │ ├── SolanaTransactionPacket.kt
│ │ │ └── TransactionStatus.kt
│ │ │
│ │ ├── ui/
│ │ │ ├── screens/
│ │ │ │ ├── WalletScreen.kt
│ │ │ │ ├── SendTransactionScreen.kt
│ │ │ │ ├── ReceiveScreen.kt
│ │ │ │ └── TransactionHistoryScreen.kt
│ │ │ │
│ │ │ └── components/
│ │ │ ├── BalanceCard.kt
│ │ │ └── TransactionListItem.kt
│ │ │
│ │ ├── viewmodels/
│ │ │ ├── WalletViewModel.kt
│ │ │ └── SendTransactionViewModel.kt
│ │ │
│ │ ├── di/ # Hilt modules
│ │ │ ├── SolanaModule.kt
│ │ │ └── DatabaseModule.kt
│ │ │
│ │ ├── BluetoothMeshService.kt # EXISTING (extend)
│ │ ├── EncryptionService.kt # EXISTING
│ │ ├── BinaryProtocol.kt # EXISTING (extend)
│ │ └── ChatViewModel.kt # EXISTING (integrate)
│ │
│ └── res/
│ ├── values/
│ │ └── strings.xml
│ └── ...
│
└── app/build.gradle.kts # Dependencies
```
---
## Security Implementation
### Android Keystore Integration
```kotlin
// Security best practices for Android
class SecureKeyStorage(private val context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
fun storePrivateKey(alias: String, privateKey: ByteArray) {
// Use Android Keystore for hardware-backed encryption
val secretKey = getOrCreateSecretKey(alias)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encryptedKey = cipher.doFinal(privateKey)
// Store IV + encrypted key in EncryptedSharedPreferences
val prefs = getEncryptedPreferences()
prefs.edit()
.putString("${alias}_iv", Base64.encodeToString(iv, Base64.DEFAULT))
.putString("${alias}_key", Base64.encodeToString(encryptedKey, Base64.DEFAULT))
.apply()
}
private fun getOrCreateSecretKey(alias: String): SecretKey {
if (!keyStore.containsAlias(alias)) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // Biometric required
.setUserAuthenticationValidityDurationSeconds(30)
.build()
keyGenerator.init(spec)
return keyGenerator.generateKey()
}
return keyStore.getKey(alias, null) as SecretKey
}
private fun getEncryptedPreferences(): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
"solana_secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
```
---
## Testing Strategy
### Unit Tests
```kotlin
// SolanaWalletServiceTest.kt
package com.bitchat.android.solana
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.Assert.*
class SolanaWalletServiceTest {
@Test
fun `generateWallet creates valid keypair`() = runTest {
val walletService = SolanaWalletService(context, rpcService)
val result = walletService.generateWallet()
assertTrue(result.isSuccess)
val walletInfo = result.getOrNull()!!
assertTrue(walletInfo.publicKey.length in 32..44)
assertEquals(24, walletInfo.mnemonic.split(" ").size)
}
@Test
fun `signTransaction produces valid signature`() = runTest {
// Test transaction signing
}
}
```
### UI Tests (Compose)
```kotlin
// WalletScreenTest.kt
@RunWith(AndroidJUnit4::class)
class WalletScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun walletScreen_displaysBalance() {
composeTestRule.setContent {
WalletScreen()
}
composeTestRule.onNodeWithText("Balance").assertIsDisplayed()
}
}
```
---
## Summary
This Android implementation plan mirrors the iOS plan but uses:
- **Kotlin** instead of Swift
- **Jetpack Compose** instead of SwiftUI
- **Room** instead of CoreData
- **Android Keystore** instead of iOS Keychain
- **Hilt** for dependency injection
- **Coroutines + Flow** for async operations
**Estimated Timeline**: 6-8 weeks for MVP with single Android developer
**Next Steps**: Start with Phase 0 to set up the foundation, then proceed through each phase systematically.
---
Would you like me to expand on any specific component or create additional code examples?