Skip to content

Commit

Permalink
Merge pull request #426 from tangem/feature/AND-5244_vechain
Browse files Browse the repository at this point in the history
AND-5244 Added Vechain blockchain
  • Loading branch information
Yoggam1 authored Jan 11, 2024
2 parents b8716e9 + 269ad8f commit ec4bba3
Show file tree
Hide file tree
Showing 24 changed files with 591 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,36 @@
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.common.BlockchainSdkConfig
import com.tangem.blockchain.extensions.letNotBlank
import com.tangem.blockchain.network.createRetrofitInstance

internal class VechainNetworkProvidersBuilder {

fun build(isTestNet: Boolean, config: BlockchainSdkConfig): List<VechainNetworkProvider> {
return buildList {
if (isTestNet) {
add(createVechainNode("https://testnet.vecha.in/"))
add(createVechainNode("https://sync-testnet.vechain.org/"))
add(createVechainNode("https://testnet.veblocks.net/"))
add(createVechainNode("https://testnetc1.vechain.network/"))
} else {
config.nowNodeCredentials?.apiKey.letNotBlank { add(createVechainNode("https://vet.nownodes.io/$it/")) }
add(createVechainNode("https://mainnet.vecha.in/"))
add(createVechainNode("https://sync-mainnet.vechain.org/"))
add(createVechainNode("https://mainnet.veblocks.net/"))
add(createVechainNode("https://mainnetc1.vechain.network/"))
add(createVechainNode("https://us.node.vechain.energy/"))
}
}
}

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?>
}
Loading

0 comments on commit ec4bba3

Please sign in to comment.