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-5815 Add AptosApi #435

Merged
merged 3 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions blockchain/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.13.0'
implementation "com.squareup.moshi:moshi-kotlin:1.13.0"
implementation 'com.squareup.moshi:moshi-adapters:1.9.1'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

есть смысл такую же версию поставить, как и у остальных компонентов

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Готово

kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.tangem.blockchain.blockchains.aptos.network

import com.tangem.blockchain.blockchains.aptos.network.request.TransactionBody
import com.tangem.blockchain.blockchains.aptos.network.response.AptosResource
import com.tangem.blockchain.blockchains.aptos.network.response.EstimateGasPriceResponse
import com.tangem.blockchain.blockchains.aptos.network.response.SimulateTransactionResponse
import com.tangem.blockchain.blockchains.aptos.network.response.SubmitTransactionResponse
import retrofit2.http.*

/**
* Aptos REST API
*
* @see <a href="https://fullnode.mainnet.aptoslabs.com/v1/spec#/">Aptos Node API</a>
*
* @author Andrew Khokhlov on 10/01/2024
*/
internal interface AptosApi {

/**
* Get account resources that contains information about account and balance
*
* @param address account address
*
* @see AptosResource to know more details about kind of resources
*/
@Headers("Content-Type: application/json")
@GET("v1/accounts/{address}/resources")
suspend fun getAccountResources(@Path("address") address: String): List<AptosResource>

/**
* Gives an estimate of the gas unit price required to get a transaction on chain in a reasonable amount of time.
* The gas unit price is the amount that each transaction commits to pay for each unit of gas consumed
* in executing the transaction.
*/
@Headers("Content-Type: application/json")
@GET("v1/estimate_gas_price")
suspend fun estimateGasPrice(): EstimateGasPriceResponse

/**
* Simulate transaction's sending. Use it to estimate the maximum gas units for a submitted transaction.
* Request queries:
* - {estimate_gas_unit_price} - If set to true, the gas unit price in the transaction will be ignored and the
* estimated value will be used
* - {estimate_max_gas_amount} - If set to true, the max gas value in the transaction will be ignored and the
* maximum possible gas will be used
* - {estimate_prioritized_gas_unit_price} - If set to true, the transaction will use a higher price than the
* original estimate
*
* @param body raw transaction data without signing transaction hash
*/
@Headers("Content-Type: application/json")
@POST(
"v1/transactions/simulate?" +
"estimate_gas_unit_price=false&" +
"estimate_max_gas_amount=true&" +
"estimate_prioritized_gas_unit_price=false",
)
suspend fun simulateTransaction(@Body body: TransactionBody): List<SimulateTransactionResponse>

/** Build raw transaction data [body] and encode in BCS */
@Headers("Content-Type: application/json")
@POST("v1/transactions/encode_submission")
suspend fun encodeSubmission(@Body body: TransactionBody): String

/** Submit transaction [body] */
@Headers("Content-Type: application/json")
@POST("v1/transactions")
suspend fun submitTransaction(@Body body: TransactionBody): SubmitTransactionResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.tangem.blockchain.blockchains.aptos.network.request

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

@JsonClass(generateAdapter = true)
internal data class TransactionBody(
@Json(name = "expiration_timestamp_secs") val expirationTimestamp: String,
@Json(name = "gas_unit_price") val gasUnitPrice: String,
@Json(name = "max_gas_amount") val maxGasAmount: String,
@Json(name = "payload") val payload: Payload,
@Json(name = "sender") val sender: String,
@Json(name = "sequence_number") val sequenceNumber: String,
@Json(name = "signature") val signature: Signature? = null,
) {

@JsonClass(generateAdapter = true)
data class Payload(
@Json(name = "type") val type: String,
@Json(name = "function") val function: String,
@Json(name = "type_arguments") val argumentTypes: List<String>,
@Json(name = "arguments") val arguments: List<String>,
)

@JsonClass(generateAdapter = true)
data class Signature(
@Json(name = "type") val type: String,
@Json(name = "public_key") val publicKey: String,
@Json(name = "signature") val signature: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.tangem.blockchain.blockchains.aptos.network.response

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory

internal sealed class AptosResource {

@JsonClass(generateAdapter = true)
data class AccountResource(
@Json(name = "data") val account: AccountData,
) : AptosResource() {

@JsonClass(generateAdapter = true)
data class AccountData(
@Json(name = "sequence_number") val sequenceNumber: String,
)
}

@JsonClass(generateAdapter = true)
data class CoinResource(
@Json(name = "data") val coin: CoinData,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ключевые слова котлина нельзя использовать

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

точно!
я к тому, чтобы при использовании не было coin.coin, потому что внутренняя сущность называется тоже coin, мб подумать чтобы как-то переименовать внешнюю?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошее замечание.
Что думаешь насчет coinData?
Разрабы блокчейна так спроектировали JSON, что очень трудно придумать что-то говорящее)
изображение

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

да, coinData звучит дельно

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Перезалил

) : AptosResource() {

@JsonClass(generateAdapter = true)
data class CoinData(
@Json(name = "coin") val coin: Coin,
) {

@JsonClass(generateAdapter = true)
data class Coin(
@Json(name = "value") val value: String,
)
}
}

object Unknown : AptosResource()

companion object {

fun createPolymorphicJsonAdapterFactory(): PolymorphicJsonAdapterFactory<AptosResource> {
return PolymorphicJsonAdapterFactory
.of(AptosResource::class.java, "type")
.withSubtype(AccountResource::class.java, "0x1::account::Account")
.withSubtype(CoinResource::class.java, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>")
.withDefaultValue(Unknown)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tangem.blockchain.blockchains.aptos.network.response

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

@JsonClass(generateAdapter = true)
internal data class EstimateGasPriceResponse(
@Json(name = "deprioritized_gas_estimate") val minimalGasUnitPrice: Long,
@Json(name = "gas_estimate") val normalGasUnitPrice: Long,
@Json(name = "prioritized_gas_estimate") val priorityGasUnitPrice: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tangem.blockchain.blockchains.aptos.network.response

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

@JsonClass(generateAdapter = true)
internal data class SimulateTransactionResponse(
@Json(name = "gas_used") val usedGasUnit: String,
@Json(name = "sequence_number") val sequenceNumber: String,
@Json(name = "success") val isSuccess: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tangem.blockchain.blockchains.aptos.network.response

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

@JsonClass(generateAdapter = true)
internal data class SubmitTransactionResponse(
@Json(name = "hash") val hash: String,
@Json(name = "sequence_number") val sequenceNumber: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ data class TokenBalanceERC20TokenMethod(
override val data: ByteArray
get() {
val prefixData = prefix.hexToBytes()
val addressData = address.hexToBytes().leftPadToFixedSize(32)
val addressData = address.hexToBytes().leftPadToFixedSize(fixedSize = 32)
return prefixData + addressData
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ data class TransferERC20TokenMethod(
override val data: ByteArray
get() {
val prefixData = prefix.hexToBytes()
val addressData = destination.hexToBytes().leftPadToFixedSize(32)
val amountData = amount.toByteArray().leftPadToFixedSize(32)
val addressData = destination.hexToBytes().leftPadToFixedSize(fixedSize = 32)
val amountData = amount.toByteArray().leftPadToFixedSize(fixedSize = 32)
return prefixData + addressData + amountData
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,21 @@ class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: W
minimum = Fee.Vechain(
amount = Amount(
token = VechainWalletManager.VTHO_TOKEN,
value = gas.toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL)
value = gas.toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL),
),
gasPriceCoef = 0,
),
normal = Fee.Vechain(
amount = Amount(
token = VechainWalletManager.VTHO_TOKEN,
value = (gas * 1.5).toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL)
value = (gas * NORMAL_FEE_COEFFICIENT).toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL),
),
gasPriceCoef = 127,
),
priority = Fee.Vechain(
amount = Amount(
token = VechainWalletManager.VTHO_TOKEN,
value = (gas * 2).toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL)
value = (gas * PRIORITY_FEE_COEFFICIENT).toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL),
),
gasPriceCoef = 255,
),
Expand Down Expand Up @@ -85,7 +85,7 @@ class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: W
fee,
transactionData.destinationAddress,
blockInfo,
nonce
nonce,
)

val publicKeys = DataVector()
Expand All @@ -95,12 +95,15 @@ class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: W
signatures.add(unmarshalSignature(signature, hash, publicKey))

val compileWithSignatures = TransactionCompiler.compileWithSignatures(
coinType, inputData.toByteArray(), signatures, publicKeys
coinType,
inputData.toByteArray(),
signatures,
publicKeys,
)

val output = VeChain.SigningOutput.parseFrom(compileWithSignatures)
if (output.error != Common.SigningError.OK) {
throw IllegalStateException("something went wrong")
error("something went wrong")
}

return output.encoded.toByteArray()
Expand All @@ -119,7 +122,7 @@ class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: W
.setChainTag(chainTag)
.setNonce(nonce)
.setBlockRef(blockInfo.blockRef)
.setExpiration(180)
.setExpiration(EXPIRATION_BLOCKS)
.setGas(intrinsicGas(clause))
.setGasPriceCoef(fee.gasPriceCoef)
.addClauses(clause)
Expand Down Expand Up @@ -166,14 +169,17 @@ class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: W
}

private fun unmarshalSignature(signature: ByteArray, hash: ByteArray, publicKey: Wallet.PublicKey): ByteArray {
val r = BigInteger(1, signature.copyOfRange(0, 32))
val s = BigInteger(1, signature.copyOfRange(32, 64))
val r = BigInteger(1, signature.copyOfRange(fromIndex = 0, toIndex = 32))
val s = BigInteger(1, signature.copyOfRange(fromIndex = 32, toIndex = 64))

val ecdsaSignature = ECDSASignature(r, s).canonicalise()

val recId = ecdsaSignature.determineRecId(
hash,
PublicKey(publicKey.blockchainKey.toDecompressedPublicKey().sliceArray(1..64)),
PublicKey(
publicKey = publicKey.blockchainKey.toDecompressedPublicKey()
.sliceArray(1..PUBLIC_KEY_LENGTH),
),
)
val signatureData = SignatureData(ecdsaSignature.r, ecdsaSignature.s, recId.toBigInteger())

Expand All @@ -183,13 +189,20 @@ class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: W
}

private companion object {
private const val TX_GAS = 5_000L
private const val CLAUSE_GAS = 16_000L
private const val VM_INVOCATION_COST = 15_000L
const val TX_GAS = 5_000L
const val CLAUSE_GAS = 16_000L
const val VM_INVOCATION_COST = 15_000L

const val Z_GAS = 4L
const val NZ_GAS = 68L

const val GAS_TO_VET_DECIMAL = 5

const val NORMAL_FEE_COEFFICIENT = 1.5
const val PRIORITY_FEE_COEFFICIENT = 2

private const val Z_GAS = 4L
private const val NZ_GAS = 68L
const val EXPIRATION_BLOCKS = 180

private const val GAS_TO_VET_DECIMAL = 5
const val PUBLIC_KEY_LENGTH = 64
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ internal class VechainNetworkProvider(
private fun mapBlockInfo(response: VechainLatestBlockResponse): VechainBlockInfo {
val blockRef = response.blockId
.removePrefix("0x")
.substring(0..15)
.substring(range = 0..BLOCK_REFERENCE_LENGTH)
.toLongOrNull(radix = 16)
return VechainBlockInfo(
blockId = response.blockId,
blockRef = blockRef ?: 0,
blockNumber = response.number,
)
}

private companion object {

const val BLOCK_REFERENCE_LENGTH = 15
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ internal class VechainNetworkService(
accountResponse = accountInfoDeferred.await().extractResult(),
pendingTxsInfo = pendingTxsDeferred.awaitAll().map { it.extractResult() },
tokenBalances = tokenBalances,
)
),
)
}
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.tangem.blockchain.externallinkprovider.ExternalLinkProviderFactory
import com.tangem.common.card.EllipticCurve
import com.tangem.crypto.hdWallet.DerivationPath

@Suppress("LargeClass")
enum class Blockchain(
val id: String,
val currency: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ internal object VechainWalletManagerAssembly : WalletManagerAssembly<VechainWall
override fun make(input: WalletManagerAssemblyInput): VechainWalletManager {
return VechainWalletManager(
wallet = input.wallet,
networkProviders = VechainNetworkProvidersBuilder().build(input.wallet.blockchain.isTestnet(), input.config)
networkProviders = VechainNetworkProvidersBuilder().build(
isTestNet = input.wallet.blockchain.isTestnet(),
config = input.config,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.tangem.blockchain.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.blockchain.blockchains.aptos.network.response.AptosResource
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
Expand Down Expand Up @@ -62,6 +63,7 @@ data class Timeout(
internal val moshi: Moshi by lazy {
Moshi.Builder()
.add(BigDecimal::class.java, BigDecimalAdapter)
.add(AptosResource.createPolymorphicJsonAdapterFactory())
.add(KotlinJsonAdapterFactory())
.build()
}
Expand Down
4 changes: 2 additions & 2 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ platform :android do

desc "Runs all the tests"
lane :test do
gradle(
task: "test")
gradle(task: "detekt")
gradle(task: "test")
end

end