diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt b/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt index 290f96629..572c4dda7 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt @@ -13,13 +13,18 @@ import com.vultisig.wallet.data.api.models.SplResponseAccountJson import com.vultisig.wallet.data.api.models.SplResponseJson import com.vultisig.wallet.data.api.models.SplTokenJson import com.vultisig.wallet.data.api.utils.postRpc +import com.vultisig.wallet.data.api.models.SplTokenInfo import com.vultisig.wallet.data.models.SplTokenDeserialized import com.vultisig.wallet.data.utils.SplTokenResponseJsonSerializer import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.serialization.json.Json import kotlinx.serialization.json.add import kotlinx.serialization.json.addJsonArray @@ -38,6 +43,7 @@ interface SolanaApi { suspend fun broadcastTransaction(tx: String): String? suspend fun getSPLTokens(walletAddress: String): List? suspend fun getSPLTokensInfo(tokens: List): List + suspend fun getSPLTokensInfo2(tokens: List): List suspend fun getSPLTokenBalance(walletAddress: String, coinAddress: String): String? } @@ -50,8 +56,10 @@ internal class SolanaApiImp @Inject constructor( private val rpcEndpoint = "https://api.mainnet-beta.solana.com" private val rpcEndpoint2 = "https://solana-rpc.publicnode.com" private val splTokensInfoEndpoint = "https://api.solana.fm/v1/tokens" + private val splTokensInfoEndpoint2 = "https://tokens.jup.ag/token" private val solanaRentExemptionEndpoint = "https://api.devnet.solana.com" override suspend fun getBalance(address: String): BigInteger { + val payload = RpcPayload( jsonrpc = "2.0", method = "getBalance", @@ -195,6 +203,20 @@ internal class SolanaApiImp @Inject constructor( } } + override suspend fun getSPLTokensInfo2(tokens: List) = coroutineScope { + tokens.map { token -> + async { + try { + httpClient.get("$splTokensInfoEndpoint2/$token").body() + } catch (e: Exception) { + Timber.tag("SolanaApiImp") + .e("Error getting spl token for $token message : ${e.message}") + null + } + } + }.awaitAll().filterNotNull() + } + override suspend fun getSPLTokens(walletAddress: String): List? { try { val payload = RpcPayload( diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt b/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt index 1ad3f25f1..7307c2550 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt @@ -79,12 +79,29 @@ data class SplTokenJson( val mint: String, ) +@Serializable +data class SplTokenInfo( + @SerialName("address") + val address: String, + @SerialName("name") + val name: String, + @SerialName("symbol") + val symbol: String, + @SerialName("decimals") + val decimals: Int, + @SerialName("logoURI") + val logoURI: String, + @SerialName("extensions") + val extensions: SplExtensionsJson?, +) + + @Serializable data class SplTokenListJson( @SerialName("symbol") val ticker: String, @SerialName("image") - val logo: String, + val logo: String?, @SerialName("extensions") val extensions: SplExtensionsJson?, ) @@ -92,7 +109,7 @@ data class SplTokenListJson( @Serializable data class SplExtensionsJson( @SerialName("coingeckoId") - val coingeckoId: String, + val coingeckoId: String?, ) diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/mappers/DataMappersModule.kt b/data/src/main/kotlin/com/vultisig/wallet/data/mappers/DataMappersModule.kt index 7b3c9ce2d..4d020f587 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/mappers/DataMappersModule.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/mappers/DataMappersModule.kt @@ -45,5 +45,11 @@ internal interface DataMappersModule { fun bindKeysignPayloadProtoMapper( impl: KeysignPayloadProtoMapperImpl ): KeysignPayloadProtoMapper - + + @Binds + @Singleton + fun bindSplTokenJsonFromSplTokenInfoMapper( + impl: SplTokenJsonFromSplTokenInfoImpl + ): SplTokenJsonFromSplTokenInfoMapper + } \ No newline at end of file diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/mappers/SplTokenJsonFromSplTokenInfo.kt b/data/src/main/kotlin/com/vultisig/wallet/data/mappers/SplTokenJsonFromSplTokenInfo.kt new file mode 100644 index 000000000..c34314103 --- /dev/null +++ b/data/src/main/kotlin/com/vultisig/wallet/data/mappers/SplTokenJsonFromSplTokenInfo.kt @@ -0,0 +1,24 @@ +package com.vultisig.wallet.data.mappers + +import com.vultisig.wallet.data.api.models.SplTokenInfo +import com.vultisig.wallet.data.api.models.SplTokenJson +import com.vultisig.wallet.data.api.models.SplTokenListJson +import javax.inject.Inject + + +interface SplTokenJsonFromSplTokenInfoMapper : SuspendMapperFunc + +internal class SplTokenJsonFromSplTokenInfoImpl @Inject constructor( + private val mapKeysignPayload: KeysignPayloadProtoMapper, +) : SplTokenJsonFromSplTokenInfoMapper { + + override suspend fun invoke(from: SplTokenInfo): SplTokenJson = SplTokenJson( + decimals = from.decimals, + tokenList = SplTokenListJson( + logo = from.logoURI, + ticker = from.symbol, + extensions = from.extensions + ), + mint = from.address + ) +} \ No newline at end of file diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/repositories/SPLTokenRepository.kt b/data/src/main/kotlin/com/vultisig/wallet/data/repositories/SPLTokenRepository.kt index 6a6652d3f..0ea003e48 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/repositories/SPLTokenRepository.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/repositories/SPLTokenRepository.kt @@ -3,8 +3,11 @@ package com.vultisig.wallet.data.repositories import com.vultisig.wallet.data.api.SolanaApi import com.vultisig.wallet.data.api.models.SplResponseAccountJson import com.vultisig.wallet.data.api.models.SplTokenJson +import com.vultisig.wallet.data.api.models.SplTokenListJson import com.vultisig.wallet.data.db.dao.TokenValueDao import com.vultisig.wallet.data.db.models.TokenValueEntity +import com.vultisig.wallet.data.mappers.KeysignMessageFromProtoMapper +import com.vultisig.wallet.data.mappers.SplTokenJsonFromSplTokenInfoMapper import com.vultisig.wallet.data.models.Chain import com.vultisig.wallet.data.models.Coin import com.vultisig.wallet.data.models.Vault @@ -27,25 +30,53 @@ internal class SplTokenRepositoryImpl @Inject constructor( private val solanaApi: SolanaApi, private val chainAccountAddressRepository: ChainAccountAddressRepository, private val tokenValueDao: TokenValueDao, -) : SplTokenRepository { + private val mapSplTokenJsonFromSplTokenInfo: SplTokenJsonFromSplTokenInfoMapper, + ) : SplTokenRepository { override suspend fun getTokens(address: String, vault: Vault): List { val rawSPLTokens = solanaApi.getSPLTokens(address) ?: return emptyList() val splTokenResponse = rawSPLTokens.map { processRawSPLToken(it) } - val result = solanaApi.getSPLTokensInfo(splTokenResponse.map { it.mint }) - val splTokens = splTokenResponse.map { key -> - val coin = createCoin(result.first { key.mint == it.mint }, key.mint, vault) - tokenValueDao.insertTokenValue( - TokenValueEntity( - Chain.Solana.id, - coin.address, - coin.ticker, - key.amount.toString() - ) - ) - coin + var result = solanaApi.getSPLTokensInfo(splTokenResponse.map { it.mint }) + if (result.size != splTokenResponse.size) { + //search for missing tokens in splTokenResponse + val missingMints = splTokenResponse.map { it.mint }.filter { mint -> + result.none { it.mint == mint } + } + val mutableResult = result.toMutableList() + solanaApi.getSPLTokensInfo2(missingMints).forEach { + mutableResult.add(mapSplTokenJsonFromSplTokenInfo(it)) + } + result = mutableResult + } + return splTokenResponse.mapNotNull { key -> + result.firstOrNull { resultItem -> resultItem.mint == key.mint } + ?.let { matchingResult -> + createCoin( + matchingResult, + key.mint, + vault + ).apply { + saveToDatabase( + this, + key + ) + } + } } - return splTokens + + } + private suspend fun saveToDatabase( + coin: Coin, + splTokenData: SplTokenResponse, + ) { + tokenValueDao.insertTokenValue( + TokenValueEntity( + Chain.Solana.id, + coin.address, + coin.ticker, + splTokenData.amount.toString() + ) + ) } override suspend fun getBalance(coin: Coin): BigInteger? { @@ -76,7 +107,7 @@ internal class SplTokenRepositoryImpl @Inject constructor( val coin = Coin( chain = Chain.Solana, ticker = tokenResponse.tokenList.ticker, - logo = tokenResponse.tokenList.logo, + logo = tokenResponse.tokenList.logo?: "", decimal = tokenResponse.decimals, priceProviderID = tokenResponse.tokenList.extensions?.coingeckoId ?: "0", contractAddress = contractAddress, diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/utils/Serializer.kt b/data/src/main/kotlin/com/vultisig/wallet/data/utils/Serializer.kt index a08514cad..a396a0c4c 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/utils/Serializer.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/utils/Serializer.kt @@ -44,7 +44,7 @@ object BigIntegerSerializer : KSerializer { encoder.encodeString(value.toString()) override fun deserialize(decoder: Decoder): BigInteger = - BigInteger(decoder.decodeInt().toString()) + BigInteger.valueOf(decoder.decodeLong()) } @Singleton