Skip to content

Commit

Permalink
Merge pull request #431 from tangem/bugfix/AND-5746_fix_solana_fee_fo…
Browse files Browse the repository at this point in the history
…r_new_tokens

AND-5746 [Solana] Improved fee calculation logic
  • Loading branch information
kozarezvlad authored Jan 17, 2024
2 parents 091dccf + 48fa2c4 commit d0c90c4
Show file tree
Hide file tree
Showing 20 changed files with 676 additions and 490 deletions.
2 changes: 1 addition & 1 deletion .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import java.math.BigDecimal
* Created by Anton Zhilenkov on 31/01/2022.
*/
interface RentProvider {

suspend fun minimalBalanceForRentExemption(): Result<BigDecimal>

suspend fun rentAmount(): BigDecimal
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.tangem.blockchain.blockchains.solana

import com.tangem.blockchain.blockchains.solana.solanaj.core.Transaction
import com.tangem.blockchain.blockchains.solana.solanaj.program.TokenProgramId
import com.tangem.blockchain.blockchains.solana.solanaj.rpc.RpcClient
import com.tangem.blockchain.blockchains.solana.solanaj.core.SolanaTransaction
import com.tangem.blockchain.blockchains.solana.solanaj.model.*
import com.tangem.blockchain.blockchains.solana.solanaj.program.SolanaTokenProgramId
import com.tangem.blockchain.blockchains.solana.solanaj.rpc.SolanaRpcClient
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.BlockchainSdkError.Solana
import com.tangem.blockchain.common.NetworkProvider
import com.tangem.blockchain.extensions.Result
Expand All @@ -12,23 +14,23 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import org.p2p.solanaj.core.PublicKey
import org.p2p.solanaj.rpc.Cluster
import org.p2p.solanaj.rpc.types.*
import org.p2p.solanaj.rpc.types.SignatureStatuses
import org.p2p.solanaj.rpc.types.TokenAccountInfo
import org.p2p.solanaj.rpc.types.config.Commitment
import java.math.BigDecimal

/**
* Created by Anton Zhilenkov on 26/01/2022.
*/
// FIXME: Refactor with wallet-core: https://tangem.atlassian.net/browse/AND-5706
class SolanaNetworkService(
private val provider: RpcClient,
internal class SolanaNetworkService(
private val provider: SolanaRpcClient,
) : NetworkProvider {

override val baseUrl: String = provider.host
val endpoint: String = provider.endpoint

suspend fun getMainAccountInfo(account: PublicKey): Result<SolanaMainAccountInfo> = withContext(Dispatchers.IO) {
val accountInfo = accountInfo(account).successOr { return@withContext it }
val accountInfo = getAccountInfo(account).successOr { return@withContext it }
val tokenAccounts = accountTokensInfo(account).successOr { return@withContext it }

val tokensByMint = tokenAccounts.map {
Expand All @@ -43,7 +45,7 @@ class SolanaNetworkService(
val txsInProgress = getTransactionsInProgressInfo(account).successOr { listOf() }
Result.Success(
SolanaMainAccountInfo(
value = accountInfo.value,
value = accountInfo,
tokensByMint = tokensByMint,
txsInProgress = txsInProgress,
),
Expand Down Expand Up @@ -82,22 +84,60 @@ class SolanaNetworkService(
}
}

private suspend fun accountInfo(account: PublicKey): Result<AccountInfo> = withContext(Dispatchers.IO) {
try {
Result.Success(provider.api.getAccountInfo(account, Commitment.FINALIZED.toMap()))
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
suspend fun getAccountInfoIfExist(account: PublicKey): Result<SolanaAccountInfo.Value> {
return withContext(Dispatchers.IO) {
try {
val accountInfo = getAccountInfo(account)
.successOr { return@withContext it }

if (accountInfo == null) {
Result.Failure(BlockchainSdkError.AccountNotFound)
} else {
Result.Success(accountInfo)
}
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
}
}
}

suspend fun getTokenAccountInfoIfExist(associatedAccount: PublicKey): Result<SolanaSplAccountInfo> {
return withContext(Dispatchers.IO) {
try {
val splAccountInfo = provider.api.getSplTokenAccountInfo(associatedAccount)

if (splAccountInfo.value == null) {
Result.Failure(BlockchainSdkError.AccountNotFound)
} else {
Result.Success(SolanaSplAccountInfo(splAccountInfo.value, associatedAccount))
}
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
}
}
}

private suspend fun getAccountInfo(account: PublicKey): Result<SolanaAccountInfo.Value?> {
return withContext(Dispatchers.IO) {
try {
val params = mapOf("commitment" to Commitment.FINALIZED)
val accountInfo = provider.api.getAccountInfoNew(account, params)

Result.Success(accountInfo.value)
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
}
}
}

private suspend fun accountTokensInfo(account: PublicKey): Result<List<TokenAccountInfo.Value>> =
withContext(Dispatchers.IO) {
try {
val tokensAccountsInfoDefault = async {
tokenAccountInfo(account, TokenProgramId.TOKEN.value)
tokenAccountInfo(account, SolanaTokenProgramId.TOKEN.value)
}
val tokensAccountsInfo2022 = async {
tokenAccountInfo(account, TokenProgramId.TOKEN_2022.value)
tokenAccountInfo(account, SolanaTokenProgramId.TOKEN_2022.value)
}

val tokensAccountsInfo = awaitAll(tokensAccountsInfoDefault, tokensAccountsInfo2022)
Expand All @@ -111,71 +151,33 @@ class SolanaNetworkService(
}

private fun tokenAccountInfo(account: PublicKey, programId: PublicKey): TokenAccountInfo {
val params = mutableMapOf<String, Any>("programId" to programId)
.apply { addCommitment(Commitment.RECENT) }
val params = buildMap {
put("programId", programId)
put("commitment", Commitment.RECENT.value)
}

return provider.api.getTokenAccountsByOwner(account, params, mutableMapOf())
}

private suspend fun splAccountInfo(associatedAccount: PublicKey): Result<SolanaSplAccountInfo> =
withContext(Dispatchers.IO) {
try {
val splAccountInfo = provider.api.getSplTokenAccountInfo(associatedAccount)
Result.Success(SolanaSplAccountInfo(splAccountInfo.value, associatedAccount))
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
}
}

suspend fun getFees(): Result<FeesInfo> = withContext(Dispatchers.IO) {
suspend fun getFeeForMessage(transaction: SolanaTransaction): Result<FeeInfo> = withContext(Dispatchers.IO) {
try {
val params = provider.api.getFees(Commitment.FINALIZED)
val params = provider.api.getFeeForMessage(transaction, Commitment.PROCESSED)
Result.Success(params)
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
}
}

suspend fun isAccountExist(account: PublicKey): Result<Boolean> = withContext(Dispatchers.IO) {
val info = accountInfo(account).successOr { return@withContext it }
Result.Success(info.accountExist)
}

suspend fun isTokenAccountExist(associatedAccount: PublicKey): Result<Boolean> {
return withContext(Dispatchers.IO) {
val info = splAccountInfo(associatedAccount).successOr { return@withContext it }

Result.Success(info.accountExist)
}
}

fun mainAccountCreationFee(): BigDecimal = accountRentFeeByEpoch(1)

suspend fun tokenAccountCreationFee(): Result<BigDecimal> = minimalBalanceForRentExemption(BUFFER_LENGTH)

internal fun accountRentFeeByEpoch(numberOfEpochs: Int = 1): BigDecimal {
// https://docs.solana.com/developing/programming-model/accounts#calculation-of-rent
// result in lamports
val minimumAccountSizeInBytes = BigDecimal(MIN_ACCOUNT_SIZE)

val rentInLamportPerByteEpoch = BigDecimal(determineRentPerByteEpoch(provider.endpoint))
val rentFeePerEpoch = minimumAccountSizeInBytes
.multiply(numberOfEpochs.toBigDecimal())
.multiply(rentInLamportPerByteEpoch)

return rentFeePerEpoch
}

suspend fun minimalBalanceForRentExemption(dataLength: Long = 0): Result<BigDecimal> = withContext(Dispatchers.IO) {
suspend fun minimalBalanceForRentExemption(dataLength: Long): Result<Long> = withContext(Dispatchers.IO) {
try {
val rent = provider.api.getMinimumBalanceForRentExemption(dataLength)
Result.Success(rent.toBigDecimal())
Result.Success(rent)
} catch (ex: Exception) {
Result.Failure(Solana.Api(ex))
}
}

suspend fun sendTransaction(signedTransaction: Transaction): Result<String> = withContext(Dispatchers.IO) {
suspend fun sendTransaction(signedTransaction: SolanaTransaction): Result<String> = withContext(Dispatchers.IO) {
try {
val result = provider.api.sendSignedTransaction(signedTransaction)
Result.Success(result)
Expand All @@ -191,68 +193,4 @@ class SolanaNetworkService(
Result.Failure(Solana.Api(ex))
}
}

private fun determineRentPerByteEpoch(endpoint: String): Double = when (endpoint) {
Cluster.TESTNET.endpoint -> RENT_PER_BYTE_EPOCH
Cluster.DEVNET.endpoint -> RENT_PER_BYTE_EPOCH_DEV_NET
else -> RENT_PER_BYTE_EPOCH
}

companion object {
const val MIN_ACCOUNT_SIZE = 128L
const val RENT_PER_BYTE_EPOCH = 19.055441478439427
const val RENT_PER_BYTE_EPOCH_DEV_NET = 0.359375
const val BUFFER_LENGTH = 165L
}
}

private val AccountInfo.accountExist
get() = value != null

data class SolanaMainAccountInfo(
val value: AccountInfo.Value?,
val tokensByMint: Map<String, SolanaTokenAccountInfo>,
val txsInProgress: List<TransactionInfo>,
) {
val balance: Long
get() = value?.lamports ?: 0L

val accountExist: Boolean
get() = value != null

val requireValue: AccountInfo.Value
get() = value!!
}

data class SolanaSplAccountInfo(
val value: TokenResultObjects.Value?,
val associatedPubK: PublicKey,
) {
val accountExist: Boolean
get() = value != null

val requireValue: TokenResultObjects.Value
get() = value!!
}

data class SolanaTokenAccountInfo(
val value: TokenAccountInfo.Value,
val address: String,
val mint: String,
val uiAmount: BigDecimal, // in SOL
)

data class TransactionInfo(
val signature: String,
val fee: Long, // in lamports
val instructions: List<TransactionResult.Instruction>,
)

private fun MutableMap<String, Any>.addCommitment(commitment: Commitment): MutableMap<String, Any> {
this["commitment"] = commitment
return this
}

private fun Commitment.toMap(): MutableMap<String, Any> {
return mutableMapOf("commitment" to this)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.tangem.blockchain.blockchains.solana

import com.tangem.blockchain.blockchains.solana.solanaj.rpc.RpcClient
import com.tangem.blockchain.blockchains.solana.solanaj.rpc.SolanaRpcClient
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.GetBlockCredentials
import com.tangem.blockchain.common.NowNodeCredentials
Expand All @@ -12,9 +12,9 @@ import org.p2p.solanaj.rpc.Cluster
/**
* Created by Anton Zhilenkov on 06.02.2023.
*/
class SolanaRpcClientBuilder {
internal class SolanaRpcClientBuilder {

fun build(isTestnet: Boolean, config: BlockchainSdkConfig): List<RpcClient> {
fun build(isTestnet: Boolean, config: BlockchainSdkConfig): List<SolanaRpcClient> {
return if (isTestnet) {
listOf(devNet())
} else {
Expand All @@ -26,41 +26,41 @@ class SolanaRpcClientBuilder {
}
}

private fun mainNet(): RpcClient = RpcClient(Cluster.MAINNET.endpoint)
private fun mainNet(): SolanaRpcClient = SolanaRpcClient(Cluster.MAINNET.endpoint)

private fun devNet(): RpcClient = RpcClient(Cluster.DEVNET.endpoint)
private fun devNet(): SolanaRpcClient = SolanaRpcClient(Cluster.DEVNET.endpoint)

@Suppress("UnusedPrivateMember")
private fun testNet(): RpcClient = RpcClient(Cluster.TESTNET.endpoint)
private fun testNet(): SolanaRpcClient = SolanaRpcClient(Cluster.TESTNET.endpoint)

private fun quickNode(cred: QuickNodeCredentials): RpcClient {
private fun quickNode(cred: QuickNodeCredentials): SolanaRpcClient {
val host = "https://${cred.subdomain}.solana-mainnet.discover.quiknode.pro/${cred.apiKey}"
return RpcClient(host)
return SolanaRpcClient(host)
}

private fun nowNode(cred: NowNodeCredentials): RpcClient {
return RpcClient(
private fun nowNode(cred: NowNodeCredentials): SolanaRpcClient {
return SolanaRpcClient(
host = "https://sol.nownodes.io",
httpInterceptors = createInterceptor(NowNodeCredentials.headerApiKey, cred.apiKey),
)
}

// contains old data about 7 hours
@Suppress("UnusedPrivateMember")
private fun ankr(): RpcClient {
return RpcClient("https://rpc.ankr.com/solana")
private fun ankr(): SolanaRpcClient {
return SolanaRpcClient(host = "https://rpc.ankr.com/solana")
}

// unstable
@Suppress("UnusedPrivateMember")
private fun getBlock(cred: GetBlockCredentials): RpcClient {
return RpcClient(host = "https://go.getblock.io/${cred.solana}")
private fun getBlock(cred: GetBlockCredentials): SolanaRpcClient {
return SolanaRpcClient(host = "https://go.getblock.io/${cred.solana}")
}

// zero uptime
@Suppress("UnusedPrivateMember")
private fun projectserum(): RpcClient {
return RpcClient(host = "https://solana-api.projectserum.com")
private fun projectserum(): SolanaRpcClient {
return SolanaRpcClient(host = "https://solana-api.projectserum.com")
}

private fun createInterceptor(key: String, value: String): List<Interceptor> {
Expand Down
Loading

0 comments on commit d0c90c4

Please sign in to comment.