Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-17 10:39:33 +03:00
parent c883a1443c
commit 63338bc540
22 changed files with 684 additions and 59 deletions

View file

@ -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'
}

View file

@ -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

View file

@ -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(

View file

@ -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<Amount> {
return BitcoinServer.getFee()
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
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)
)
)
}
}
}
}

View file

@ -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<BitcoinAddressResponse> {
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<BitcoinFee> {
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<UnspentTransaction>)
data class BitcoinFee(
val minimalPerKb: BigDecimal,
val normalPerKb: BigDecimal,
val priorityPerKb: BigDecimal
)

View file

@ -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<BitcoinAddressResponse>
suspend fun getFee(): Result<BitcoinFee>
suspend fun sendTransaction(transaction: String): SimpleResult
}

View file

@ -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<BitcoinAddressResponse> {
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<BitcoinFee> {
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)
}
}
}

View file

@ -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<BitcoinAddressResponse> {
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<BitcoinFee> {
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()

View file

@ -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
}

View file

@ -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
}

View file

@ -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/"

View file

@ -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<BlockchainInfoTransaction>? = 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<BlockchainInfoUtxo>
)
@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
)

View file

@ -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<BlockcypherTxref>? = 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)

View file

@ -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")
}
}

View file

@ -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 {

View file

@ -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<ByteArray>, cardId: String,
callback: (result: TaskEvent<SignResponse>) -> Unit)
suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse>
}
interface FeeProvider {
fun getFee(amount: Amount, source: String, destination: String): List<Amount>
suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>>
}

View file

@ -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
)

View file

@ -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

View file

@ -0,0 +1,49 @@
package com.tangem.blockchain.common.extensions
import kotlinx.coroutines.delay
import java.io.IOException
suspend fun <T> 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 <T: Any> handleRequest(requestFunc: suspend () -> T): Result<T> {
// return try {
// Result.success(requestFunc.invoke())
// } catch (he: HttpException) {
// Result.failure(he)
//// HttpException
//// SocketTimeoutException
//// IOException
// }
//}
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
sealed class SimpleResult {
object Success : SimpleResult()
data class Failure(val error: Throwable?) : SimpleResult()
}

View file

@ -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/"

View file

@ -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
}

View file

@ -8,8 +8,8 @@ class CurrencyWallet(
override val address: String,
override val exploreUrl: String? = null,
override val shareUrl: String? = null,
val pendingTransactions: List<TransactionData> = listOf(),
val balances: MutableList<Amount> = mutableListOf(),
val pendingTransactions: MutableList<TransactionData> = mutableListOf(),
val balances: MutableMap<AmountType, Amount> = mutableMapOf(),
val isTestnet: Boolean = false
) : Wallet, TransactionValidator {