diff --git a/blockchain/src/main/java/com/tangem/blockchain/eth/EthereumAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/eth/EthereumAddress.kt deleted file mode 100644 index da8dc1a54e..0000000000 --- a/blockchain/src/main/java/com/tangem/blockchain/eth/EthereumAddress.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/eth/EthereumWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/eth/EthereumWalletManager.kt deleted file mode 100644 index 374476fd73..0000000000 --- a/blockchain/src/main/java/com/tangem/blockchain/eth/EthereumWalletManager.kt +++ /dev/null @@ -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 { - val gasPrices: List = getEthGasPrices() - val gasLimit = getGasLimit(amount).value - - val fees = mutableListOf() - 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 { - 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 - } -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/ethereum/EthereumAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/ethereum/EthereumAddress.kt new file mode 100644 index 0000000000..200a1ca465 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/ethereum/EthereumAddress.kt @@ -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); +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/ethereum/EthereumWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/ethereum/EthereumWalletManager.kt new file mode 100644 index 0000000000..0e1e5908b8 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/ethereum/EthereumWalletManager.kt @@ -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> { + val result = networkManager.getFee(getGasLimit(amount).value) + when (result) { + is Result.Success -> { + val feeValues: List = 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) + +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 + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/EthereumNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/EthereumNetworkManager.kt new file mode 100644 index 0000000000..c141f51aef --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/EthereumNetworkManager.kt @@ -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> { + return try { + Result.Success( + provider.getGasPrice().result!!.parseFee(gasLimit) + ) + } catch (error: Exception) { + Result.Failure(error) + } + } + + suspend fun getInfo(address: String, contractAddress: String? = null): Result { + 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? = 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 { + 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 +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraApi.kt b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraApi.kt new file mode 100644 index 0000000000..df84fae5ad --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraApi.kt @@ -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 = 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") +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraProvider.kt new file mode 100644 index 0000000000..2a04de5825 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraProvider.kt @@ -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" + )) + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraResponse.kt new file mode 100644 index 0000000000..6e193290b5 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/ethereum/network/InfuraResponse.kt @@ -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 +) + +