Skip to content

Commit

Permalink
Add Choose token option to Solana
Browse files Browse the repository at this point in the history
  • Loading branch information
aminsato committed Oct 31, 2024
1 parent 9c06bc2 commit fc56ba1
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ import com.vultisig.wallet.data.models.logo
import com.vultisig.wallet.data.repositories.AccountsRepository
import com.vultisig.wallet.data.repositories.BalanceVisibilityRepository
import com.vultisig.wallet.data.repositories.ExplorerLinkRepository
import com.vultisig.wallet.data.repositories.RequestResultRepository
import com.vultisig.wallet.data.repositories.VaultRepository
import com.vultisig.wallet.data.usecases.DiscoverTokenUseCase
import com.vultisig.wallet.data.usecases.EnableTokenUseCase
import com.vultisig.wallet.ui.models.TokenSelectionViewModel.Companion.REQUEST_SEARCHED_TOKEN_ID
import com.vultisig.wallet.ui.models.mappers.FiatValueToStringMapper
import com.vultisig.wallet.ui.models.mappers.TokenValueToDecimalUiStringMapper
import com.vultisig.wallet.ui.navigation.Destination
Expand Down Expand Up @@ -53,7 +50,6 @@ internal data class ChainTokensUiModel(
val canSelectTokens: Boolean = false,
val isBalanceVisible: Boolean = true,
val isBuyWeweVisible: Boolean = false,
val enableCustomToken: Boolean = false,
)

@Immutable
Expand All @@ -78,20 +74,14 @@ internal class ChainTokensViewModel @Inject constructor(
private val accountsRepository: AccountsRepository,
private val balanceVisibilityRepository: BalanceVisibilityRepository,
private val enableTokenUseCase: EnableTokenUseCase,
private val requestResultRepository: RequestResultRepository,
private val vaultRepository: VaultRepository,
) : ViewModel() {
private val tokens = MutableStateFlow(emptyList<Coin>())
private val chainRaw: String =
requireNotNull(savedStateHandle.get<String>(Destination.ARG_CHAIN_ID))
private val vaultId: String =
requireNotNull(savedStateHandle.get<String>(Destination.ARG_VAULT_ID))

val uiState = MutableStateFlow(
ChainTokensUiModel(
enableCustomToken = Chain.fromRaw(chainRaw) == Chain.Solana
)
)
val uiState = MutableStateFlow(ChainTokensUiModel())

private var loadDataJob: Job? = null

Expand Down Expand Up @@ -164,18 +154,6 @@ internal class ChainTokensViewModel @Inject constructor(
}
}

fun openCustomTokenScreen() {
viewModelScope.launch {
navigator.navigate(
Destination.CustomToken(
chainId = chainRaw
)
)
val searchedCoin = requestResultRepository.request<Coin>(REQUEST_SEARCHED_TOKEN_ID)
vaultRepository.addTokenToVault(vaultId, searchedCoin)
}
}

fun openToken(model: ChainTokenUiModel) {
viewModelScope.launch {
navigator.navigate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vultisig.wallet.data.models.Chain
import com.vultisig.wallet.data.models.Coin
import com.vultisig.wallet.data.models.FiatValue
import com.vultisig.wallet.data.models.logo
import com.vultisig.wallet.data.repositories.RequestResultRepository
import com.vultisig.wallet.data.usecases.CoinAndFiatValue
import com.vultisig.wallet.data.usecases.SearchTokenUseCase
import com.vultisig.wallet.ui.models.TokenSelectionViewModel.Companion.REQUEST_SEARCHED_TOKEN_ID
import com.vultisig.wallet.ui.models.mappers.FiatValueToStringMapper
Expand Down Expand Up @@ -49,7 +49,7 @@ internal class CustomTokenViewModel @Inject constructor(
fun searchCustomToken() {
viewModelScope.launch {
showLoading()
val searchedToken: Pair<Coin, FiatValue>? =
val searchedToken: CoinAndFiatValue? =
searchToken(
chainId,
searchFieldState.text.toString()
Expand All @@ -58,12 +58,12 @@ internal class CustomTokenViewModel @Inject constructor(
if (searchedToken == null) {
showError()
} else {
val price = fiatValueToStringMapper.map(searchedToken.second)
val price = fiatValueToStringMapper.map(searchedToken.fiatValue)
uiModel.update {
it.copy(
isLoading = false,
hasError = false,
token = searchedToken.first,
token = searchedToken.coin,
price = price
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,10 @@ internal class TokenSelectionViewModel @Inject constructor(
private val chainId: String =
requireNotNull(savedStateHandle[ARG_CHAIN_ID])

private val enabledTokens = MutableStateFlow(emptySet<String>())
private val enabledTokenIds = MutableStateFlow(emptySet<String>())

private val selectedTokens = MutableStateFlow(emptyList<Coin>())
private val allTokens = MutableStateFlow(emptyList<Coin>())
private val builtInTokens = MutableStateFlow(emptyList<Coin>())
private val enabledTokens = MutableStateFlow(emptyList<Coin>())
private val disabledTokens = MutableStateFlow(emptyList<Coin>())

val uiState = MutableStateFlow(TokenSelectionUiModel())

Expand All @@ -80,14 +79,14 @@ internal class TokenSelectionViewModel @Inject constructor(

fun enableToken(coin: Coin) = viewModelScope.launch {
enableTokenUseCase(vaultId, coin)?.let { updatedCoinId ->
enabledTokens.update { it + updatedCoinId }
enabledTokenIds.update { it + updatedCoinId }
}
}

fun disableToken(coin: Coin) {
viewModelScope.launch {
vaultRepository.deleteTokenFromVault(vaultId, coin.id)
enabledTokens.update { it - coin.id }
enabledTokenIds.update { it - coin.id }
}
}

Expand All @@ -101,28 +100,25 @@ internal class TokenSelectionViewModel @Inject constructor(
val chain = Chain.fromRaw(chainId)

viewModelScope.launch {
val enabled = vaultRepository
val enabledTokens = vaultRepository
.getEnabledTokens(vaultId)
.first()
.filter { !it.isNativeToken && it.chain == chain }

selectedTokens.value = enabled
this@TokenSelectionViewModel.enabledTokens.value = enabledTokens

val enabledTokenIds = enabled.map { it.id }.toSet()
enabledTokens.value = enabledTokenIds

builtInTokens.value = tokenRepository.builtInTokens.first()
.asSequence()
.filter { it.chain == chain && !it.isNativeToken }
.filter { it.id !in enabledTokenIds }
.toList()
val enabledTokenIds = enabledTokens.map { it.id }.toSet()
this@TokenSelectionViewModel.enabledTokenIds.value = enabledTokenIds

try {
val tokens = tokenRepository.getChainTokens(chain)
val vault = vaultRepository.get(vaultId) ?: error("No vault with id $vaultId")
val enabledCoins = vault.coins.filter { it.chain == chain }
val address = enabledCoins.first().address
val allChainTokens = tokenRepository.getChainTokens(chain, address)
.map { tokens -> tokens.filter { !it.isNativeToken } }
.first()

allTokens.value = tokens.filter { it.id !in enabledTokenIds }
disabledTokens.value = allChainTokens.filter { it.id !in enabledTokenIds }
} catch (e: Exception) {
// todo handle error
Timber.e(e)
Expand All @@ -132,22 +128,21 @@ internal class TokenSelectionViewModel @Inject constructor(

private fun collectTokens() {
combine(
enabledTokenIds,
enabledTokens,
selectedTokens,
allTokens,
builtInTokens,
disabledTokens,
searchTextFieldState.textAsFlow()
.map { it.toString() },
) { enabled, selected, all, builtIn, query ->
val selectedUiTokens = selected
) { enabledTokenIds, enabledTokens, disabledTokens, query ->
val selectedUiTokens = enabledTokens
.filter { it.ticker.contains(query, ignoreCase = true) }
.asUiTokens(enabled)
.asUiTokens(enabledTokenIds)

val otherUiTokens = if (query.isNotBlank()) {
all.filter { it.ticker.contains(query, ignoreCase = true) }
disabledTokens.filter { it.ticker.contains(query, ignoreCase = true) }
} else {
builtIn
}.asUiTokens(enabled)
enabledTokens + disabledTokens
}.asUiTokens(enabledTokenIds)

uiState.update {
it.copy(
Expand All @@ -170,7 +165,7 @@ internal class TokenSelectionViewModel @Inject constructor(
private fun enableSearchedToken(coin: Coin) {
viewModelScope.launch {
coin.apply {
if (enabledTokens.value.contains(id))
if (enabledTokenIds.value.contains(id))
return@apply
enableToken(this).join()
loadTokens()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ internal fun ChainTokensScreen(
onSwap = viewModel::swap,
onDeposit = viewModel::deposit,
onSelectTokens = viewModel::selectTokens,
onCustomTokenClick = viewModel::openCustomTokenScreen,
onTokenClick = viewModel::openToken,
onBuyWeweClick = viewModel::buyWewe,
onQrBtnClick = viewModel::navigateToQrAddressScreen,
Expand All @@ -110,7 +109,6 @@ private fun ChainTokensScreen(
onSwap: () -> Unit = {},
onDeposit: () -> Unit = {},
onSelectTokens: () -> Unit = {},
onCustomTokenClick: () -> Unit = {},
onTokenClick: (ChainTokenUiModel) -> Unit = {},
onBuyWeweClick: () -> Unit = {},
onQrBtnClick: () -> Unit = {},
Expand Down Expand Up @@ -272,14 +270,6 @@ private fun ChainTokensScreen(
)
}

if (uiModel.enableCustomToken) {
UiPlusButton(
title = stringResource(R.string.chain_tokens_screen_custom_token),
onClick = onCustomTokenClick,
modifier = Modifier
.padding(vertical = 16.dp),
)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,6 @@
<string name="transaction_complete_screen_title" translatable="false">Transaction complete</string>
<string name="keygen_fast_vault_email_hint" translatable="false">This email is only used to send the backup</string>
<string name="register_vault_screen_save_qr_code" translatable="false">Save QR Code</string>
<string name="verify_swap_screen_total_fees" translatable="false">Total Fees</string>
<string name="chain_tokens_screen_custom_token" translatable="false">Custom Token</string>
<string name="verify_swap_screen_total_fees" translatable="false">Total Fees</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ internal class CoinGeckoApiImpl @Inject constructor(
Chain.Optimism -> "optimistic-ethereum"
Chain.BscChain -> "binance-smart-chain"
Chain.ZkSync -> "zksync"
Chain.Solana -> "solana"

else -> error("No CoinGecko asset id for chain $this")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ val Chain.canSelectTokens: Boolean
get() = when {
this == Chain.CronosChain || this == Chain.ZkSync -> false
standard == EVM -> true
this == Chain.MayaChain -> true
this == Chain.MayaChain || this == Chain.Solana -> true
else -> false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ internal class AccountsRepositoryImpl @Inject constructor(
solanaCoins: List<Coin>,
vault: Vault
): List<Coin> {
if (solanaCoins.isNotEmpty()) return emptyList()
val solanaAddress = solanaCoins.firstOrNull()?.address
val newSPLTokens = mutableListOf<Coin>()
solanaAddress?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface SplTokenRepository {
suspend fun getCachedBalance(coin: Coin): BigInteger
suspend fun getBalance(coin: Coin): BigInteger?
suspend fun getTokenByContract(contractAddress: String): Coin?
suspend fun getTokens(address: String): List<Coin>
}

internal class SplTokenRepositoryImpl @Inject constructor(
Expand All @@ -33,19 +34,29 @@ internal class SplTokenRepositoryImpl @Inject constructor(
private val mapSplAccountJsonToSplToken: SplResponseAccountJsonMapper,
) : SplTokenRepository {

override suspend fun getTokens(address: String, vault: Vault): List<Coin> {
override suspend fun getTokens(address: String, vault: Vault) =
fetchTokens(address)
.filterNotNull()
.map { (key, coin) ->
createCoin(coin, vault).apply {
saveTokenValueToDatabase(this, key)
}
}

override suspend fun getTokens(address: String) = fetchTokens(address)
.filterNotNull()
.map { it.second }

private suspend fun fetchTokens(address: String): List<Pair<SplTokenResponse, Coin>?> {
val rawSPLTokens = solanaApi.getSPLTokens(address) ?: return emptyList()
val splTokenResponse = rawSPLTokens.map(mapSplAccountJsonToSplToken)
val result = getSplTokensByContractAddress(splTokenResponse.map { it.mint })
return splTokenResponse.mapNotNull { key ->
return splTokenResponse.map { key ->
result.firstOrNull { resultItem -> resultItem.mint == key.mint }
?.let { matchingResult ->
createCoin(matchingResult, key.mint, vault).apply {
saveToDatabase(this, key)
}
key to initCoinData(matchingResult, key.mint)
}
}

}

private suspend fun getSplTokensByContractAddress(contractAddresses: List<String>): List<SplTokenJson> {
Expand All @@ -64,7 +75,7 @@ internal class SplTokenRepositoryImpl @Inject constructor(
return result
}

private suspend fun saveToDatabase(
private suspend fun saveTokenValueToDatabase(
coin: Coin,
splTokenData: SplTokenResponse,
) {
Expand Down Expand Up @@ -104,14 +115,12 @@ internal class SplTokenRepositoryImpl @Inject constructor(
}

private suspend fun createCoin(
tokenResponse: SplTokenJson,
contractAddress: String,
initialCoin: Coin,
vault: Vault
): Coin {
val coin = initCoinData(tokenResponse, contractAddress)
val (derivedAddress, derivedPublicKey) = chainAccountAddressRepository
.getAddress(coin, vault)
return coin.copy(address = derivedAddress, hexPublicKey = derivedPublicKey)
.getAddress(initialCoin, vault)
return initialCoin.copy(address = derivedAddress, hexPublicKey = derivedPublicKey)
}

private fun initCoinData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ interface TokenPriceRepository {
chainId: String,
contractAddress: String,
): BigDecimal

suspend fun getPriceByPriceProviderId(
priceProviderId: String
): BigDecimal
}


Expand Down Expand Up @@ -147,6 +151,12 @@ internal class TokenPriceRepositoryImpl @Inject constructor(
return BigDecimal.ZERO
}

override suspend fun getPriceByPriceProviderId(priceProviderId: String): BigDecimal {
val currency = appCurrencyRepository.currency.first().ticker.lowercase()
val cryptoPrices = coinGeckoApi.getCryptoPrices(listOf(priceProviderId), listOf(currency))
return cryptoPrices.values.firstOrNull()?.values?.firstOrNull() ?: BigDecimal.ZERO
}

private suspend fun savePrices(
tokenIdToPrices: Map<String, CurrencyToPrice>,
currency: String,
Expand Down
Loading

0 comments on commit fc56ba1

Please sign in to comment.