# 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?