From ae6e895846ee93f94094bcb84aaefd392d333761 Mon Sep 17 00:00:00 2001 From: Sajal Bansal Date: Sat, 26 Oct 2024 20:31:57 +0530 Subject: [PATCH 1/2] Token added functionality,balance and price of all tokens --- wallet_app/app/build.gradle.kts | 3 + wallet_app/app/src/main/AndroidManifest.xml | 1 + .../java/com/example/walletapp/model/Token.kt | 13 +- .../com/example/walletapp/ui/MainActivity.kt | 12 +- .../com/example/walletapp/ui/WalletApp.kt | 7 +- .../walletapp/ui/account/AddTokenScreen.kt | 42 +++++- .../walletapp/ui/account/WalletScreen.kt | 133 +++++++++++------- .../example/walletapp/utils/StarknetClient.kt | 5 +- 8 files changed, 153 insertions(+), 63 deletions(-) diff --git a/wallet_app/app/build.gradle.kts b/wallet_app/app/build.gradle.kts index 41e6c2b0..cacebe9c 100644 --- a/wallet_app/app/build.gradle.kts +++ b/wallet_app/app/build.gradle.kts @@ -117,6 +117,9 @@ dependencies { implementation(libs.androidx.hilt.navigation.fragment) implementation (libs.androidx.hilt.navigation.compose.v100alpha03) + implementation("androidx.room:room-runtime:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") implementation(libs.androidx.ui.tooling.preview) debugImplementation(libs.androidx.ui.tooling) diff --git a/wallet_app/app/src/main/AndroidManifest.xml b/wallet_app/app/src/main/AndroidManifest.xml index f3e65a07..7b3ef23a 100644 --- a/wallet_app/app/src/main/AndroidManifest.xml +++ b/wallet_app/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ { AddTokenScreen( + tokenViewModel=tokenViewModel, onConfirm = { navController.navigateUp() } ) } diff --git a/wallet_app/app/src/main/java/com/example/walletapp/ui/account/AddTokenScreen.kt b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/AddTokenScreen.kt index 67942fa1..fa01ff94 100644 --- a/wallet_app/app/src/main/java/com/example/walletapp/ui/account/AddTokenScreen.kt +++ b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/AddTokenScreen.kt @@ -1,5 +1,7 @@ package com.example.walletapp.ui.account +import android.app.Activity +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -22,20 +24,24 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt import com.example.walletapp.R +import com.example.walletapp.model.Token +import com.swmansion.starknet.data.types.Felt @Composable -fun AddTokenScreen(onConfirm: () -> Unit) { +fun AddTokenScreen(tokenViewModel:TokenViewModel,onConfirm: () -> Unit) { Surface(modifier = Modifier.fillMaxSize()) { val contactAddress = rememberSaveable { mutableStateOf("") } val name = rememberSaveable { mutableStateOf("") } val symbol = rememberSaveable { mutableStateOf("") } val decimals = rememberSaveable { mutableStateOf("") } + val context = (LocalContext.current as Activity) Column( modifier = Modifier @@ -92,7 +98,36 @@ fun AddTokenScreen(onConfirm: () -> Unit) { // TODO: handle saving the token data Button( - onClick = onConfirm, + onClick = { + val decimalValue = decimals.value.toIntOrNull() + val addressPattern = Regex("^0x[a-fA-F0-9]{64}$") + if (!addressPattern.matches(contactAddress.value)) { + Toast.makeText(context, "Please enter a valid address", Toast.LENGTH_LONG).show() + } + else if (name.value.isEmpty()) { + Toast.makeText(context, "Please enter name", Toast.LENGTH_LONG) + .show() + } + else if (symbol.value.isEmpty()) { + Toast.makeText(context, "Please enter symbol", Toast.LENGTH_LONG) + .show() + } else if (decimalValue == null || decimalValue !in 0..18) { + Toast.makeText(context, "Please enter valid decimal", Toast.LENGTH_LONG) + .show() + }else{ + onConfirm() + // Save data to local storage here + val address=Felt.fromHex(contactAddress.value) + val newToken = Token( + contactAddress = address, + name = name.value, + symbol = symbol.value, + decimals = decimalValue + ) + tokenViewModel.insertToken(newToken) // Insert the new token + Toast.makeText(context, "Token added!", Toast.LENGTH_LONG).show() + } + }, colors = ButtonDefaults.buttonColors(backgroundColor = Color("#1B1B76".toColorInt())), modifier = Modifier .fillMaxWidth() @@ -135,10 +170,11 @@ fun SimpleTextField( shape = RoundedCornerShape(15.dp), modifier = Modifier.fillMaxWidth(), textStyle = TextStyle( - color = Color.Black, + color = Color.White, fontSize = 16.sp ) ) } } + diff --git a/wallet_app/app/src/main/java/com/example/walletapp/ui/account/WalletScreen.kt b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/WalletScreen.kt index 34dc73d2..9ad92374 100644 --- a/wallet_app/app/src/main/java/com/example/walletapp/ui/account/WalletScreen.kt +++ b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/WalletScreen.kt @@ -1,6 +1,8 @@ package com.example.walletapp.ui.account +import android.annotation.SuppressLint import android.app.Activity +import android.util.Log import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.ui.graphics.painter.Painter @@ -18,6 +20,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults @@ -40,9 +43,13 @@ import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.* import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.viewmodel.compose.viewModel @@ -51,9 +58,13 @@ import com.example.walletapp.R import com.example.walletapp.utils.StarknetClient import com.example.walletapp.utils.toDoubleWithTwoDecimal import com.example.walletapp.utils.weiToEther +import com.swmansion.starknet.crypto.starknetKeccak import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.provider.exceptions.RpcRequestFailedException +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext @@ -61,55 +72,67 @@ import kotlinx.coroutines.withContext fun WalletScreen( onNewTokenPress: () -> Unit, onSendPress: () -> Unit, - onReceivePress: () -> Unit + onReceivePress: () -> Unit, + tokenViewModel: TokenViewModel ) { Surface(modifier = Modifier.fillMaxSize()) { Wallet( modifier = Modifier.padding(10.dp), onNewTokenPress = onNewTokenPress, onSendPress = onSendPress, - onReceivePress = onReceivePress + onReceivePress = onReceivePress, + tokenViewModel = tokenViewModel ) } } +@SuppressLint("MutableCollectionMutableState") @Composable -fun Wallet(modifier: Modifier, onNewTokenPress: () -> Unit, onReceivePress: () -> Unit, onSendPress: () -> Unit) { +fun Wallet(modifier: Modifier, onNewTokenPress: () -> Unit, onReceivePress: () -> Unit, onSendPress: () -> Unit,tokenViewModel: TokenViewModel) { val networkList = listOf("Starknet Mainnet", "Test Networks") var selectedNetworkIndex by remember { mutableStateOf(0) } + val tokens by tokenViewModel.tokens.observeAsState(initial = emptyList()) val context = (LocalContext.current as Activity) val address= BuildConfig.ACCOUNT_ADDRESS val accountAddress = Felt.fromHex(address) val starknetClient = StarknetClient(BuildConfig.RPC_URL) - var balance by remember { mutableStateOf("") } - + var balances by remember { mutableStateOf>>(emptyList()) } val coinViewModel: CoinViewModel = viewModel() - - val coinsPrices : HashMap = rememberSaveable { - hashMapOf() - } - - val prices by coinViewModel.prices + val coinsPrices by rememberSaveable { mutableStateOf>(hashMapOf()) } + var prices by rememberSaveable { mutableStateOf(mapOf()) } + prices = coinViewModel.prices.value val errorMessage by coinViewModel.errorMessage - // TODO(#106): use the accounts stored tokens instead of hardcoding - LaunchedEffect(Unit) { - coinViewModel.getTokenPrices(ids = "starknet,ethereum", vsCurrencies = "usd") - } - - LaunchedEffect (Unit){ - // TODO(#107): fetch all token balances + LaunchedEffect(tokens) { try { - val getBalance = starknetClient.getEthBalance(accountAddress) - withContext(Dispatchers.Main) { - balance = weiToEther(getBalance).toDoubleWithTwoDecimal() + // TODO(#106): use the accounts stored tokens instead of hardcoding + coinViewModel.getTokenPrices(ids = tokens.joinToString(",") { it.name.toLowerCase() }, vsCurrencies = "usd") + + // TODO(#107): fetch all token balances + val balanceDeferred: List>> = tokens.map { token -> + async(Dispatchers.IO) { + try { + val balanceInWei = starknetClient.getBalance(accountAddress, token.contactAddress) + val balanceInEther = weiToEther(balanceInWei).toDoubleWithTwoDecimal() + hashMapOf(token.name to balanceInEther) + } catch (e: RpcRequestFailedException) { + withContext(Dispatchers.Main) { + Toast.makeText(context, "${e.code}: ${e.message}", Toast.LENGTH_LONG).show() + } + hashMapOf(token.name to "0.0") + } + } } - } catch (e: RpcRequestFailedException) { - withContext(Dispatchers.Main) { Toast.makeText(context, "${e.code}: ${e.message}", Toast.LENGTH_LONG).show() } + + // Wait for all balance fetching to complete + balances = balanceDeferred.awaitAll() + } catch (e: Exception) { - withContext(Dispatchers.Main) { Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() } + withContext(Dispatchers.Main) { + Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() + } } } if (errorMessage.isNotEmpty()) { @@ -167,36 +190,34 @@ fun Wallet(modifier: Modifier, onNewTokenPress: () -> Unit, onReceivePress: () - ) Spacer(modifier = Modifier.height(32.dp)) - - - WalletCard( - icon = painterResource(id = R.drawable.ic_ethereum), - amount = coinsPrices["ethereum"]?.let { "$ $it" } ?: "", - exchange = balance, - type = "ETH" - ) - - // TOOD(#82): load actual balance - WalletCard( - icon = painterResource(id = R.drawable.token2), - amount = coinsPrices["starknet"]?.let { "$ $it" } ?: "", - exchange ="4.44", - type = "STRK" - ) + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp/2 + LazyColumn(modifier=Modifier.height(screenHeight)) { + items(tokens.size) { index-> + WalletCard( + icon = painterResource(id = R.drawable.ic_ethereum), + amount = coinsPrices[tokens[index].name]?.let { "$ $it" } ?: "", + name=tokens[index].name, + balance = balances.firstOrNull { balanceMap -> + balanceMap.containsKey(tokens[index].name) + }?.get(tokens[index].name)?: "0.0", + type = tokens[index].symbol.toUpperCase() + ) + } + } Spacer(modifier = Modifier.height(32.dp)) - Text( text = "+ New Token", fontFamily = FontFamily(Font(R.font.publicsans_bold)), color = Color.White, fontSize = 14.sp, - modifier = Modifier + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() .clickable { onNewTokenPress() } .background(Color.Transparent) - .padding(10.dp) .align(Alignment.CenterHorizontally) ) @@ -238,7 +259,7 @@ fun Wallet(modifier: Modifier, onNewTokenPress: () -> Unit, onReceivePress: () - @Composable -fun WalletCard(icon: Painter, amount: String, exchange: String, type: String) { +fun WalletCard(icon: Painter,name:String, amount: String, balance: String, type: String) { Card( backgroundColor = Color(0xFF1E1E96), modifier = Modifier @@ -251,10 +272,21 @@ fun WalletCard(icon: Painter, amount: String, exchange: String, type: String) { .padding(16.dp) .fillMaxWidth() ) { - Image( - painter = icon, // replace with your Ethereum icon - contentDescription = null, - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = icon, // replace with your Ethereum icon + contentDescription = null, + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = name.replaceFirstChar { it.uppercase() }, + fontFamily = FontFamily(Font(R.font.publicsans_semibold)), + color = Color.White, + fontSize = 18.sp + ) + + } + Spacer(modifier = Modifier.weight(1f)) Column(modifier = Modifier, horizontalAlignment = Alignment.End) { Text( @@ -267,7 +299,7 @@ fun WalletCard(icon: Painter, amount: String, exchange: String, type: String) { Row { Text( - text = exchange, + text = balance, fontFamily = FontFamily(Font(R.font.publicsans_bold)), color = Color.White, fontSize = 14.sp @@ -442,8 +474,5 @@ fun SwitchNetwork( } } } - - - } } diff --git a/wallet_app/app/src/main/java/com/example/walletapp/utils/StarknetClient.kt b/wallet_app/app/src/main/java/com/example/walletapp/utils/StarknetClient.kt index ff3401c6..30512364 100644 --- a/wallet_app/app/src/main/java/com/example/walletapp/utils/StarknetClient.kt +++ b/wallet_app/app/src/main/java/com/example/walletapp/utils/StarknetClient.kt @@ -59,12 +59,13 @@ class StarknetClient(private val rpcUrl: String) { // TODO(#107): change name to getBalance, support getting balance for any token // follow example: https://github.com/software-mansion/starknet-jvm/blob/main/androiddemo/src/main/java/com/example/androiddemo/MainActivity.kt - suspend fun getEthBalance(accountAddress: Felt): Uint256 { + suspend fun getBalance(accountAddress: Felt,contractAddress:Felt): Uint256 { val erc20ContractAddress = Felt.fromHex(ETH_ERC20_ADDRESS) + // Create a call to Starknet ERC-20 ETH contract val call = Call( - contractAddress = erc20ContractAddress, + contractAddress = contractAddress, entrypoint = "balanceOf", // entrypoint can be passed both as a string name and Felt value calldata = listOf(accountAddress), // calldata is List, so we wrap accountAddress in listOf() ) From 2ef9db30e22d25101da7b871dd5900952e431a8b Mon Sep 17 00:00:00 2001 From: Sajal Bansal Date: Sun, 27 Oct 2024 17:44:54 +0530 Subject: [PATCH 2/2] files added --- .../example/walletapp/WalletAppApplication.kt | 11 ++++ .../java/com/example/walletapp/db/TokenDao.kt | 19 ++++++ .../com/example/walletapp/db/TokenDatabase.kt | 59 +++++++++++++++++++ .../walletapp/ui/account/TokenRepository.kt | 20 +++++++ .../walletapp/ui/account/TokenViewModel.kt | 39 ++++++++++++ .../com/example/walletapp/utils/Converters.kt | 17 ++++++ 6 files changed, 165 insertions(+) create mode 100644 wallet_app/app/src/main/java/com/example/walletapp/WalletAppApplication.kt create mode 100644 wallet_app/app/src/main/java/com/example/walletapp/db/TokenDao.kt create mode 100644 wallet_app/app/src/main/java/com/example/walletapp/db/TokenDatabase.kt create mode 100644 wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenRepository.kt create mode 100644 wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenViewModel.kt create mode 100644 wallet_app/app/src/main/java/com/example/walletapp/utils/Converters.kt diff --git a/wallet_app/app/src/main/java/com/example/walletapp/WalletAppApplication.kt b/wallet_app/app/src/main/java/com/example/walletapp/WalletAppApplication.kt new file mode 100644 index 00000000..65e3d9ac --- /dev/null +++ b/wallet_app/app/src/main/java/com/example/walletapp/WalletAppApplication.kt @@ -0,0 +1,11 @@ +package com.example.walletapp + +import android.app.Application +import com.example.walletapp.db.TokenDatabase + + +class WalletAppApplication : Application() { + + // Lazy initialization of the database + val database: TokenDatabase by lazy { TokenDatabase.getDatabase(this) } +} \ No newline at end of file diff --git a/wallet_app/app/src/main/java/com/example/walletapp/db/TokenDao.kt b/wallet_app/app/src/main/java/com/example/walletapp/db/TokenDao.kt new file mode 100644 index 00000000..a7084227 --- /dev/null +++ b/wallet_app/app/src/main/java/com/example/walletapp/db/TokenDao.kt @@ -0,0 +1,19 @@ +package com.example.walletapp.db + + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import com.example.walletapp.model.Token +import kotlinx.coroutines.flow.Flow +import androidx.room.Query + +@Dao +interface TokenDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(token: Token) + + @Query("SELECT * FROM token_table") + fun getAllTokens(): Flow> +} diff --git a/wallet_app/app/src/main/java/com/example/walletapp/db/TokenDatabase.kt b/wallet_app/app/src/main/java/com/example/walletapp/db/TokenDatabase.kt new file mode 100644 index 00000000..a2b08f63 --- /dev/null +++ b/wallet_app/app/src/main/java/com/example/walletapp/db/TokenDatabase.kt @@ -0,0 +1,59 @@ +package com.example.walletapp.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase +import com.example.walletapp.model.Token +import com.example.walletapp.utils.Converters +import com.swmansion.starknet.data.types.Felt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Database(entities = [Token::class], version = 1, exportSchema = false) +@TypeConverters(Converters::class) +abstract class TokenDatabase : RoomDatabase() { + + abstract fun tokenDao(): TokenDao + + companion object { + @Volatile + private var INSTANCE: TokenDatabase? = null + + fun getDatabase(context: Context): TokenDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + TokenDatabase::class.java, + "token_database" + ) + .addCallback(DatabaseCallback()) // Add callback here + .build() + INSTANCE = instance + instance + } + } + } + + private class DatabaseCallback : RoomDatabase.Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + INSTANCE?.let { database -> + CoroutineScope(Dispatchers.IO).launch { + populateDatabase(database.tokenDao()) + } + } + } + + suspend fun populateDatabase(tokenDao: TokenDao) { + // Add default tokens + val token1 = Token(contactAddress = Felt.fromHex("0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"), name = "ethereum", symbol = "ETH", decimals = 18) + val token2 = Token(contactAddress = Felt.fromHex("0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), name = "starknet", symbol = "STRK", decimals = 18) + tokenDao.insert(token1) + tokenDao.insert(token2) + } + } +} diff --git a/wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenRepository.kt b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenRepository.kt new file mode 100644 index 00000000..13d0851d --- /dev/null +++ b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenRepository.kt @@ -0,0 +1,20 @@ +package com.example.walletapp.ui.account + +import com.example.walletapp.db.TokenDao +import com.example.walletapp.model.Token +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +class TokenRepository(private val tokenDao: TokenDao) { + + // Get all tokens from the database + val allTokens: Flow> = tokenDao.getAllTokens() + + // Insert a token into the database + suspend fun insertToken(token: Token) { + withContext(Dispatchers.IO) { + tokenDao.insert(token) + } + } +} diff --git a/wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenViewModel.kt b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenViewModel.kt new file mode 100644 index 00000000..afddc8f5 --- /dev/null +++ b/wallet_app/app/src/main/java/com/example/walletapp/ui/account/TokenViewModel.kt @@ -0,0 +1,39 @@ +package com.example.walletapp.ui.account + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.example.walletapp.model.Token +import kotlinx.coroutines.launch + +class TokenViewModel(application: Application, private val repository: TokenRepository) : AndroidViewModel(application) { + + + // Expose all tokens as LiveData + val tokens: LiveData> = repository.allTokens.asLiveData() + + // Function to insert a new token + fun insertToken(token: Token) { + viewModelScope.launch { + repository.insertToken(token) + } + } + + // Define a factory to pass the repository + class Factory(private val application: Application, private val repository: TokenRepository) : + ViewModelProvider.AndroidViewModelFactory(application) { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(TokenViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return TokenViewModel(application, repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + } +} + diff --git a/wallet_app/app/src/main/java/com/example/walletapp/utils/Converters.kt b/wallet_app/app/src/main/java/com/example/walletapp/utils/Converters.kt new file mode 100644 index 00000000..bb9ea1f6 --- /dev/null +++ b/wallet_app/app/src/main/java/com/example/walletapp/utils/Converters.kt @@ -0,0 +1,17 @@ +package com.example.walletapp.utils + +import androidx.room.TypeConverter +import com.swmansion.starknet.data.types.Felt + +class Converters { + + @TypeConverter + fun fromFelt(felt: Felt): String { + return felt.hexString() // Convert Felt to String + } + + @TypeConverter + fun toFelt(feltString: String): Felt { + return Felt.fromHex(feltString) // Convert String back to Felt + } +}