-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
578 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/VechainAccountInfo.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) |
7 changes: 7 additions & 0 deletions
7
blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/VechainBlockInfo.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
25 changes: 25 additions & 0 deletions
25
...src/main/java/com/tangem/blockchain/blockchains/vechain/VechainNetworkProvidersBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
195 changes: 195 additions & 0 deletions
195
...hain/src/main/java/com/tangem/blockchain/blockchains/vechain/VechainTransactionBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
97 changes: 97 additions & 0 deletions
97
blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/VechainWalletManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/network/VechainApi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?> | ||
} |
25 changes: 25 additions & 0 deletions
25
...n/src/main/java/com/tangem/blockchain/blockchains/vechain/network/VechainNetworkModels.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.