Skip to content

Commit

Permalink
AND-5244 Added Vechain blockchain
Browse files Browse the repository at this point in the history
  • Loading branch information
Yoggam1 committed Jan 9, 2024
1 parent 5e49ff0 commit 144d5c5
Show file tree
Hide file tree
Showing 23 changed files with 578 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class TonWalletManager(
is Result.Failure -> updateError(walletInfoResult.error)
is Result.Success -> updateWallet(walletInfoResult.data)
}
wallet.blockchain.getSupportedCurves()
}

override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tangem.blockchain.blockchains.vechain

import java.math.BigDecimal

data class VechainAccountInfo(
val balance: BigDecimal,
val energy: BigDecimal,
val completedTxIds: Set<String>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tangem.blockchain.blockchains.vechain

data class VechainBlockInfo(
val blockId: String,
val blockRef: Long,
val blockNumber: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.tangem.blockchain.blockchains.vechain

import com.tangem.blockchain.blockchains.vechain.network.VechainApi
import com.tangem.blockchain.blockchains.vechain.network.VechainNetworkProvider
import com.tangem.blockchain.network.createRetrofitInstance

internal class VechainNetworkProvidersBuilder {

fun build(isTestNet: Boolean): List<VechainNetworkProvider> {
return buildList {
add(createVechainNode(if (isTestNet) "https://testnet.veblocks.net/" else "https://mainnet.veblocks.net/"))
add(createVechainNode(if (isTestNet) "https://testnet.vecha.in/" else "https://mainnet.vecha.in/"))
add(createVechainNode(if (isTestNet) "https://sync-testnet.vechain.org/" else "https://sync-mainnet.vechain.org/"))
add(createVechainNode(if (isTestNet) "https://vethor-node-test.vechaindev.com/" else "https://vethor-node.vechain.com/"))
}
}

private fun createVechainNode(url: String): VechainNetworkProvider {
val vechainApi = createRetrofitInstance(url).create(VechainApi::class.java)
return VechainNetworkProvider(
baseUrl = url,
api = vechainApi,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package com.tangem.blockchain.blockchains.vechain

import com.google.protobuf.ByteString
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.trustWalletCoinType
import com.tangem.common.extensions.toDecompressedPublicKey
import org.kethereum.crypto.api.ec.ECDSASignature
import org.kethereum.crypto.determineRecId
import org.kethereum.crypto.impl.ec.canonicalise
import org.kethereum.extensions.removeLeadingZero
import org.kethereum.model.PublicKey
import org.kethereum.model.SignatureData
import wallet.core.jni.DataVector
import wallet.core.jni.TransactionCompiler
import wallet.core.jni.proto.Common
import wallet.core.jni.proto.TransactionCompiler.PreSigningOutput
import wallet.core.jni.proto.VeChain
import java.math.BigInteger

class VechainTransactionBuilder(blockchain: Blockchain, private val publicKey: Wallet.PublicKey) {

/**
* “Chain tag is the last byte of the genesis block ID”.
* Testnet blockId: 0x000000000b2bce3c70bc649a02749e8687721b09ed2e15997f466536b20bb127
* Mainnet blockId: 0x00000000851caf3cfdb6e899cf5958bfb1ac3413d346d43539627e6be7ec1b4a
*/
private val chainTag = if (blockchain.isTestnet()) 0x27 else 0x4a
private val coinType = blockchain.trustWalletCoinType

fun constructFee(amount: Amount, destination: String): TransactionFee {
val toClause = buildClause(amount, destination)
val gas = intrinsicGas(toClause)
return TransactionFee.Choosable(
minimum = Fee.Vechain(
amount = Amount(
token = VechainWalletManager.VTHO_TOKEN,
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)
),
gasPriceCoef = 127,
),
priority = Fee.Vechain(
amount = Amount(
token = VechainWalletManager.VTHO_TOKEN,
value = (gas * 2).toBigDecimal().movePointLeft(GAS_TO_VET_DECIMAL)
),
gasPriceCoef = 255,
),
)
}

fun buildForSign(transactionData: TransactionData, blockInfo: VechainBlockInfo, nonce: Long): ByteArray {
val fee = transactionData.fee as? Fee.Vechain ?: throw BlockchainSdkError.FailedToBuildTx
val input =
createSigningInput(transactionData.amount, fee, transactionData.destinationAddress, blockInfo, nonce)
val preImageHashes = TransactionCompiler.preImageHashes(coinType, input.toByteArray())
val preSigningOutput = PreSigningOutput.parseFrom(preImageHashes)

if (preSigningOutput.error != Common.SigningError.OK) {
throw BlockchainSdkError.FailedToBuildTx
}

return preSigningOutput.dataHash.toByteArray()
}

fun buildForSend(
transactionData: TransactionData,
hash: ByteArray,
signature: ByteArray,
blockInfo: VechainBlockInfo,
nonce: Long,
): ByteArray {
val fee = transactionData.fee as? Fee.Vechain ?: throw BlockchainSdkError.FailedToBuildTx
val inputData = createSigningInput(
transactionData.amount,
fee,
transactionData.destinationAddress,
blockInfo,
nonce
)

val publicKeys = DataVector()
publicKeys.add(publicKey.blockchainKey.toDecompressedPublicKey())

val signatures = DataVector()
signatures.add(unmarshalSignature(signature, hash, publicKey))

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

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

return output.encoded.toByteArray()
}

private fun createSigningInput(
amount: Amount,
fee: Fee.Vechain,
destination: String,
blockInfo: VechainBlockInfo,
nonce: Long,
): VeChain.SigningInput {
val clause = buildClause(amount, destination)

return VeChain.SigningInput.newBuilder()
.setChainTag(chainTag)
.setNonce(nonce)
.setBlockRef(blockInfo.blockRef)
.setExpiration(180)
.setGas(intrinsicGas(clause))
.setGasPriceCoef(fee.gasPriceCoef)
.addClauses(clause)
.build()
}

private fun intrinsicGas(clause: VeChain.Clause): Long {
val data = clause.data.toStringUtf8()
val dataCost = calculateDataCost(data)
val vmInvocationCost = if (dataCost > 0) VM_INVOCATION_COST * 2 else 0

return TX_GAS + CLAUSE_GAS + dataCost + vmInvocationCost
}

private fun calculateDataCost(data: String): Long {
return data
.windowed(2, 2)
.sumOf { if (it == "00") Z_GAS else NZ_GAS }
}

private fun buildClause(amount: Amount, destination: String): VeChain.Clause {
val value = amount.value?.movePointRight(amount.decimals)?.toBigInteger() ?: BigInteger.ZERO
return when (val type = amount.type) {
is AmountType.Token -> {
val token = type.token
val data = EthereumUtils.createErc20TransferData(destination, amount)
VeChain.Clause.newBuilder()
.setToBytes(ByteString.copyFromUtf8(token.contractAddress))
.setValue(ByteString.EMPTY)
.setData(ByteString.copyFrom(data))
.build()
}

AmountType.Coin -> {
VeChain.Clause.newBuilder()
.setToBytes(ByteString.copyFromUtf8(destination))
.setValue(ByteString.copyFrom(value.toByteArray()))
.setData(ByteString.EMPTY)
.build()
}

AmountType.Reserve -> error("Not supported")
}
}

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 ecdsaSignature = ECDSASignature(r, s).canonicalise()

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

return signatureData.r.toByteArray().removeLeadingZero() +
signatureData.s.toByteArray().removeLeadingZero() +
signatureData.v.toByteArray()
}

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

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

private const val GAS_TO_VET_DECIMAL = 5
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.tangem.blockchain.blockchains.vechain

import android.util.Log
import com.tangem.blockchain.blockchains.vechain.network.VechainNetworkProvider
import com.tangem.blockchain.blockchains.vechain.network.VechainNetworkService
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.successOr
import com.tangem.blockchain.extensions.toSimpleFailure
import com.tangem.common.CompletionResult

internal class VechainWalletManager(
wallet: Wallet,
networkProviders: List<VechainNetworkProvider>,
) : WalletManager(wallet), TransactionSender {

private val networkService = VechainNetworkService(
networkProviders = networkProviders,
blockchain = wallet.blockchain,
)

override val currentHost: String get() = networkService.host

private val transactionBuilder = VechainTransactionBuilder(
blockchain = wallet.blockchain,
publicKey = wallet.publicKey,
)

override suspend fun updateInternal() {
val pendingTxIds = wallet.recentTransactions.mapNotNullTo(hashSetOf()) { it.hash }
when (val response = networkService.getAccountInfo(wallet.address, pendingTxIds)) {
is Result.Failure -> updateError(response.error)
is Result.Success -> updateWallet(response.data)
}
}

private fun updateWallet(info: VechainAccountInfo) {
wallet.setAmount(Amount(value = info.balance, blockchain = wallet.blockchain))
wallet.addTokenValue(value = info.energy, token = VTHO_TOKEN)
info.completedTxIds.forEach { completedTxId ->
wallet.recentTransactions.find { it.hash == completedTxId }?.let { transactionData ->
transactionData.status = TransactionStatus.Confirmed
}
}
}

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

override suspend fun getFee(amount: Amount, destination: String): Result<TransactionFee> {
return Result.Success(transactionBuilder.constructFee(amount, destination))
}

override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val blockInfo = networkService.getLatestBlock()
.successOr { return SimpleResult.Failure(it.error.toBlockchainSdkError()) }
val nonce = (0..Long.MAX_VALUE).random()
val txToSign = transactionBuilder.buildForSign(transactionData, blockInfo, nonce)
return when (val signatureResult = signer.sign(txToSign, wallet.publicKey)) {
is CompletionResult.Success -> {
val rawTx =
transactionBuilder.buildForSend(
transactionData = transactionData,
hash = txToSign,
signature = signatureResult.data,
blockInfo = blockInfo,
nonce = nonce,
)
when (val sendResult = networkService.sendTransaction(rawTx)) {
is Result.Success -> {
transactionData.hash = sendResult.data.txId
wallet.addOutgoingTransaction(transactionData, hashToLowercase = false)

SimpleResult.Success
}
is Result.Failure -> sendResult.toSimpleFailure()
}
}
is CompletionResult.Failure -> SimpleResult.Failure(signatureResult.error.toBlockchainSdkError())
}
}

internal companion object {
// https://explore.vechain.org/accounts/0x0000000000000000000000000000456e65726779/
internal val VTHO_TOKEN = Token(
id = "vethor-token",
name = "VeThor",
symbol = "VTHO",
contractAddress = "0x0000000000000000000000000000456E65726779",
decimals = 18,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.tangem.blockchain.blockchains.vechain.network

import retrofit2.Response
import retrofit2.http.*

internal interface VechainApi {

@GET("accounts/{address}")
suspend fun getAccount(@Path("address") address: String): VechainGetAccountResponse

@GET("blocks/best")
suspend fun getLatestBlockInfo(): VechainLatestBlockResponse

@POST("transactions")
suspend fun commitTransaction(@Body body: VechainCommitTransactionRequest): VechainCommitTransactionResponse

@GET("transactions/{id}")
suspend fun getTransactionInfo(
@Path("id") transactionId: String,
@Query("pending") pending: Boolean,
): Response<VechainTransactionInfoResponse?>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.tangem.blockchain.blockchains.vechain.network

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

@JsonClass(generateAdapter = true)
internal data class VechainGetAccountResponse(
@Json(name = "balance") val balance: String,
@Json(name = "energy") val energy: String,
)

@JsonClass(generateAdapter = true)
internal data class VechainLatestBlockResponse(
@Json(name = "number") val number: Long,
@Json(name = "id") val blockId: String,
)

@JsonClass(generateAdapter = true)
internal data class VechainCommitTransactionRequest(@Json(name = "raw") val raw: String)

@JsonClass(generateAdapter = true)
internal data class VechainCommitTransactionResponse(@Json(name = "id") val txId: String)

@JsonClass(generateAdapter = true)
internal data class VechainTransactionInfoResponse(@Json(name = "id") val txId: String)
Loading

0 comments on commit 144d5c5

Please sign in to comment.