diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/models/AptosAccountInfo.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/models/AptosAccountInfo.kt new file mode 100644 index 000000000..245f61879 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/models/AptosAccountInfo.kt @@ -0,0 +1,5 @@ +package com.tangem.blockchain.blockchains.aptos.models + +import java.math.BigDecimal + +internal data class AptosAccountInfo(val sequenceNumber: Long, val balance: BigDecimal) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/models/AptosTransactionInfo.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/models/AptosTransactionInfo.kt new file mode 100644 index 000000000..5505f41a9 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/models/AptosTransactionInfo.kt @@ -0,0 +1,28 @@ +package com.tangem.blockchain.blockchains.aptos.models + +/** + * Aptos transaction info + * + * @property sequenceNumber sequence number + * @property publicKey wallet public key + * @property sourceAddress source address + * @property destinationAddress destination address + * @property amount amount + * @property gasUnitPrice gas unit price + * @property maxGasAmount max gas amount + * @property expirationTimestamp expiration timestamp in seconds + * @property hash hash of transaction + * + * @author Andrew Khokhlov on 16/01/2024 + */ +data class AptosTransactionInfo( + val sequenceNumber: Long, + val publicKey: String, + val sourceAddress: String, + val destinationAddress: String, + val amount: Long, + val gasUnitPrice: Long, + val maxGasAmount: Long, + val expirationTimestamp: Long, + val hash: String? = null, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/AptosNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/AptosNetworkProvider.kt new file mode 100644 index 000000000..4f6b76416 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/AptosNetworkProvider.kt @@ -0,0 +1,40 @@ +package com.tangem.blockchain.blockchains.aptos.network + +import com.tangem.blockchain.blockchains.aptos.models.AptosAccountInfo +import com.tangem.blockchain.blockchains.aptos.models.AptosTransactionInfo +import com.tangem.blockchain.common.NetworkProvider +import com.tangem.blockchain.extensions.Result + +/** + * Aptos network provider + * + * @author Andrew Khokhlov on 11/01/2024 + */ +internal interface AptosNetworkProvider : NetworkProvider { + + /** Get account information by [address] */ + suspend fun getAccountInfo(address: String): Result + + /** + * Get normal gas price. + * Prioritizing transactions isn't supports due to difficult scheme of fee calculating. + */ + suspend fun getGasUnitPrice(): Result + + /** + * Calculate gas price unit that required to send transaction + * + * @param transaction unsigned transaction + */ + suspend fun calculateUsedGasPriceUnit(transaction: AptosTransactionInfo): Result + + /** + * Encode transaction in BCS + * + * @param transaction unsigned transaction + */ + suspend fun encodeTransaction(transaction: AptosTransactionInfo): Result + + /** Submit signed transaction [transaction] */ + suspend fun submitTransaction(transaction: AptosTransactionInfo): Result +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/AptosNetworkService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/AptosNetworkService.kt new file mode 100644 index 000000000..98b1056bc --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/AptosNetworkService.kt @@ -0,0 +1,82 @@ +package com.tangem.blockchain.blockchains.aptos.network + +import com.tangem.blockchain.blockchains.aptos.models.AptosAccountInfo +import com.tangem.blockchain.blockchains.aptos.models.AptosTransactionInfo +import com.tangem.blockchain.common.toBlockchainSdkError +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.successOr +import com.tangem.blockchain.network.MultiNetworkProvider + +/** + * Aptos network service. + * Implementation of [AptosNetworkProvider] that wrap network requests in [MultiNetworkProvider]. + * + * @param providers list of [AptosNetworkProvider] + * + * @author Andrew Khokhlov on 11/01/2024 + */ +internal class AptosNetworkService(providers: List) : AptosNetworkProvider { + + override val baseUrl: String + get() = multiJsonRpcProvider.currentProvider.baseUrl + + private val multiJsonRpcProvider = MultiNetworkProvider(providers) + + override suspend fun getAccountInfo(address: String): Result { + return try { + val accountInfo = multiJsonRpcProvider.performRequest(AptosNetworkProvider::getAccountInfo, address) + .successOr { return it } + + Result.Success(accountInfo) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun getGasUnitPrice(): Result { + return try { + val gasUnitPrice = multiJsonRpcProvider.performRequest(AptosNetworkProvider::getGasUnitPrice) + .successOr { return it } + + Result.Success(gasUnitPrice) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun calculateUsedGasPriceUnit(transaction: AptosTransactionInfo): Result { + return try { + val usedGasPriceUnit = multiJsonRpcProvider.performRequest( + request = AptosNetworkProvider::calculateUsedGasPriceUnit, + data = transaction, + ) + .successOr { return it } + + Result.Success(usedGasPriceUnit) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun encodeTransaction(transaction: AptosTransactionInfo): Result { + return try { + val hash = multiJsonRpcProvider.performRequest(AptosNetworkProvider::encodeTransaction, transaction) + .successOr { return it } + + Result.Success(hash) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun submitTransaction(transaction: AptosTransactionInfo): Result { + return try { + val hash = multiJsonRpcProvider.performRequest(AptosNetworkProvider::submitTransaction, transaction) + .successOr { return it } + + Result.Success(hash) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/converter/AptosTransactionConverter.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/converter/AptosTransactionConverter.kt new file mode 100644 index 000000000..fae9fc0a7 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/converter/AptosTransactionConverter.kt @@ -0,0 +1,45 @@ +package com.tangem.blockchain.blockchains.aptos.network.converter + +import com.tangem.blockchain.blockchains.aptos.models.AptosTransactionInfo +import com.tangem.blockchain.blockchains.aptos.network.request.AptosTransactionBody + +/** + * Converter from [AptosTransactionInfo] to [AptosTransactionBody] + * + * @author Andrew Khokhlov on 16/01/2024 + */ +internal object AptosTransactionConverter { + + private const val TRANSFER_PAYLOAD_TYPE = "entry_function_payload" + private const val TRANSFER_PAYLOAD_FUNCTION = "0x1::aptos_account::transfer" + private const val SIGNATURE_TYPE = "ed25519_signature" + + fun convert(from: AptosTransactionInfo): AptosTransactionBody { + return AptosTransactionBody( + sender = from.sourceAddress, + sequenceNumber = from.sequenceNumber.toString(), + expirationTimestamp = from.expirationTimestamp.toString(), + gasUnitPrice = from.gasUnitPrice.toString(), + maxGasAmount = from.maxGasAmount.toString(), + payload = createTransferPayload(from.destinationAddress, from.amount), + signature = from.hash?.let { createSignature(publicKey = from.publicKey, hash = it) }, + ) + } + + private fun createTransferPayload(destination: String, amount: Long): AptosTransactionBody.Payload { + return AptosTransactionBody.Payload( + type = TRANSFER_PAYLOAD_TYPE, + function = TRANSFER_PAYLOAD_FUNCTION, + argumentTypes = listOf(), + arguments = listOf(destination, amount.toString()), + ) + } + + private fun createSignature(publicKey: String, hash: String): AptosTransactionBody.Signature { + return AptosTransactionBody.Signature( + type = SIGNATURE_TYPE, + publicKey = publicKey, + signature = hash, + ) + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/provider/AptosJsonRpcNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/provider/AptosJsonRpcNetworkProvider.kt new file mode 100644 index 000000000..40482aa7d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/aptos/network/provider/AptosJsonRpcNetworkProvider.kt @@ -0,0 +1,107 @@ +package com.tangem.blockchain.blockchains.aptos.network.provider + +import com.tangem.blockchain.blockchains.aptos.models.AptosAccountInfo +import com.tangem.blockchain.blockchains.aptos.models.AptosTransactionInfo +import com.tangem.blockchain.blockchains.aptos.network.AptosApi +import com.tangem.blockchain.blockchains.aptos.network.AptosNetworkProvider +import com.tangem.blockchain.blockchains.aptos.network.converter.AptosTransactionConverter +import com.tangem.blockchain.blockchains.aptos.network.response.AptosResourceBody +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.toBlockchainSdkError +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.network.createRetrofitInstance +import okhttp3.Interceptor + +internal class AptosJsonRpcNetworkProvider( + override val baseUrl: String, + headerInterceptors: List, + private val decimals: Int, +) : AptosNetworkProvider { + + private val api = createRetrofitInstance(baseUrl = baseUrl, headerInterceptors = headerInterceptors) + .create(AptosApi::class.java) + + override suspend fun getAccountInfo(address: String): Result { + return try { + val resources = api.getAccountResources(address) + + val accountResource = resources.getResource()?.account + val coinResource = resources.getResource()?.coinData + + if (accountResource != null && coinResource != null) { + Result.Success( + AptosAccountInfo( + sequenceNumber = accountResource.sequenceNumber.toLong(), + balance = coinResource.coin.value.toBigDecimal().movePointLeft(decimals), + ), + ) + } else { + Result.Failure(BlockchainSdkError.AccountNotFound) + } + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun getGasUnitPrice(): Result { + return try { + val response = api.estimateGasPrice() + + Result.Success(response.normalGasUnitPrice) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun calculateUsedGasPriceUnit(transaction: AptosTransactionInfo): Result { + return try { + val requestBody = AptosTransactionConverter.convert(transaction) + val response = api.simulateTransaction(requestBody).firstOrNull() + + val usedGasUnit = response?.usedGasUnit?.toLongOrNull() + if (response != null && usedGasUnit != null && response.isSuccess) { + Result.Success(usedGasUnit) + } else { + Result.Failure(BlockchainSdkError.FailedToLoadFee) + } + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun encodeTransaction(transaction: AptosTransactionInfo): Result { + return try { + val requestBody = AptosTransactionConverter.convert(transaction) + val hash = api.encodeSubmission(requestBody) + + if (hash.isNotBlank()) { + Result.Success(hash) + } else { + Result.Failure(BlockchainSdkError.FailedToSendException) + } + + Result.Success(hash) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + override suspend fun submitTransaction(transaction: AptosTransactionInfo): Result { + return try { + val requestBody = AptosTransactionConverter.convert(transaction) + val response = api.submitTransaction(requestBody) + + if (response.hash.isNotBlank()) { + Result.Success(response.hash) + } else { + Result.Failure(BlockchainSdkError.FailedToSendException) + } + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + + private inline fun List.getResource(): T? { + return firstNotNullOfOrNull { it as? T } + } +}