Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AND-5746 [Solana] Improved fee calculation logic #431

Merged
merged 2 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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