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-8610, AND-8611, AND-8612, AND-8613 Casper integration #806

Merged
merged 9 commits into from
Oct 23, 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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.tangem.blockchain.blockchains.casper

import com.tangem.blockchain.blockchains.casper.cashaddr.CasperAddressUtils.checksum
import com.tangem.blockchain.blockchains.casper.utils.CasperAddressUtils.checksum
import com.tangem.blockchain.common.address.AddressService
import com.tangem.blockchain.extensions.isSameCase
import com.tangem.common.card.EllipticCurve
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.tangem.blockchain.blockchains.casper

import com.tangem.blockchain.blockchains.casper.network.CasperNetworkProvider
import com.tangem.blockchain.blockchains.casper.network.provider.CasperRpcNetworkProvider
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.NowNodeCredentials
import com.tangem.blockchain.common.createWithPostfixIfContained
import com.tangem.blockchain.common.logging.AddHeaderInterceptor
import com.tangem.blockchain.common.network.providers.NetworkProvidersBuilder
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.blockchain.extensions.letNotBlank

internal class CasperProvidersBuilder(
override val providerTypes: List<ProviderType>,
private val config: BlockchainSdkConfig,
private val blockchain: Blockchain,
) : NetworkProvidersBuilder<CasperNetworkProvider>() {

override fun createProviders(blockchain: Blockchain): List<CasperNetworkProvider> {
return providerTypes.mapNotNull {
when (it) {
is ProviderType.Public -> createPublicNetworkProvider(baseUrl = it.url)
ProviderType.NowNodes -> createNowNodesNetworkProvider()
else -> null
}
}
}

private fun createPublicNetworkProvider(baseUrl: String): CasperNetworkProvider {
return createWithPostfixIfContained(
baseUrl = baseUrl,
postfixUrl = POSTFIX_URL,
create = { baseUrl, postfixUrl ->
CasperRpcNetworkProvider(
baseUrl = baseUrl,
postfixUrl = postfixUrl,
blockchain = blockchain,
)
},
)
}

private fun createNowNodesNetworkProvider(): CasperNetworkProvider? {
return config.nowNodeCredentials?.apiKey?.letNotBlank {
CasperRpcNetworkProvider(
baseUrl = "https://casper.nownodes.io/",
postfixUrl = POSTFIX_URL,
headerInterceptors = listOf(
AddHeaderInterceptor(mapOf(NowNodeCredentials.headerApiKey to it)),
),
blockchain = blockchain,
)
}
}

private companion object {
const val POSTFIX_URL = "rpc"
Mama1emon marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.tangem.blockchain.blockchains.casper

import com.tangem.blockchain.common.Wallet

/**
* Casper transaction builder
*
* @property wallet wallet
*
*/
@Suppress("UnusedPrivateMember")
internal class CasperTransactionBuilder(private val wallet: Wallet) {

var minReserve = 2.5.toBigDecimal()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.tangem.blockchain.blockchains.casper

import android.util.Log
import com.tangem.blockchain.blockchains.casper.models.CasperBalance
import com.tangem.blockchain.blockchains.casper.network.CasperNetworkProvider
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.transaction.TransactionSendResult
import com.tangem.blockchain.extensions.Result
import java.math.BigDecimal

internal class CasperWalletManager(
wallet: Wallet,
private val networkProvider: CasperNetworkProvider,
private val transactionBuilder: CasperTransactionBuilder,
) : WalletManager(wallet), ReserveAmountProvider {

override val currentHost: String get() = networkProvider.baseUrl
private val blockchain = wallet.blockchain

override suspend fun updateInternal() {
when (val result = networkProvider.getBalance(wallet.address)) {
is Result.Success -> updateWallet(result.data)
is Result.Failure -> updateError(result.error)
}
}

private fun updateWallet(balance: CasperBalance) {
if (balance.value != wallet.amounts[AmountType.Coin]?.value) {
wallet.recentTransactions.clear()
}
wallet.setCoinValue(balance.value)
}

private fun updateError(error: BlockchainError) {
Log.e(this::class.java.simpleName, error.customMessage, error)
if (error is BlockchainSdkError) throw error
}

override suspend fun send(
transactionData: TransactionData,
signer: TransactionSigner,
): Result<TransactionSendResult> {
TODO("Not yet implemented")
}

override suspend fun getFee(amount: Amount, destination: String): Result<TransactionFee> {
return Result.Success(TransactionFee.Single(Fee.Common(Amount(FEE, blockchain))))
}

override fun getReserveAmount(): BigDecimal = transactionBuilder.minReserve
Mama1emon marked this conversation as resolved.
Show resolved Hide resolved

override suspend fun isAccountFunded(destinationAddress: String): Boolean =
networkProvider.getBalance(destinationAddress) is Result.Success

companion object {
// according to Casper Wallet
private val FEE = 0.1.toBigDecimal()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tangem.blockchain.blockchains.casper.models

import java.math.BigDecimal

internal data class CasperBalance(
val value: BigDecimal,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.tangem.blockchain.blockchains.casper.network

import com.tangem.blockchain.blockchains.casper.network.response.CasperRpcResponse
import com.tangem.blockchain.common.JsonRPCRequest
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Url

/**
* Casper RPC API
*
* @see <a href="https://docs.casper.network/developers/json-rpc/">Casper JSON RPC</a>
*
*/
internal interface CasperApi {

/**
* Get data by body [JsonRPCRequest]
*
* @param postfixUrl postfix url for supports base url without '/'
* @param body rpc body
*/
@Headers("Content-Type: application/json")
@POST
suspend fun post(@Url postfixUrl: String, @Body body: JsonRPCRequest): CasperRpcResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tangem.blockchain.blockchains.casper.network

import com.tangem.blockchain.blockchains.casper.models.CasperBalance
import com.tangem.blockchain.common.NetworkProvider
import com.tangem.blockchain.extensions.Result

internal interface CasperNetworkProvider : NetworkProvider {
suspend fun getBalance(address: String): Result<CasperBalance>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tangem.blockchain.blockchains.casper.network

import com.tangem.blockchain.blockchains.casper.models.CasperBalance
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.network.MultiNetworkProvider

internal class CasperNetworkService(
providers: List<CasperNetworkProvider>,
) : CasperNetworkProvider {

override val baseUrl: String
get() = multiJsonRpcProvider.currentProvider.baseUrl

private val multiJsonRpcProvider = MultiNetworkProvider(providers)

override suspend fun getBalance(address: String): Result<CasperBalance> {
return multiJsonRpcProvider.performRequest(CasperNetworkProvider::getBalance, address)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.tangem.blockchain.blockchains.casper.network.converters

import com.tangem.blockchain.blockchains.casper.network.request.CasperQueryBalanceBody

internal object CasperBalanceBodyConverter {

fun convert(address: String): CasperQueryBalanceBody {
return CasperQueryBalanceBody(
purseIdentifier = CasperQueryBalanceBody.PurseIdentifier(
mainPurseUnderPublicKey = address,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.tangem.blockchain.blockchains.casper.network.provider

import com.squareup.moshi.adapter
import com.tangem.blockchain.blockchains.casper.models.CasperBalance
import com.tangem.blockchain.blockchains.casper.network.CasperApi
import com.tangem.blockchain.blockchains.casper.network.CasperNetworkProvider
import com.tangem.blockchain.blockchains.casper.network.request.CasperRpcBodyFactory
import com.tangem.blockchain.blockchains.casper.network.response.CasperRpcResponse
import com.tangem.blockchain.blockchains.casper.network.response.CasperRpcResponseResult
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.JsonRPCRequest
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.network.createRetrofitInstance
import com.tangem.blockchain.network.moshi
import kotlinx.io.IOException
import okhttp3.Interceptor
import java.math.BigDecimal

@OptIn(ExperimentalStdlibApi::class)
internal class CasperRpcNetworkProvider(
override val baseUrl: String,
private val postfixUrl: String,
headerInterceptors: List<Interceptor> = emptyList(),
private val blockchain: Blockchain,
) : CasperNetworkProvider {

private val api = createRetrofitInstance(baseUrl, headerInterceptors).create(CasperApi::class.java)

override suspend fun getBalance(address: String): Result<CasperBalance> = post(
body = CasperRpcBodyFactory.createQueryBalanceBody(address),
onSuccess = { response: CasperRpcResponseResult.Balance ->
CasperBalance(value = BigDecimal(response.balance).movePointLeft(blockchain.decimals()))
},
onFailure = {
// Account is not funded yet
if (it.code == ERROR_CODE_QUERY_FAILED) {
Result.Success(CasperBalance(value = BigDecimal.ZERO))
} else {
Result.Failure(toDefaultError(it))
}
},
)

private suspend inline fun <reified Data, Domain> post(
body: JsonRPCRequest,
onSuccess: (Data) -> Domain,
onFailure: (CasperRpcResponse.Failure) -> Result<Domain>,
): Result<Domain> {
return try {
when (val response = api.post(body = body, postfixUrl = postfixUrl)) {
is CasperRpcResponse.Success -> {
runCatching {
moshi.adapter<Data>().fromJsonValue(response.result)
}.getOrNull()?.let { Result.Success(onSuccess(it)) } ?: Result.Failure(
BlockchainSdkError.UnsupportedOperation(
"Unknown Casper JSON-RPC response result",
),
)
}
is CasperRpcResponse.Failure -> onFailure(response)
}
} catch (e: Exception) {
Result.Failure(e.toBlockchainSdkError())
}
}

private fun toDefaultError(response: CasperRpcResponse.Failure): BlockchainSdkError {
return IOException(response.message).toBlockchainSdkError()
}

companion object {
// https://github.com/casper-network/casper-node/blob/dev/node/src/components/rpc_server/rpcs/error_code.rs
private const val ERROR_CODE_QUERY_FAILED = -32003
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.tangem.blockchain.blockchains.casper.network.request

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
internal data class CasperQueryBalanceBody(
@Json(name = "purse_identifier") val purseIdentifier: PurseIdentifier,
) {
@JsonClass(generateAdapter = true)
data class PurseIdentifier(
@Json(name = "main_purse_under_public_key") val mainPurseUnderPublicKey: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.tangem.blockchain.blockchains.casper.network.request

import com.tangem.blockchain.blockchains.casper.network.converters.CasperBalanceBodyConverter
import com.tangem.blockchain.common.JsonRPCRequest

/**
* Factory for creating [JsonRPCRequest]
*
* @see <a href="https://github.com/casper-network/casper-sidecar/blob/feat-2.0/resources/test/rpc_schema.json">Casper JSON-RPC schema</a>
*
*/
internal object CasperRpcBodyFactory {

/**
* Create query balance body
*
* @param address address
*/
fun createQueryBalanceBody(address: String) = create(
method = CasperRpcMethod.QueryBalance,
params = CasperBalanceBodyConverter.convert(address),
)

private fun create(method: CasperRpcMethod, params: Any?): JsonRPCRequest {
return JsonRPCRequest(
id = "1",
method = method.name,
params = params,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tangem.blockchain.blockchains.casper.network.request

/**
* Casper rpc method
*
* @property name method name
*/
internal sealed class CasperRpcMethod(val name: String) {

data object QueryBalance : CasperRpcMethod(name = "query_balance")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.tangem.blockchain.blockchains.casper.network.response

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/** Casper RPC response */
internal sealed interface CasperRpcResponse {
nzeeei marked this conversation as resolved.
Show resolved Hide resolved

/** Success response with [result] */
data class Success(val result: Any) : CasperRpcResponse

/** Failure response with error [message] */
@JsonClass(generateAdapter = true)
data class Failure(
@Json(name = "message") val message: String,
@Json(name = "code") val code: Int = 0,
) : CasperRpcResponse
}
Loading
Loading