Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-03 18:24:00 +03:00
parent a21044feee
commit 05cc0946f1
8 changed files with 376 additions and 145 deletions

View file

@ -1,16 +0,0 @@
package com.tangem.blockchain.eth
import org.kethereum.crypto.toAddress
import org.kethereum.functions.isValid
import org.kethereum.model.Address
import org.kethereum.model.PublicKey
import org.kethereum.wallet.model.WalletCrypto
object EthereumAddressFactory {
fun makeAddress(cardPublicKey: ByteArray, testNet: Boolean = false): String =
PublicKey(cardPublicKey).toAddress().hex
}
object EthereumAddressValidator {
fun validate(address: String, testNet: Boolean = false): Boolean = Address(address).isValid()
}

View file

@ -1,129 +0,0 @@
package com.tangem.blockchain.eth
import com.tangem.blockchain.common.*
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.tasks.TaskEvent
import org.kethereum.DEFAULT_GAS_LIMIT
import org.kethereum.ETH_IN_WEI
import org.kethereum.crypto.api.ec.ECDSASignature
import org.kethereum.crypto.determineRecId
import org.kethereum.crypto.impl.ec.canonicalise
import org.kethereum.functions.encodeRLP
import org.kethereum.keccakshortcut.keccak
import org.kethereum.model.*
import java.math.BigInteger
class EthereumWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
private val isTestNet: Boolean,
walletConfig: WalletConfig
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain: Blockchain = Blockchain.Ethereum
private val address = blockchain.makeAddress(walletPublicKey)
override var wallet: Wallet = CurrencyWallet(walletConfig, address)
override fun update() {
}
override fun send(transactionData: TransactionData, signer: TransactionSigner) {
val builder = EthereumTransactionBuilder()
val hashes = builder.buildToSign(transactionData, walletPublicKey)
signer.sign(hashes.toTypedArray(), cardId) {
when (it) {
is TaskEvent.Event -> builder.buildToSend(it.data.signature, walletPublicKey)
}
}
}
override fun getFee(amount: Amount, source: String, destination: String): List<Amount> {
val gasPrices: List<Long> = getEthGasPrices()
val gasLimit = getGasLimit(amount).value
val fees = mutableListOf<Amount>()
for (gasPrice in gasPrices) {
val feeValue = (gasPrice * gasLimit).toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
fees.add(Amount(blockchain.currency, feeValue, address, blockchain.decimals))
}
}
}
private class EthereumTransactionBuilder {
var nonce: BigInteger? = null
var transaction: Transaction? = null
var hashToSign: ByteArray? = null
fun buildToSign(transactionData: TransactionData, publicKey: ByteArray): List<ByteArray> {
val from = Address(transactionData.sourceAddress)
val to = Address(transactionData.destinationAddress)
val value = transactionData.amount.value!!
.movePointRight(transactionData.amount.decimals.toInt()).toBigInteger()
val fee = transactionData.fee!!.value!!
.movePointRight(transactionData.fee.decimals.toInt()).toBigInteger()
val gasPrice = fee.divide(DEFAULT_GAS_LIMIT)
transaction = createTransactionWithDefaults(
from = from,
to = to,
value = value,
gasPrice = gasPrice,
gasLimit = DEFAULT_GAS_LIMIT,
nonce = nonce,
chain = ChainId(Chain.Mainnet.id)
)
val transactionToSign = transaction!!
.encodeRLP(SignatureData().apply { v = transaction!!.chain!! })
hashToSign = transactionToSign.keccak()
return listOf(hashToSign!!)
}
fun buildToSend(signature: ByteArray, walletPublicKey: ByteArray): ByteArray {
val r = BigInteger(1, signature.copyOfRange(0, 32))
var s = BigInteger(1, signature.copyOfRange(32, 64))
val ecdsaSignature = ECDSASignature(r, s).canonicalise()
val recId = ecdsaSignature.determineRecId(hashToSign!!, PublicKey(walletPublicKey))
val v = (recId + 27 + Chain.Mainnet.id).toBigInteger() // TODO: where to put chainId?
val signatureData = SignatureData(ecdsaSignature.r, ecdsaSignature.s, v)
return transaction!!.encodeRLP(signatureData)
}
}
private enum class Chain(val id: Long) {
Mainnet(1),
Morden(2),
Ropsten(3),
Rinkeby(4),
Rootstock_mainnet(30),
Rootstock_testnet(31),
Kovan(42),
Ethereum_Classic_mainnet(61),
Ethereum_Classic_testnet(62),
Geth_private_chains(1337),
Matic_Testnet(8995);
}
private enum class GasLimit(val value: Long) {
Default(210000),
Token(60000),
High(300000)
}
private fun getGasLimit(amount: Amount): GasLimit {
return when (amount.currencySymbol) {
"ETH" -> GasLimit.Default
"DGX" -> GasLimit.High
else -> GasLimit.Token
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.blockchain.ethereum
import org.kethereum.crypto.toAddress
import org.kethereum.functions.isValid
import org.kethereum.model.Address
import org.kethereum.model.PublicKey
class EthereumAddressFactory {
companion object {
fun makeAddress(walletPublicKey: ByteArray): String =
PublicKey(walletPublicKey.sliceArray(1..64)).toAddress().hex
}
}
class EthereumAddressValidator {
companion object {
fun validate(address: String): Boolean = Address(address).isValid()
}
}
enum class Chain(val id: Int) {
Mainnet(1),
Morden(2),
Ropsten(3),
Rinkeby(4),
RootstockMainnet(30),
RootstockTestnet(31),
Kovan(42),
EthereumClassicMainnet(61),
EthereumClassicTestnet(62),
Geth_private_chains(1337),
MaticTestnet(8995);
}

View file

@ -0,0 +1,143 @@
package com.tangem.blockchain.ethereum
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.ethereum.network.EthereumResponse
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.common.extensions.toHexString
import com.tangem.tasks.TaskEvent
import org.kethereum.DEFAULT_GAS_LIMIT
import org.kethereum.crypto.api.ec.ECDSASignature
import org.kethereum.crypto.determineRecId
import org.kethereum.crypto.impl.ec.canonicalise
import org.kethereum.functions.encodeRLP
import org.kethereum.keccakshortcut.keccak
import org.kethereum.model.*
import java.math.BigDecimal
import java.math.BigInteger
class EthereumWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
chain: Chain,
walletConfig: WalletConfig
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain: Blockchain = Blockchain.Ethereum
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val builder = EthereumTransactionBuilder(chain)
private val networkManager = EthereumNetworkManager()
private var pendingTxCount = -1L
private var txCount = -1L
override suspend fun update() {
val result = networkManager.getInfo(address, currencyWallet.balances[AmountType.Token]?.address)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)
}
}
private fun updateWallet(data: EthereumResponse) {
currencyWallet.balances[AmountType.Coin]?.value = data.balance
currencyWallet.balances[AmountType.Token]?.value = data.tokenBalance
txCount = data.txCount
pendingTxCount = data.pendingTxCount
if (txCount == pendingTxCount) {
currencyWallet.pendingTransactions.forEach { it.status = TransactionStatus.Confirmed }
} else if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
}
}
private fun updateError(error: Throwable?) {
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val transactionToSign = builder.buildToSign(transactionData, txCount.toBigInteger())
?: return SimpleResult.Failure(Exception("Not enough data"))
when (val signerResponse = signer.sign(transactionToSign.hashes.toTypedArray(), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = builder.buildToSend(signerResponse.data.signature, transactionToSign, walletPublicKey)
return networkManager.sendTransaction(String.format("0x%s", transactionToSend.toHexString()))
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
val result = networkManager.getFee(getGasLimit(amount).value)
when (result) {
is Result.Success -> {
val feeValues: List<BigDecimal> = result.data
return Result.Success(
feeValues.map { Amount(blockchain.currency, it, address, blockchain.decimals) })
}
is Result.Failure -> return result
}
}
}
private class EthereumTransactionBuilder(private val chain: Chain) {
fun buildToSign(transactionData: TransactionData, nonce: BigInteger?): TransactionToSign? {
val amount: BigDecimal = transactionData.amount.value ?: return null
val transactionFee: BigDecimal = transactionData.fee?.value ?: return null
val value = amount.movePointRight(transactionData.amount.decimals.toInt()).toBigInteger()
val fee = transactionFee.movePointRight(transactionData.fee.decimals.toInt()).toBigInteger()
val transaction = createTransactionWithDefaults(
from = Address(transactionData.sourceAddress),
to = Address(transactionData.destinationAddress),
value = value,
gasPrice = fee.divide(DEFAULT_GAS_LIMIT),
gasLimit = DEFAULT_GAS_LIMIT,
nonce = nonce,
chain = ChainId(chain.id.toLong())
)
val hash = transaction.encodeRLP(SignatureData(v = chain.id.toBigInteger())).keccak()
return TransactionToSign(transaction, listOf(hash))
}
fun buildToSend(signature: ByteArray, transactionToSign: TransactionToSign, walletPublicKey: ByteArray): 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(transactionToSign.hashes[0], PublicKey(walletPublicKey.sliceArray(1..64)))
val v = (recId + 27 + 8 + (chain.id * 2)).toBigInteger()
val signatureData = SignatureData(ecdsaSignature.r, ecdsaSignature.s, v)
return transactionToSign.transaction.encodeRLP(signatureData)
}
}
private class TransactionToSign(val transaction: Transaction, val hashes: List<ByteArray>)
enum class GasLimit(val value: Long) {
Default(21000),
Token(60000),
High(300000)
}
private fun getGasLimit(amount: Amount): GasLimit {
return when (amount.currencySymbol) {
Blockchain.Ethereum.currency -> GasLimit.Default
"DGX" -> GasLimit.High
else -> GasLimit.Token
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.blockchain.ethereum.network
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.retryIO
import com.tangem.blockchain.common.network.API_INFURA
import com.tangem.blockchain.common.network.createRetrofitInstance
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.kethereum.ETH_IN_WEI
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
class EthereumNetworkManager {
private val api: InfuraApi by lazy {
createRetrofitInstance(API_INFURA).create(InfuraApi::class.java)
}
private val provider: InfuraProvider by lazy { InfuraProvider(api) }
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val response = retryIO { provider.sendTransaction(transaction) }
if (response.error == null) {
SimpleResult.Success
} else {
SimpleResult.Failure(Exception("Code: ${(response.error.code)}, ${(response.error.message)}"))
}
} catch (error: Exception) {
SimpleResult.Failure(error)
}
}
suspend fun getFee(gasLimit: Long): Result<List<BigDecimal>> {
return try {
Result.Success(
provider.getGasPrice().result!!.parseFee(gasLimit)
)
} catch (error: Exception) {
Result.Failure(error)
}
}
suspend fun getInfo(address: String, contractAddress: String? = null): Result<EthereumResponse> {
return try {
coroutineScope {
val balanceResponse = retryIO { async { provider.getBalance(address) } }
val txCountResponse = retryIO { async { provider.getTxCount(address) } }
val pendingTxCountResponse = retryIO { async { provider.getPendingTxCount(address) } }
var tokenBalanceResponse: Deferred<InfuraResponse>? = null
if (contractAddress != null) {
tokenBalanceResponse = retryIO { async { provider.getTokenBalance(address, contractAddress) } }
}
Result.Success(EthereumResponse(
balanceResponse.await().result!!.parseAmount(),
tokenBalanceResponse?.await()?.result?.parseAmount(),
txCountResponse.await().result?.responseToNumber()?.toLong() ?: 0,
pendingTxCountResponse.await().result?.responseToNumber()?.toLong() ?: 0
))
}
} catch (error: Exception) {
Result.Failure(error)
}
}
private fun String.parseFee(gasLimit: Long): List<BigDecimal> {
val gasPrice = this.responseToNumber().toBigDecimal()
val minFee = gasPrice.multiply(gasLimit.toBigDecimal())
val normalFee = minFee.multiply(BigDecimal(1.2))
val priorityFee = minFee.multiply(BigDecimal(1.5))
return listOf(
minFee.convertFeeToEth(),
normalFee.convertFeeToEth(),
priorityFee.convertFeeToEth()
)
}
private fun String.responseToNumber(): BigInteger = this.substring(2).toBigInteger(16)
private fun String.parseAmount(): BigDecimal =
this.responseToNumber().toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
private fun BigDecimal.convertFeeToEth(): BigDecimal {
return this.divide(ETH_IN_WEI.toBigDecimal())
.setScale(12, Blockchain.Ethereum.roundingMode()).stripTrailingZeros()
}
}
data class EthereumResponse(
val balance: BigDecimal,
val tokenBalance: BigDecimal?,
val txCount: Long,
val pendingTxCount: Long
)

View file

@ -0,0 +1,31 @@
package com.tangem.blockchain.ethereum.network
import com.squareup.moshi.JsonClass
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface InfuraApi {
@Headers("Content-Type: application/json")
@POST("v3/613a0b14833145968b1f656240c7d245")
suspend fun postToInfura(@Body body: InfuraBody?): InfuraResponse
}
@JsonClass(generateAdapter = true)
data class InfuraBody(
val jsonrpc: String = "2.0",
val id: Int = 67,
val method: String? = null,
val params: List<Any> = listOf()
)
data class EthCallParams(private val data: String, private val to: String)
enum class InfuraMethod(val value: String) {
GET_BALANCE("eth_getBalance"),
GET_TRANSACTION_COUNT("eth_getTransactionCount"),
GET_PENDING_COUNT("eth_getPendingCount"),
CALL("eth_call"),
SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
GAS_PRICE("eth_gasPrice")
}

View file

@ -0,0 +1,39 @@
package com.tangem.blockchain.ethereum.network
class InfuraProvider(private val api: InfuraApi) {
suspend fun getBalance(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_BALANCE, address))
suspend fun getTokenBalance(address: String, contractAddress: String) = api.postToInfura(createInfuraBody(InfuraMethod.CALL, address, contractAddress))
suspend fun getTxCount(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_TRANSACTION_COUNT, address))
suspend fun getPendingTxCount(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_PENDING_COUNT, address))
suspend fun getGasPrice() = api.postToInfura(createInfuraBody(InfuraMethod.GAS_PRICE))
suspend fun sendTransaction(transaction: String) = api.postToInfura(createInfuraBody(InfuraMethod.SEND_RAW_TRANSACTION, transaction = transaction))
}
private fun createInfuraBody(
method: InfuraMethod,
address: String? = null,
contractAddress: String? = null,
transaction: String? = null): InfuraBody {
return when (method) {
InfuraMethod.GET_BALANCE ->
InfuraBody(method = InfuraMethod.GET_BALANCE.value, params = listOf(address ?: "", "latest"))
InfuraMethod.GET_TRANSACTION_COUNT ->
InfuraBody(method = InfuraMethod.GET_TRANSACTION_COUNT.value, params = listOf(address ?: "", "latest"))
InfuraMethod.GET_PENDING_COUNT ->
InfuraBody(method = InfuraMethod.GET_TRANSACTION_COUNT.value, params = listOf(address ?: "", "pending"))
InfuraMethod.GAS_PRICE ->
InfuraBody(method = InfuraMethod.GAS_PRICE.value)
InfuraMethod.SEND_RAW_TRANSACTION ->
InfuraBody(method = InfuraMethod.SEND_RAW_TRANSACTION.value, params = listOf(transaction ?: ""))
InfuraMethod.CALL -> {
InfuraBody(
method = InfuraMethod.CALL.value,
params = listOf(EthCallParams(
"0x70a08231000000000000000000000000" + address?.substring(2), contractAddress
?: ""),
"latest"
))
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.blockchain.ethereum.network
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class InfuraResponse(
@Json(name = "jsonrpc")
val jsonrpc: String = "",
@Json(name = "id")
val id: Int? = null,
@Json(name = "result")
val result: String? = null,
@Json(name = "error")
val error: InfuraError? = null
)
@JsonClass(generateAdapter = true)
data class InfuraError(
@Json(name = "code")
val code: Int? = null,
@Json(name = "message")
val message: String? = null
)