From 63338bc5407320e8084cc2cf00797e55e1cf946d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Jan 2020 10:39:33 +0300 Subject: [PATCH] Updated on 2026-08-14 --- blockchain/build.gradle | 25 ++++- .../blockchain/bitcoin/BitcoinAddress.kt | 3 +- .../bitcoin/BitcoinTransactionBuilder.kt | 4 +- .../bitcoin/BitcoinWalletManager.kt | 82 ++++++++++++--- .../bitcoin/network/BitcoinNetworkManager.kt | 99 +++++++++++++++++++ .../bitcoin/network/BitcoinProvider.kt | 10 ++ .../bitcoin/network/BlockchainInfoProvider.kt | 78 +++++++++++++++ .../bitcoin/network/BlockcypherProvider.kt | 83 ++++++++++++++++ .../bitcoin/network/api/BlockchainInfoApi.kt | 18 ++++ .../bitcoin/network/api/BlockcypherApi.kt | 38 +++++++ .../bitcoin/network/api/EstimateFeeApi.kt | 16 +++ .../response/BlockchainInfoResponse.kt | 43 ++++++++ .../network/response/BlockcypherResponse.kt | 52 ++++++++++ .../tangem/blockchain/common/Blockchain.kt | 28 +++--- .../com/tangem/blockchain/common/Wallet.kt | 9 +- .../tangem/blockchain/common/WalletManager.kt | 13 +-- .../blockchain/common/WalletManagerFactory.kt | 23 +++-- .../common/extensions/BitInteger.kt | 11 +++ .../common/extensions/Coroutines.kt | 49 +++++++++ .../common/network/RetrofitBuilder.kt | 44 +++++++++ .../tangem/blockchain/extensions/ByteArray.kt | 11 --- .../blockchain/wallets/CurrencyWallet.kt | 4 +- 22 files changed, 684 insertions(+), 59 deletions(-) create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinNetworkManager.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinProvider.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockchainInfoProvider.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockcypherProvider.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockchainInfoApi.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockcypherApi.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/EstimateFeeApi.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockchainInfoResponse.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockcypherResponse.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/common/extensions/BitInteger.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/common/extensions/Coroutines.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt delete mode 100644 blockchain/src/main/java/com/tangem/blockchain/extensions/ByteArray.kt diff --git a/blockchain/build.gradle b/blockchain/build.gradle index 2bd12af9e7..09ba1f5663 100644 --- a/blockchain/build.gradle +++ b/blockchain/build.gradle @@ -1,9 +1,10 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { compileSdkVersion 29 - buildToolsVersion "29.0.0" + buildToolsVersion "29.0.2" defaultConfig { @@ -23,6 +24,10 @@ android { } } + packagingOptions { + exclude 'org.slf4j:slf4j-jdk14:1.7.25' + } + } dependencies { @@ -34,15 +39,29 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' - implementation 'com.squareup.retrofit2:retrofit:2.6.2' + implementation 'com.squareup.retrofit2:retrofit:2.7.0' implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' implementation 'com.squareup.moshi:moshi:1.9.2' + kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2") + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' + + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.3" implementation 'org.bitcoinj:bitcoinj-core:0.15.2' - implementation 'com.github.komputing:kethereum:0.79.5' implementation 'com.github.stellar:java-stellar-sdk:0.11.0' + ext.kethereum_version = '0.79.5' + implementation "com.github.walleth.kethereum:functions:$kethereum_version" + implementation "com.github.walleth.kethereum:keccak_shortcut:$kethereum_version" + implementation "com.github.walleth.kethereum:wallet:$kethereum_version" + implementation "com.github.walleth.kethereum:crypto_impl_spongycastle:$kethereum_version" + implementation "com.github.walleth.kethereum:crypto:$kethereum_version" + implementation "com.github.walleth.kethereum:crypto_api:$kethereum_version" + implementation "com.github.walleth.kethereum:model:$kethereum_version" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' + testImplementation "com.google.truth:truth:1.0" androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt index 54fabba6b5..b533321c6a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinAddress.kt @@ -1,6 +1,7 @@ package com.tangem.blockchain.bitcoin -import com.tangem.blockchain.extensions.calculateRipemd160 + +import com.tangem.common.extensions.calculateRipemd160 import com.tangem.common.extensions.calculateSha256 import org.bitcoinj.core.AddressFormatException import org.bitcoinj.core.Base58 diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt index 1d99ce8db0..ce6271b458 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinTransactionBuilder.kt @@ -1,7 +1,7 @@ package com.tangem.blockchain.bitcoin import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.extensions.toCanonicalised +import com.tangem.blockchain.common.extensions.toCanonicalised import org.bitcoinj.core.* import org.bitcoinj.crypto.TransactionSignature import org.bitcoinj.script.Script @@ -17,7 +17,7 @@ class BitcoinTransactionBuilder(private val testNet: Boolean) { fun calculateChange(transactionData: TransactionData) : Long { val fullAmount = unspentOutputs.map { it.amount }.sum() - return fullAmount - (transactionData.amount.value!!.toLong() + (transactionData.fee?.value!!.toLong())) + return fullAmount - (transactionData.amount.value!!.toLong() + (transactionData.fee?.value?.toLong() ?: 0)) } fun buildToSign( diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt index d7da71a6fc..34a5398a21 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/BitcoinWalletManager.kt @@ -1,10 +1,17 @@ package com.tangem.blockchain.bitcoin +import android.util.Log +import com.tangem.blockchain.bitcoin.network.BitcoinAddressResponse +import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult import com.tangem.blockchain.wallets.CurrencyWallet +import com.tangem.common.extensions.toHexString import com.tangem.tasks.TaskEvent import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Transaction +import java.math.BigDecimal class BitcoinWalletManager( private val cardId: String, @@ -18,36 +25,89 @@ class BitcoinWalletManager( override val blockchain = if (isTestNet) Blockchain.BitcoinTestnet else Blockchain.Bitcoin private val address = blockchain.makeAddress(walletPublicKey) - override var wallet: Wallet = CurrencyWallet(walletConfig, address) + private val currencyWallet = CurrencyWallet(walletConfig, address) + override var wallet: Wallet = currencyWallet private val transactionBuilder = BitcoinTransactionBuilder(isTestNet) + private val networkManager = BitcoinNetworkManager(isTestNet) + override suspend fun update() { + val response = networkManager.getInfo(address) + when (response) { + is Result.Success -> updateWallet(response.data) + is Result.Failure -> updateError(response.error) + } + } - override fun update() { - transactionBuilder.unspentOutputs = listOf() + private fun updateWallet(response: BitcoinAddressResponse) { + Log.d(this::class.java.simpleName, "Balance is ${response.balance.toString()}") + currencyWallet.balances[AmountType.Coin]?.value = response.balance.toBigDecimal() + transactionBuilder.unspentOutputs = response.unspentTransactions + if (response.hasUnconfirmed) { + if (currencyWallet.pendingTransactions.isEmpty()) { + currencyWallet.pendingTransactions.add(TransactionData( + Amount(blockchain.currency, decimals = blockchain.decimals), + null, + "unknown", + currencyWallet.address)) + } else { + currencyWallet.pendingTransactions.clear() + } + } + } + + private fun updateError(error: Throwable?) { + Log.e(this::class.java.simpleName, error?.message ?: "") } - override fun getEstimateSize(transactionData: TransactionData): Int { + override suspend fun getEstimateSize(transactionData: TransactionData): Int { val transaction: Transaction = transactionData.toBitcoinJTransaction( NetworkParameters.fromID(NetworkParameters.ID_MAINNET), transactionBuilder.unspentOutputs, transactionBuilder.calculateChange(transactionData) - ) + ) var size: Int = transaction.unsafeBitcoinSerialize().size size += transaction.inputs.sumBy { 130 } return size } - override fun send(transactionData: TransactionData, signer: TransactionSigner) { + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { val hashes = transactionBuilder.buildToSign(transactionData) - signer.sign(hashes.toTypedArray(), cardId) { - when (it) { - is TaskEvent.Event -> transactionBuilder.buildToSend(it.data.signature, walletPublicKey) + when (val signerResponse = signer.sign(hashes.toTypedArray(), cardId)) { + is TaskEvent.Event -> { + val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature, walletPublicKey) + return networkManager.sendTransaction(transactionToSend.toHexString()) } + is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error) } } - override fun getFee(amount: Amount, source: String, destination: String): List { - return BitcoinServer.getFee() + override suspend fun getFee(amount: Amount, source: String, destination: String): Result> { + when (val result = networkManager.getFee()) { + is Result.Failure -> return result + is Result.Success -> { + val bytesInKb = BigDecimal(1024) + val size = getEstimateSize(TransactionData(amount, null, source, destination)).toBigDecimal() + val minFee = result.data.minimalPerKb / bytesInKb * size + val normalFee = result.data.normalPerKb / bytesInKb * size + val priorityFee = result.data.priorityPerKb / bytesInKb * size + return Result.Success( + listOf( + Amount(blockchain.currency, + minFee, + source, + blockchain.decimals), + Amount(blockchain.currency, + normalFee, + source, + blockchain.decimals), + Amount(blockchain.currency, + priorityFee, + source, + blockchain.decimals) + ) + ) + } + } } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinNetworkManager.kt new file mode 100644 index 0000000000..56e9a70ad0 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinNetworkManager.kt @@ -0,0 +1,99 @@ +package com.tangem.blockchain.bitcoin.network + +import com.tangem.blockchain.bitcoin.UnspentTransaction +import com.tangem.blockchain.bitcoin.network.api.BlockchainInfoApi +import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi +import com.tangem.blockchain.bitcoin.network.api.EstimatefeeApi +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.common.network.API_BLOCKCHAIN_INFO +import com.tangem.blockchain.common.network.API_BLOCKCYPHER +import com.tangem.blockchain.common.network.API_ESTIMATEFEE +import com.tangem.blockchain.common.network.createRetrofitInstance +import retrofit2.HttpException +import java.io.IOException +import java.math.BigDecimal + + +class BitcoinNetworkManager(private val isTestNet: Boolean) : BitcoinProvider { + + private val blockcypherProvider by lazy { + val api = createRetrofitInstance(API_BLOCKCYPHER) + .create(BlockcypherApi::class.java) + BlockcypherProvider(api, isTestNet) + } + + private val blockchainInfoProvider by lazy { + val api = createRetrofitInstance(API_BLOCKCHAIN_INFO) + .create(BlockchainInfoApi::class.java) + val estimateFeeApi = createRetrofitInstance(API_ESTIMATEFEE) + .create(EstimatefeeApi::class.java) + BlockchainInfoProvider(api, estimateFeeApi) + } + + private var bitcoinProvider: BitcoinProvider = blockchainInfoProvider + + private fun changeProvider() { + bitcoinProvider = if (bitcoinProvider == blockchainInfoProvider) { + blockcypherProvider + } else { + blockchainInfoProvider + } + } + + override suspend fun getInfo(address: String): Result { + val result = bitcoinProvider.getInfo(address) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return bitcoinProvider.getInfo(address) + } else { + return result + } + } + } + } + + override suspend fun getFee(): Result { + val result = bitcoinProvider.getFee() + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return bitcoinProvider.getFee() + } else { + return result + } + } + } + } + + override suspend fun sendTransaction(transaction: String): SimpleResult { + val result = bitcoinProvider.sendTransaction(transaction) + when (result) { + is SimpleResult.Success -> return result + is SimpleResult.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return bitcoinProvider.sendTransaction(transaction) + } else { + return result + } + } + } + } +} + +data class BitcoinAddressResponse( + val balance: Long, + val hasUnconfirmed: Boolean, + val unspentTransactions: List) + +data class BitcoinFee( + val minimalPerKb: BigDecimal, + val normalPerKb: BigDecimal, + val priorityPerKb: BigDecimal +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinProvider.kt new file mode 100644 index 0000000000..e82fb2f67c --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BitcoinProvider.kt @@ -0,0 +1,10 @@ +package com.tangem.blockchain.bitcoin.network + +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult + +interface BitcoinProvider { + suspend fun getInfo(address: String): Result + suspend fun getFee(): Result + suspend fun sendTransaction(transaction: String): SimpleResult +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockchainInfoProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockchainInfoProvider.kt new file mode 100644 index 0000000000..18902089d1 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockchainInfoProvider.kt @@ -0,0 +1,78 @@ +package com.tangem.blockchain.bitcoin.network + +import com.tangem.blockchain.bitcoin.UnspentTransaction +import com.tangem.blockchain.bitcoin.network.api.BlockchainInfoApi +import com.tangem.blockchain.bitcoin.network.api.EstimatefeeApi +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.common.extensions.retryIO +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + + +class BlockchainInfoProvider( + private val blockchainApi: BlockchainInfoApi, + private val estimatefeeApi: EstimatefeeApi +) : BitcoinProvider { + + override suspend fun getInfo(address: String): Result { + return try { + coroutineScope { + val addressDeferred = retryIO { async { blockchainApi.getAddress(address) } } + val unspentsDeferred = retryIO { async { blockchainApi.getUnspents(address) } } + + val addressData = addressDeferred.await() + val unspents = unspentsDeferred.await() + val unconfinedTransactions = addressData.transactions?.find { it.blockHeight == 0L } != null + + val bitcoinUnspents = unspents.unspentOutputs.map { + UnspentTransaction( + it.amount!!, + it.outputIndex!!.toLong(), + it.hash!!.toByteArray(), + it.outputScript!!.toByteArray()) + } + + Result.Success( + BitcoinAddressResponse( + addressData.finalBalance + ?: 0L, unconfinedTransactions, bitcoinUnspents)) + } + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + override suspend fun getFee(): Result { + return try { + coroutineScope { + val minFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeeMinimal() } } + val normalFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeeNormal() } } + val priorityFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeePriority() } } + + val minFee = minFeeDeferred.await() + val normalFee = normalFeeDeferred.await() + val priorityFee = priorityFeeDeferred.await() + + Result.Success(BitcoinFee( + minFee.toBigDecimal(), + normalFee.toBigDecimal(), + priorityFee.toBigDecimal())) + } + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + + override suspend fun sendTransaction(transaction: String): SimpleResult { + return try { + retryIO { blockchainApi.sendTransaction(transaction) } + SimpleResult.Success + } catch (exception: Exception) { + SimpleResult.Failure(exception) + } + } + + +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockcypherProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockcypherProvider.kt new file mode 100644 index 0000000000..024046d1a8 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/BlockcypherProvider.kt @@ -0,0 +1,83 @@ +package com.tangem.blockchain.bitcoin.network + +import com.tangem.blockchain.bitcoin.UnspentTransaction +import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi +import com.tangem.blockchain.bitcoin.network.response.BlockcypherBody +import com.tangem.blockchain.bitcoin.network.response.BlockcypherFee +import com.tangem.blockchain.bitcoin.network.response.BlockcypherResponse +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.common.extensions.retryIO + +class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : BitcoinProvider { + + private val blockchain = "btc" + + private val network = if (isTestNet) { + BlockcypherNetwork.Test.network + } else { + BlockcypherNetwork.Main.network + } + + override suspend fun getInfo(address: String): Result { + try { + val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) } + val unspents = addressData.txrefs!!.map { + UnspentTransaction( + it.amount!!, + it.outputIndex!!.toLong(), + it.hash!!.toByteArray(), + it.outputScript!!.toByteArray() + ) + } + return Result.Success(BitcoinAddressResponse( + addressData.balance!!, + addressData.unconfirmedBalance != 0L, + unspents)) + + } catch (error: Exception) { + return Result.Failure(error) + } + } + + override suspend fun getFee(): Result { + try { + val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) } + return Result.Success( + BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal() / satoshiInBtc, + receivedFee.normalFeePerKb!!.toBigDecimal() / satoshiInBtc, + receivedFee.priorityFeePerKb!!.toBigDecimal() / satoshiInBtc) + ) + } catch (error: Exception) { + return Result.Failure(error) + } + } + + override suspend fun sendTransaction(transaction: String): SimpleResult { + try { + retryIO { + api.sendTransaction( + blockchain, network, BlockcypherBody(transaction), BlockcypherToken.getToken()) + } + return SimpleResult.Success + } catch (error: Exception) { + return SimpleResult.Failure(error) + } + } +} + +private object BlockcypherToken { + private val tokens = listOf( + "aa8184b0e0894b88a5688e01b3dc1e82", + "56c4ca23c6484c8f8864c32fde4def8d", + "66a8a37c5e9d4d2c9bb191acfe7f93aa") + + fun getToken(): String = tokens.random() +} + +private enum class BlockcypherNetwork(val network: String) { + Main("main"), + Test("test3") +} + +val satoshiInBtc = 100000000.toBigDecimal() \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockchainInfoApi.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockchainInfoApi.kt new file mode 100644 index 0000000000..1b43631d5a --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockchainInfoApi.kt @@ -0,0 +1,18 @@ +package com.tangem.blockchain.bitcoin.network.api + +import com.tangem.blockchain.bitcoin.network.response.BlockchainInfoAddress +import com.tangem.blockchain.bitcoin.network.response.BlockchainInfoUnspents +import okhttp3.ResponseBody +import retrofit2.http.* + +interface BlockchainInfoApi { + @GET("rawaddr/{address}?limit=5") + suspend fun getAddress(@Path("address") address: String): BlockchainInfoAddress + + @GET("unspent") + suspend fun getUnspents(@Query("active") address: String): BlockchainInfoUnspents + + @FormUrlEncoded + @POST("pushtx") + suspend fun sendTransaction(@Field("tx") transaction: String): ResponseBody +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockcypherApi.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockcypherApi.kt new file mode 100644 index 0000000000..2984d42a1d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/BlockcypherApi.kt @@ -0,0 +1,38 @@ +package com.tangem.blockchain.bitcoin.network.api + +import com.tangem.blockchain.bitcoin.network.response.BlockcypherBody +import com.tangem.blockchain.bitcoin.network.response.BlockcypherFee +import com.tangem.blockchain.bitcoin.network.response.BlockcypherResponse +import com.tangem.blockchain.bitcoin.network.response.BlockcypherTx +import retrofit2.http.* + +interface BlockcypherApi { + @GET("v1/{blockchain}/{network}") + fun getFee( + @Path("blockchain") blockchain: String, + @Path("network") network: String + ): BlockcypherFee + + @GET("v1/{blockchain}/{network}/addrs/{address}?unspentOnly=true&includeScript=true") + fun getAddressData( + @Path("blockchain") blockchain: String, + @Path("network") network: String, + @Path("address") address: String + ): BlockcypherResponse + + @GET("v1/{blockchain}/{network}/txs/{txHash}?includeHex=true") + fun getTransactions( + @Path("blockchain") blockchain: String, + @Path("network") network: String, + @Path("txHash") txHash: String + ): BlockcypherTx + + @Headers("Content-Type: application/json") + @POST("v1/{blockchain}/{network}/txs/push") + fun sendTransaction( + @Path("blockchain") blockchain: String, + @Path("network") network: String, + @Body blockcypherBody: BlockcypherBody, + @Query("token") token: String + ): BlockcypherResponse +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/EstimateFeeApi.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/EstimateFeeApi.kt new file mode 100644 index 0000000000..22e95304db --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/api/EstimateFeeApi.kt @@ -0,0 +1,16 @@ +package com.tangem.blockchain.bitcoin.network.api + +import retrofit2.http.GET + +interface EstimatefeeApi { + @GET(ESTIMATE_FEE_URL + "n/2") + suspend fun getEstimateFeePriority(): String + + @GET(ESTIMATE_FEE_URL + "n/3") + suspend fun getEstimateFeeNormal(): String + + @GET(ESTIMATE_FEE_URL + "n/6") + suspend fun getEstimateFeeMinimal(): String +} + +const val ESTIMATE_FEE_URL = "https://estimatefee.com/" \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockchainInfoResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockchainInfoResponse.kt new file mode 100644 index 0000000000..015d867985 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockchainInfoResponse.kt @@ -0,0 +1,43 @@ +package com.tangem.blockchain.bitcoin.network.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class BlockchainInfoAddress( + @Json(name = "final_balance") + val finalBalance: Long? = null, + + @Json(name = "txs") + val transactions: List? = null +) + +@JsonClass(generateAdapter = true) +data class BlockchainInfoTransaction( + @Json(name = "hash") + val hash: String? = null, + + @Json(name = "block_height") + val blockHeight: Long? = null +) + +@JsonClass(generateAdapter = true) +data class BlockchainInfoUnspents( + @Json(name = "unspent_outputs") + val unspentOutputs: List +) + +@JsonClass(generateAdapter = true) +data class BlockchainInfoUtxo( + @Json(name = "tx_hash_big_endian") + val hash: String? = null, + + @Json(name = "tx_output_n") + val outputIndex: Int? = null, + + @Json(name = "value") + val amount: Long? = null, + + @Json(name = "script") + val outputScript: String? = null +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockcypherResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockcypherResponse.kt new file mode 100644 index 0000000000..eca6fd1bfb --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/bitcoin/network/response/BlockcypherResponse.kt @@ -0,0 +1,52 @@ +package com.tangem.blockchain.bitcoin.network.response + +import com.squareup.moshi.Json + +data class BlockcypherResponse( + @Json(name = "address") + var address: String? = null, + + @Json(name = "balance") + var balance: Long? = null, + + @Json(name = "unconfirmed_balance") + var unconfirmedBalance: Long? = null, + + @Json(name = "txrefs") + var txrefs: List? = null +) + +data class BlockcypherTxref( + @Json(name = "tx_hash") + var hash: String? = null, + + @Json(name = "tx_output_n") + var outputIndex: Int? = null, + + @Json(name = "value") + var amount: Long? = null, + + @Json(name = "confirmations") + var confirmations: Long? = null, + + @Json(name = "script") + var outputScript: String? = null +) + +data class BlockcypherTx( + @Json(name = "hex") + var hex: String? = null +) + +data class BlockcypherFee( + @Json(name = "low_fee_per_kb") + var minFeePerKb: Long? = null, + + @Json(name = "medium_fee_per_kb") + var normalFeePerKb: Long? = null, + + @Json(name = "high_fee_per_kb") + var priorityFeePerKb: Long? = null +) + +data class BlockcypherBody(val tx: String) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index 065f19f1d8..c048f2faaf 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.bitcoin.BitcoinAddressValidator import com.tangem.blockchain.eth.EthereumAddressFactory import com.tangem.blockchain.eth.EthereumAddressValidator import com.tangem.blockchain.stellar.StellarAddressFactory -import com.tangem.blockchain.stellar.StellarAddressValidator import java.math.BigDecimal enum class Blockchain( @@ -13,10 +12,11 @@ enum class Blockchain( val currency: String, val decimals: Byte, val fullName: String, - val pendingTransactionTimeout: Int) { + val pendingTransactionTimeout: Int +) { Unknown("", "", 0, "", 0), - Bitcoin("", "", 8, "", 0), - BitcoinTestnet("", "", 8, "", 0), + Bitcoin("btc", "", 8, "", 0), + BitcoinTestnet("btc", "", 8, "", 0), Ethereum("", "", 18, "", 0), Rootstock("", "", 18, "", 0), Cardano("", "", 6, "", 0), @@ -36,11 +36,12 @@ enum class Blockchain( Bitcoin -> BitcoinAddressFactory.makeAddress(cardPublicKey) BitcoinTestnet -> BitcoinAddressFactory.makeAddress(cardPublicKey, testNet = true) Ethereum -> EthereumAddressFactory.makeAddress(cardPublicKey) - Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey) - Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey) - Ripple -> RippleAddressFactory.makeAddress(cardPublicKey) - Binance -> BinanceAddressFactory.makeAddress(cardPublicKey) +// Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey) +// Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey) +// Ripple -> RippleAddressFactory.makeAddress(cardPublicKey) +// Binance -> BinanceAddressFactory.makeAddress(cardPublicKey) Stellar -> StellarAddressFactory.makeAddress(cardPublicKey) + else -> throw Exception("unsupported blockchain") } } @@ -50,11 +51,12 @@ enum class Blockchain( Bitcoin -> BitcoinAddressValidator.validate(address) BitcoinTestnet -> BitcoinAddressValidator.validate(address, testNet = true) Ethereum -> EthereumAddressValidator.validate(address) - Rootstock -> RootstockAddressValidator.validate(address) - Cardano -> CardanoAddressValidator.validate(address) - Ripple -> RippleAddressValidator.validate(address) - Binance -> BinanceAddressValidator.validate(address) - Stellar -> StellarAddressValidator.validate(address) +// Rootstock -> RootstockAddressValidator.validate(address) +// Cardano -> CardanoAddressValidator.validate(address) +// Ripple -> RippleAddressValidator.validate(address) +// Binance -> BinanceAddressValidator.validate(address) +// Stellar -> StellarAddressValidator.validate(address) + else -> throw Exception("unsupported blockchain") } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Wallet.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Wallet.kt index caadc0d15d..fc025b619c 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Wallet.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Wallet.kt @@ -19,8 +19,8 @@ class WalletConfig( data class Amount( val currencySymbol: String, - val value: BigDecimal?, - val address: String, + var value: BigDecimal? = null, + val address: String? = null, val decimals: Byte, val type: AmountType = AmountType.Coin ) @@ -29,11 +29,14 @@ data class TransactionData( val amount: Amount, val fee: Amount?, val sourceAddress: String, - val destinationAddress: String + val destinationAddress: String, + var status: TransactionStatus = TransactionStatus.Uncomfirmed ) enum class AmountType { Coin, Token, Reserve } +enum class TransactionStatus {Confirmed, Uncomfirmed} + enum class ValidationError { WrongAmount, WrongFee, WrongTotal } interface TransactionValidator { diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt index 610319380f..6fe40b1bf7 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManager.kt @@ -1,5 +1,7 @@ package com.tangem.blockchain.common +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult import com.tangem.commands.SignResponse import com.tangem.tasks.TaskEvent @@ -7,22 +9,21 @@ interface WalletManager { var wallet: Wallet val blockchain: Blockchain - fun update() + suspend fun update() } interface TransactionEstimator { - fun getEstimateSize(transactionData: TransactionData): Int + suspend fun getEstimateSize(transactionData: TransactionData): Int } interface TransactionSender { - fun send(transactionData: TransactionData, signer: TransactionSigner) + suspend fun send(transactionData: TransactionData, signer: TransactionSigner) : SimpleResult } interface TransactionSigner { - fun sign(hashes: Array, cardId: String, - callback: (result: TaskEvent) -> Unit) + suspend fun sign(hashes: Array, cardId: String): TaskEvent } interface FeeProvider { - fun getFee(amount: Amount, source: String, destination: String): List + suspend fun getFee(amount: Amount, source: String, destination: String): Result> } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt index 33e0a0edb0..090c15eb1a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt @@ -1,6 +1,7 @@ package com.tangem.blockchain.common import com.tangem.blockchain.bitcoin.BitcoinWalletManager +import com.tangem.blockchain.eth.Chain import com.tangem.blockchain.eth.EthereumWalletManager import com.tangem.blockchain.stellar.StellarWalletManager import com.tangem.commands.Card @@ -8,8 +9,8 @@ import com.tangem.commands.Card object WalletManagerFactory { fun makeWalletManager(card: Card): WalletManager? { - val walletPublicKey = card.walletPublicKey ?: return null - val blockchainName = card.cardData?.blockchainName ?: return null + val walletPublicKey: ByteArray = card.walletPublicKey ?: return null + val blockchainName: String = card.cardData?.blockchainName ?: return null when { blockchainName.contains("btc") || blockchainName.contains("bitcoin") -> { @@ -17,13 +18,19 @@ object WalletManagerFactory { cardId = card.cardId, walletPublicKey = walletPublicKey, walletConfig = WalletConfig(true, true), - isTestNet = blockchainName.contains("test")) + isTestNet = isTestNet(blockchainName)) } blockchainName.contains("eth") -> { + val chain = if (isTestNet(blockchainName)) { + Chain.EthereumClassicTestnet + } else { + Chain.EthereumClassicMainnet + } return EthereumWalletManager( cardId = card.cardId, walletPublicKey = walletPublicKey, - walletConfig = WalletConfig(true, true)) + walletConfig = WalletConfig(true, true), + chain = chain) } blockchainName.contains("xlm") -> { val token = getToken(card) @@ -32,17 +39,19 @@ object WalletManagerFactory { walletPublicKey = walletPublicKey, walletConfig = WalletConfig(true, token == null), token = token, - isTestNet = blockchainName.contains("test")) + isTestNet = isTestNet(blockchainName)) } else -> return null } } + private fun isTestNet(blockchainName: String) = blockchainName.contains("test") + private fun getToken(card: Card): Token? { val symbol = card.cardData?.tokenSymbol ?: return null val contractAddress = card.cardData?.tokenContractAddress ?: return null val decimals = card.cardData?.tokenDecimal ?: return null - return Token(symbol, contractAddress, decimals) + return Token(symbol, contractAddress, decimals.toByte()) } } @@ -50,5 +59,5 @@ object WalletManagerFactory { data class Token( val symbol: String, val contractAddress: String, - val decimals: Int + val decimals: Byte ) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/extensions/BitInteger.kt b/blockchain/src/main/java/com/tangem/blockchain/common/extensions/BitInteger.kt new file mode 100644 index 0000000000..6ac80e1bbd --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/common/extensions/BitInteger.kt @@ -0,0 +1,11 @@ +package com.tangem.blockchain.common.extensions + +import org.bitcoinj.core.ECKey +import java.math.BigInteger + +fun BigInteger.toCanonicalised(): BigInteger { + if (!this.isCanonical()) ECKey.CURVE.n - this + return this +} + +fun BigInteger.isCanonical(): Boolean = this <= ECKey.HALF_CURVE_ORDER \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/extensions/Coroutines.kt b/blockchain/src/main/java/com/tangem/blockchain/common/extensions/Coroutines.kt new file mode 100644 index 0000000000..2dfc103e1a --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/common/extensions/Coroutines.kt @@ -0,0 +1,49 @@ +package com.tangem.blockchain.common.extensions + +import kotlinx.coroutines.delay +import java.io.IOException + +suspend fun retryIO( + times: Int = Int.MAX_VALUE, + initialDelay: Long = 100, + maxDelay: Long = 1000, + factor: Double = 2.0, + block: suspend () -> T +): T { + var currentDelay = initialDelay + repeat(times - 1) { + try { + return block() + } catch (e: IOException) { + + + } + delay(currentDelay) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + } + return block() // last attempt +} + + +//suspend fun handleRequest(requestFunc: suspend () -> T): Result { +// return try { +// Result.success(requestFunc.invoke()) +// } catch (he: HttpException) { +// Result.failure(he) +//// HttpException +//// SocketTimeoutException +//// IOException +// } +//} + + +sealed class Result { + data class Success(val data: T) : Result() + data class Failure(val error: Throwable?) : Result() +} + +sealed class SimpleResult { + object Success : SimpleResult() + data class Failure(val error: Throwable?) : SimpleResult() +} + diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt new file mode 100644 index 0000000000..90e05fa592 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt @@ -0,0 +1,44 @@ +package com.tangem.blockchain.common.network + +import com.tangem.blockchain.BuildConfig +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +private val okHttpClient: OkHttpClient by lazy { + OkHttpClient.Builder().apply { + if (BuildConfig.DEBUG) addInterceptor(createHttpLoggingInterceptor()) + }.build() +} + +private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + return logging +} + + +fun createRetrofitInstance(baseUrl: String): Retrofit = + Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(MoshiConverterFactory.create()) + .client(okHttpClient) + .build() + + +const val API_TANGEM = "https://verify.tangem.com/" +const val API_COINMARKETCAP = "https://pro-api.coinmarketcap.com/" +const val API_INFURA = "https://mainnet.infura.io/" +const val API_SOCHAIN_V2 = "https://chain.so/" +const val API_ESTIMATEFEE = "https://estimatefee.com/" +const val API_UPDATE_VERSION = "https://raw.githubusercontent.com/" +const val API_ROOTSTOCK = "https://public-node.rsk.co/" +const val API_BLOCKCYPHER = "https://api.blockcypher.com/" +const val API_BINANCE = "https://dex.binance.org/" +const val API_BINANCE_TESTNET = "https://testnet-dex.binance.org/" +const val API_MATIC_TESTNET = "https://testnet2.matic.network/" +const val API_STELLAR = "https://horizon.stellar.org/" +const val API_STELLAR_RESERVE = "https://horizon.sui.li/" +const val API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/" +const val API_BLOCKCHAIN_INFO = "https://blockchain.info/" \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/extensions/ByteArray.kt b/blockchain/src/main/java/com/tangem/blockchain/extensions/ByteArray.kt deleted file mode 100644 index 2619ca0184..0000000000 --- a/blockchain/src/main/java/com/tangem/blockchain/extensions/ByteArray.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.blockchain.extensions - -import org.spongycastle.crypto.digests.RIPEMD160Digest - -fun ByteArray.calculateRipemd160(): ByteArray { - val digest = RIPEMD160Digest() - digest.update(this, 0, this.size) - val out = ByteArray(20) - digest.doFinal(out, 0) - return out -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/wallets/CurrencyWallet.kt b/blockchain/src/main/java/com/tangem/blockchain/wallets/CurrencyWallet.kt index a1e2bb544a..218964d51c 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/wallets/CurrencyWallet.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/wallets/CurrencyWallet.kt @@ -8,8 +8,8 @@ class CurrencyWallet( override val address: String, override val exploreUrl: String? = null, override val shareUrl: String? = null, - val pendingTransactions: List = listOf(), - val balances: MutableList = mutableListOf(), + val pendingTransactions: MutableList = mutableListOf(), + val balances: MutableMap = mutableMapOf(), val isTestnet: Boolean = false ) : Wallet, TransactionValidator {