Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-11 09:40:26 +00:00
commit faece7f6b8
53 changed files with 4671 additions and 2 deletions

1
.idea/gradle.xml generated
View file

@ -11,6 +11,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/blockchain" />
<option value="$PROJECT_DIR$/server-android" />
<option value="$PROJECT_DIR$/tangem-card-old" />
<option value="$PROJECT_DIR$/tangem-core" />

1
blockchain/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

70
blockchain/build.gradle Normal file
View file

@ -0,0 +1,70 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
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.stellar:java-stellar-sdk:0.13.0'
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.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"
implementation 'com.github.komputing.khex:core:1.0.0-RC6'
implementation 'com.github.komputing.khex:extensions:1.0.0-RC6'
implementation 'co.nstant.in:cbor:0.8'
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'
}

21
blockchain/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1 @@
<manifest package="com.tangem.blockchain" />

View file

@ -0,0 +1,72 @@
package com.tangem.blockchain.bitcoin
import com.tangem.common.extensions.calculateRipemd160
import com.tangem.common.extensions.calculateSha256
import org.bitcoinj.core.AddressFormatException
import org.bitcoinj.core.Base58
import org.bitcoinj.core.SegwitAddress
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import java.security.MessageDigest
class BitcoinAddressFactory {
companion object {
fun makeAddress(walletPublicKey: ByteArray, testNet: Boolean = false): String {
val netSelectionByte = if (testNet) 0x6f.toByte() else 0x00.toByte()
val hash1 = walletPublicKey.calculateSha256().calculateRipemd160()
val hash2 = byteArrayOf(netSelectionByte).plus(hash1).calculateSha256().calculateSha256()
val result = byteArrayOf(netSelectionByte) + hash1 + hash2[0] + hash2[1] + hash2[2] + hash2[3]
return Base58.encode(result)
}
}
}
class BitcoinAddressValidator {
companion object {
private const val firstLetters = "123nm"
private const val firstLettersNonTestNet = "13"
fun validate(address: String, testNet: Boolean = false): Boolean {
if (firstLetters.contains(address.first())) {
if (testNet && firstLettersNonTestNet.contains(address.first())) return false
if (address.length !in 26..35) return false
val decoded = address.decodeBase58() ?: return false
val hash = recursiveSha256(decoded, 0, 21, 2)
return hash.sliceArray(0..3).contentEquals(decoded.sliceArray(21..24))
} else {
return validateSegwitAddress(address, testNet)
}
}
private fun recursiveSha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray {
if (recursion == 0) return data
val md = MessageDigest.getInstance("SHA-256")
md.update(data.sliceArray(start until start + len))
return recursiveSha256(md.digest(), 0, 32, recursion - 1)
}
private fun String.decodeBase58(): ByteArray? {
return try {
Base58.decode(this)
} catch (exception: AddressFormatException) {
null
}
}
private fun validateSegwitAddress(address: String, testNet: Boolean): Boolean {
return try {
if (testNet) {
SegwitAddress.fromBech32(TestNet3Params(), address)
true
} else {
SegwitAddress.fromBech32(MainNetParams(), address)
true
}
} catch (e: Exception) {
false
}
}
}
}

View file

@ -0,0 +1,99 @@
package com.tangem.blockchain.bitcoin
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.toCanonicalised
import org.bitcoinj.core.*
import org.bitcoinj.crypto.TransactionSignature
import org.bitcoinj.script.Script
import org.bitcoinj.script.ScriptBuilder
import java.math.BigDecimal
import java.math.BigInteger
class BitcoinTransactionBuilder(private val testNet: Boolean) {
private lateinit var transaction: Transaction
private var networkParameters: NetworkParameters? = null
var unspentOutputs: List<UnspentTransaction>? = null
fun buildToSign(
transactionData: TransactionData): Result<List<ByteArray>> {
if (unspentOutputs == null) return Result.Failure(Exception("Currently there's an unconfirmed transaction"))
val change: BigDecimal = calculateChange(transactionData)
networkParameters = if (testNet) {
NetworkParameters.fromID(NetworkParameters.ID_TESTNET)
} else {
NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
}
transaction = transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs!!, change)
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }
for (input in transaction.inputs) {
val index = input.index
hashesForSign[index] = transaction.hashForSignature(index, input.scriptBytes, Transaction.SigHash.ALL, false).bytes
}
return Result.Success(hashesForSign)
}
private fun calculateChange(transactionData: TransactionData): BigDecimal {
val fullAmount = unspentOutputs!!.map { it.amount }.reduce { acc, number -> acc + number }
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.value
?: 0.toBigDecimal()))
}
fun buildToSend(signedTransaction: ByteArray, publicKey: ByteArray): ByteArray {
for (index in transaction.inputs.indices) {
transaction.inputs[index].scriptSig = createScript(index, signedTransaction, publicKey)
}
return transaction.bitcoinSerialize()
}
private fun createScript(index: Int, signedTransaction: ByteArray, publicKey: ByteArray): Script {
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64))
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64))
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
val signature = TransactionSignature(r, canonicalS)
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
}
fun getEstimateSize(transactionData: TransactionData, walletPublicKey: ByteArray): Result<Int> {
val buildTransactionResult = buildToSign(transactionData)
when (buildTransactionResult) {
is Result.Failure -> return buildTransactionResult
is Result.Success -> {
val hashes = buildTransactionResult.data
val finalTransaction = buildToSend(ByteArray(64 * hashes.size) { 1 }, walletPublicKey)
return Result.Success(finalTransaction.size)
}
}
}
}
internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkParameters?,
unspentOutputs: List<UnspentTransaction>,
change: BigDecimal): Transaction {
val transaction = Transaction(networkParameters)
for (utxo in unspentOutputs) {
transaction.addInput(Sha256Hash.wrap(utxo.hash), utxo.outputIndex, Script(utxo.outputScript))
}
transaction.addOutput(
Coin.parseCoin(this.amount.value!!.toPlainString()),
Address.fromString(networkParameters, this.destinationAddress))
if (change != 0.toBigDecimal()) {
transaction.addOutput(
Coin.parseCoin(change.toPlainString()),
Address.fromString(networkParameters,
this.sourceAddress))
}
return transaction
}
class UnspentTransaction(
val amount: BigDecimal,
val outputIndex: Long,
val hash: ByteArray,
val outputScript: ByteArray
)

View file

@ -0,0 +1,109 @@
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.bitcoin.network.BitcoinNetworkManager.Companion.SATOSHI_IN_BTC
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 java.math.BigDecimal
class BitcoinWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig,
isTestNet: Boolean = false
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = if (isTestNet) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
private val address = blockchain.makeAddress(walletPublicKey)
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)
}
}
private fun updateWallet(response: BitcoinAddressResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
currencyWallet.balances[AmountType.Coin]?.value = response.balance
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 suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val buildTransactionResult = transactionBuilder.buildToSign(transactionData)
when (buildTransactionResult) {
is Result.Failure -> return SimpleResult.Failure(buildTransactionResult.error)
is Result.Success -> {
when (val signerResponse = signer.sign(buildTransactionResult.data.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 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 sizeResult = transactionBuilder.getEstimateSize(
TransactionData(amount,
Amount(1.toBigDecimal().divide(SATOSHI_IN_BTC), blockchain),
address, destination),
walletPublicKey
)
when (sizeResult) {
is Result.Failure -> return sizeResult
is Result.Success -> {
val transactionSize = sizeResult.data.toBigDecimal()
val minFee = result.data.minimalPerKb.calculateFee(transactionSize)
val normalFee = result.data.normalPerKb.calculateFee(transactionSize)
val priorityFee = result.data.priorityPerKb.calculateFee(transactionSize)
return Result.Success(
listOf(Amount(minFee, blockchain),
Amount(normalFee, blockchain),
Amount(priorityFee, blockchain))
)
}
}
}
}
}
private fun BigDecimal.calculateFee(transactionSize: BigDecimal): BigDecimal {
val bytesInKb = BigDecimal(1024)
return this.divide(bytesInKb).multiply(transactionSize)
.setScale(8, blockchain.roundingMode())
}
}

View file

@ -0,0 +1,104 @@
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
}
}
}
}
companion object {
val SATOSHI_IN_BTC = 100000000.toBigDecimal()
}
}
data class BitcoinAddressResponse(
val balance: BigDecimal,
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,80 @@
package com.tangem.blockchain.bitcoin.network
import com.tangem.blockchain.bitcoin.UnspentTransaction
import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager.Companion.SATOSHI_IN_BTC
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 com.tangem.common.extensions.hexToBytes
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 unconfirmedTransactions = addressData.transactions?.find { it.blockHeight == 0L } != null
val bitcoinUnspents = unspents.unspentOutputs.map {
UnspentTransaction(
it.amount!!.toBigDecimal().divide(SATOSHI_IN_BTC),
it.outputIndex!!.toLong(),
it.hash!!.hexToBytes(),
it.outputScript!!.hexToBytes())
}
Result.Success(
BitcoinAddressResponse(
addressData.finalBalance?.toBigDecimal()?.divide(SATOSHI_IN_BTC)
?: 0.toBigDecimal(), unconfirmedTransactions, 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.BitcoinNetworkManager.Companion.SATOSHI_IN_BTC
import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi
import com.tangem.blockchain.bitcoin.network.api.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
import com.tangem.common.extensions.hexToBytes
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!!.toBigDecimal().divide(SATOSHI_IN_BTC),
it.outputIndex!!.toLong(),
it.hash!!.hexToBytes(),
it.outputScript!!.hexToBytes()
)
}
return Result.Success(BitcoinAddressResponse(
addressData.balance!!.toBigDecimal().divide(SATOSHI_IN_BTC),
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().divide(SATOSHI_IN_BTC),
receivedFee.normalFeePerKb!!.toBigDecimal().divide(SATOSHI_IN_BTC),
receivedFee.priorityFeePerKb!!.toBigDecimal().divide(SATOSHI_IN_BTC))
)
} 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")
}

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,41 @@
package com.tangem.blockchain.bitcoin.network.api
import com.squareup.moshi.JsonClass
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}")
suspend fun getFee(
@Path("blockchain") blockchain: String,
@Path("network") network: String
): BlockcypherFee
@GET("v1/{blockchain}/{network}/addrs/{address}?unspentOnly=true&includeScript=true")
suspend fun getAddressData(
@Path("blockchain") blockchain: String,
@Path("network") network: String,
@Path("address") address: String
): BlockcypherResponse
@GET("v1/{blockchain}/{network}/txs/{txHash}?includeHex=true")
suspend 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")
suspend fun sendTransaction(
@Path("blockchain") blockchain: String,
@Path("network") network: String,
@Body blockcypherBody: BlockcypherBody,
@Query("token") token: String
): BlockcypherTx
}
@JsonClass(generateAdapter = true)
data class BlockcypherBody(val tx: String)

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,55 @@
package com.tangem.blockchain.bitcoin.network.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class BlockcypherResponse(
@Json(name = "address")
val address: String? = null,
@Json(name = "balance")
val balance: Long? = null,
@Json(name = "unconfirmed_balance")
val unconfirmedBalance: Long? = null,
@Json(name = "txrefs")
val txrefs: List<BlockcypherTxref>? = null
)
@JsonClass(generateAdapter = true)
data class BlockcypherTxref(
@Json(name = "tx_hash")
val hash: String? = null,
@Json(name = "tx_output_n")
val outputIndex: Int? = null,
@Json(name = "value")
val amount: Long? = null,
@Json(name = "confirmations")
val confirmations: Long? = null,
@Json(name = "script")
val outputScript: String? = null
)
@JsonClass(generateAdapter = true)
data class BlockcypherTx(
@Json(name = "hex")
val hex: String? = null
)
@JsonClass(generateAdapter = true)
data class BlockcypherFee(
@Json(name = "low_fee_per_kb")
val minFeePerKb: Long? = null,
@Json(name = "medium_fee_per_kb")
val normalFeePerKb: Long? = null,
@Json(name = "high_fee_per_kb")
val priorityFeePerKb: Long? = null
)

View file

@ -0,0 +1,105 @@
package com.tangem.blockchain.cardano
import co.nstant.`in`.cbor.CborBuilder
import co.nstant.`in`.cbor.CborDecoder
import co.nstant.`in`.cbor.CborEncoder
import co.nstant.`in`.cbor.model.Array
import co.nstant.`in`.cbor.model.ByteString
import co.nstant.`in`.cbor.model.UnsignedInteger
import com.tangem.blockchain.cardano.crypto.Blake2b
import com.tangem.blockchain.common.extensions.decodeBase58
import com.tangem.blockchain.common.extensions.encodeBase58
import org.spongycastle.crypto.util.DigestFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.CRC32
class CardanoAddressFactory {
companion object {
fun makeAddress(cardPublicKey: ByteArray, testNet: Boolean = false): String {
val extendedPublicKey = extendPublicKey(cardPublicKey)
val pubKeyWithAttributesBaos = ByteArrayOutputStream()
CborEncoder(pubKeyWithAttributesBaos).encode(CborBuilder()
.addArray()
.add(0)
.addArray()
.add(0)
.add(extendedPublicKey)
.end()
.addMap()
.end()
.end()
.build())
val pubKeyWithAttributes = pubKeyWithAttributesBaos.toByteArray()
val sha3Digest = DigestFactory.createSHA3_256()
sha3Digest.update(pubKeyWithAttributes, 0, pubKeyWithAttributes.size)
val sha3Hash = ByteArray(32)
sha3Digest.doFinal(sha3Hash, 0)
val blake2b = Blake2b.Digest.newInstance(28)
val blakeHash = blake2b.digest(sha3Hash)
val hashWithAttributesBaos = ByteArrayOutputStream()
CborEncoder(hashWithAttributesBaos).encode(CborBuilder()
.addArray()
.add(blakeHash)
.addMap() //additional attributes
.end()
.add(0) //address type
.end()
.build())
val hashWithAttributes = hashWithAttributesBaos.toByteArray()
val crc32 = CRC32()
crc32.update(hashWithAttributes)
val checksum = crc32.value
val addressItem = CborBuilder().add(hashWithAttributes).build().get(0)
addressItem.setTag(24)
//addr + checksum
val addressBaos = ByteArrayOutputStream()
CborEncoder(addressBaos).encode(CborBuilder()
.addArray()
.add(addressItem)
.add(checksum)
.end()
.build())
val hexAddress = addressBaos.toByteArray()
return hexAddress.encodeBase58()
}
fun extendPublicKey(publicKey: ByteArray): ByteArray {
val zeroBytes = ByteArray(32)
zeroBytes.fill(0)
return publicKey + zeroBytes
}
}
}
class CardanoAddressValidator {
companion object {
fun validate(address: String): Boolean {
val decoded = address.decodeBase58() ?: return false
return try {
val bais = ByteArrayInputStream(decoded)
val addressList =
(CborDecoder(bais).decode()[0] as Array).dataItems
val addressItemBytes = (addressList[0] as ByteString).bytes
val checksum = (addressList[1] as UnsignedInteger).value.toLong()
val crc32 = CRC32()
crc32.update(addressItemBytes)
val calculatedChecksum = crc32.value
checksum == calculatedChecksum
} catch (e: Exception) {
false
}
}
}
}

View file

@ -0,0 +1,171 @@
package com.tangem.blockchain.cardano
import co.nstant.`in`.cbor.CborBuilder
import co.nstant.`in`.cbor.CborDecoder
import co.nstant.`in`.cbor.CborEncoder
import co.nstant.`in`.cbor.builder.ArrayBuilder
import com.tangem.blockchain.cardano.crypto.Blake2b
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.extensions.decodeBase58
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigDecimal
class CardanoTransactionBuilder() {
var unspentOutputs: List<UnspentOutput> = listOf()
var transactionBody: ByteArray = ByteArray(0)
fun buildToSign(transactionData: TransactionData): ByteArray {
val transactionBuilder = CborBuilder()
val transactionArray = transactionBuilder.addArray()
transactionArray.addInputArray()
transactionArray.addOutputArray(transactionData)
transactionArray.addMap().end()
val transactionBaos = ByteArrayOutputStream()
CborEncoder(transactionBaos).encode(transactionBuilder.build())
transactionBody = transactionBaos.toByteArray()
val blake2b = Blake2b.Digest.newInstance(32)
val transactionBodyHash = blake2b.digest(transactionBody)
val magicBaos = ByteArrayOutputStream()
CborEncoder(magicBaos).encode(CborBuilder().add(PROTOCOL_MAGIC).build())
val magic = magicBaos.toByteArray()
//dataToSign prefix
val prefixedHashBaos = ByteArrayOutputStream()
prefixedHashBaos.write(byteArrayOf(0x01.toByte()))
prefixedHashBaos.write(magic)
prefixedHashBaos.write(byteArrayOf(0x58.toByte(), 0x20.toByte()))
prefixedHashBaos.write(transactionBodyHash)
return prefixedHashBaos.toByteArray()
}
fun buildToSend(signature: ByteArray, publicKey: ByteArray): ByteArray {
val extendedPublicKey = CardanoAddressFactory.extendPublicKey(publicKey)
//pubkey + signature
val witnessBodyBaos = ByteArrayOutputStream()
CborEncoder(witnessBodyBaos).encode(CborBuilder()
.addArray()
.add(extendedPublicKey)
.add(signature)
.end()
.build())
val witnessBody = witnessBodyBaos.toByteArray()
val witnessBodyItem = CborBuilder().add(witnessBody).build()[0]
witnessBodyItem.setTag(24)
val witnessBuilder = CborBuilder()
val witnessArrayBuilder = witnessBuilder.addArray()
//witness type + witness body
for (utxo in unspentOutputs) {
witnessArrayBuilder
.addArray()
.add(0)
.add(witnessBodyItem)
.end()
}
val witnessBaos = ByteArrayOutputStream()
CborEncoder(witnessBaos).encode(witnessBuilder.build())
val witness = witnessBaos.toByteArray()
val transactionBaos = ByteArrayOutputStream()
transactionBaos.write(byteArrayOf(0x82.toByte()))
transactionBaos.write(transactionBody)
transactionBaos.write(witness)
return transactionBaos.toByteArray()
}
private fun ArrayBuilder<CborBuilder>.addInputArray() {
val inputArray = this.startArray()
for (utxo in unspentOutputs) {
val inputBaos = ByteArrayOutputStream()
CborEncoder(inputBaos).encode(CborBuilder()
.addArray()
.add(utxo.hash)
.add(utxo.outputIndex)
.end()
.build())
val input = inputBaos.toByteArray()
val inputItem = CborBuilder().add(input).build().get(0)
inputItem.setTag(24)
//input type + input
inputArray
.addArray()
.add(0)
.add(inputItem)
.end()
}
inputArray.end()
}
private fun ArrayBuilder<CborBuilder>.addOutputArray(transactionData: TransactionData) {
val amount = transactionData.amount.value!!
.movePointRight(transactionData.amount.decimals.toInt()).toLong()
val fee = transactionData.fee!!.value!!
.movePointRight(transactionData.fee.decimals.toInt()).toLong()
val change = calculateChange(amount, fee)
val outputArray = this.startArray()
//1st output
val targetAddressItem =
CborDecoder(ByteArrayInputStream(transactionData.destinationAddress.decodeBase58()))
.decode()[0]
outputArray
.addArray()
.add(targetAddressItem)
.add(amount)
.end()
//2nd output (optional)
if (change > 0) {
val myAddressItem =
CborDecoder(ByteArrayInputStream(transactionData.sourceAddress.decodeBase58()))
.decode()[0]
outputArray
.addArray()
.add(myAddressItem)
.add(change)
.end()
}
outputArray.end()
}
private fun calculateChange(amount: Long, fee: Long): Long {
val fullAmount = unspentOutputs.map { it.amount }.sum()
return fullAmount - (amount + fee)
}
fun getEstimateSize(transactionData: TransactionData, walletPublicKey: ByteArray): Int {
val dummyFeeValue = BigDecimal.valueOf(0.1)
val dummyFee = transactionData.amount.copy(value = dummyFeeValue)
val dummyAmount =
transactionData.amount.copy(value = transactionData.amount.value!! - dummyFeeValue)
val dummyTransactionData = transactionData.copy(
amount = dummyAmount,
fee = dummyFee
)
buildToSign(dummyTransactionData)
return buildToSend(ByteArray(64), walletPublicKey).size
}
companion object {
private const val PROTOCOL_MAGIC: Long = 764824073
}
}
class UnspentOutput(
val amount: Long,
val outputIndex: Long,
val hash: ByteArray
)

View file

@ -0,0 +1,70 @@
package com.tangem.blockchain.cardano
import android.util.Base64
import android.util.Log
import com.tangem.blockchain.cardano.network.CardanoAddressResponse
import com.tangem.blockchain.cardano.network.CardanoNetworkManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.encodeBase64NoWrap
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.tasks.TaskEvent
import java.math.BigDecimal
class CardanoWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = Blockchain.Cardano
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = CardanoTransactionBuilder()
private val networkManager = CardanoNetworkManager()
override suspend fun update() {
val response = networkManager.getInfo(address)
when (response) {
is Result.Success -> updateWallet(response.data)
is Result.Failure -> updateError(response.error)
}
}
private fun updateWallet(response: CardanoAddressResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance.toString()}")
currencyWallet.balances[AmountType.Coin]?.value =
response.balance.toBigDecimal().movePointLeft(blockchain.decimals.toInt())
transactionBuilder.unspentOutputs = response.unspentOutputs
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val transactionHash = transactionBuilder.buildToSign(transactionData)
when (val signerResponse = signer.sign(arrayOf(transactionHash), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature, walletPublicKey)
return networkManager.sendTransaction(transactionToSend.encodeBase64NoWrap())
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
val a = 0.155381
val b = 0.000043946
val size = transactionBuilder.getEstimateSize(
TransactionData(amount, null, source, destination), walletPublicKey
)
val fee = (a + b * size).toBigDecimal()
return Result.Success(listOf(Amount(blockchain.currency, fee, source, blockchain.decimals)))
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
package com.tangem.blockchain.cardano.network
import com.tangem.blockchain.cardano.UnspentOutput
import com.tangem.blockchain.cardano.network.adalite.AdaliteProvider
import com.tangem.blockchain.cardano.network.api.AdaliteApi
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_ADALITE
import com.tangem.blockchain.common.network.API_ADALITE_RESERVE
import com.tangem.blockchain.common.network.createRetrofitInstance
import retrofit2.HttpException
import java.io.IOException
class CardanoNetworkManager {
private val adaliteProvider by lazy {
val api = createRetrofitInstance(API_ADALITE)
.create(AdaliteApi::class.java)
AdaliteProvider(api)
}
private val adaliteReserveProvider by lazy {
val api = createRetrofitInstance(API_ADALITE_RESERVE)
.create(AdaliteApi::class.java)
AdaliteProvider(api)
}
private var provider = adaliteProvider
private fun changeProvider() {
provider = if (provider == adaliteProvider) adaliteReserveProvider else adaliteProvider
}
suspend fun getInfo(address: String): Result<CardanoAddressResponse> {
val result = provider.getInfo(address)
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.getInfo(address)
} else {
return result
}
}
}
}
suspend fun sendTransaction(transaction: String): SimpleResult {
val result = provider.sendTransaction(transaction)
when (result) {
is SimpleResult.Success -> return result
is SimpleResult.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.sendTransaction(transaction)
} else {
return result
}
}
}
}
}
data class CardanoAddressResponse(
val balance: Long,
val unspentOutputs: List<UnspentOutput>
)

View file

@ -0,0 +1,20 @@
package com.tangem.blockchain.cardano.network.api
import com.tangem.blockchain.cardano.network.adalite.AdaliteAddress
import com.tangem.blockchain.cardano.network.adalite.AdaliteSendBody
import com.tangem.blockchain.cardano.network.adalite.AdaliteUnspents
import retrofit2.http.*
import shadow.okhttp3.ResponseBody
interface AdaliteApi {
@GET("/api/addresses/summary/{address}")
suspend fun getAddress(@Path("address") address: String): AdaliteAddress
@Headers("Content-Type: application/json")
@POST("/api/bulk/addresses/utxo")
suspend fun getUnspents(@Body address: String): AdaliteUnspents
@Headers("Content-Type: application/json")
@POST("/api/v2/txs/signed")
suspend fun sendTransaction(@Body adaliteBody: AdaliteSendBody): ResponseBody // List<Any>?
}

View file

@ -0,0 +1,53 @@
package com.tangem.blockchain.cardano.network.adalite
import com.tangem.blockchain.cardano.network.CardanoAddressResponse
import com.tangem.blockchain.cardano.UnspentOutput
import com.tangem.blockchain.cardano.network.api.AdaliteApi
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 AdaliteProvider(private val api: AdaliteApi) {
suspend fun getInfo(address: String): Result<CardanoAddressResponse> {
return try {
coroutineScope {
val addressDeferred = retryIO { async { api.getAddress(address) } }
val unspentsDeferred = retryIO { async { api.getUnspents(address) } }
val addressData = addressDeferred.await()
val unspents = unspentsDeferred.await()
val cardanoUnspents = unspents.data.map {
UnspentOutput(
it.amountData!!.amount!!,
it.outputIndex!!.toLong(),
it.hash!!.toByteArray()
)
}
Result.Success(
CardanoAddressResponse(
addressData.data!!.balanceData!!.amount!!,
cardanoUnspents
)
)
}
} catch (exception: Exception) {
Result.Failure(exception)
}
}
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
retryIO { api.sendTransaction(AdaliteSendBody(transaction)) }
SimpleResult.Success
} catch (exception: Exception) {
SimpleResult.Failure(exception)
}
}
}
data class AdaliteSendBody(val signedTransaction: String)

View file

@ -0,0 +1,49 @@
package com.tangem.blockchain.cardano.network.adalite
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AdaliteAddress(
@Json(name = "final_balance")
var data: AdaliteAddressData? = null
)
@JsonClass(generateAdapter = true)
data class AdaliteAddressData(
@Json(name = "caBalance")
var balanceData: BalanceData? = null
// @Json(name = "caTxList")
// var transactions: List<AdaliteTransaction>
)
@JsonClass(generateAdapter = true)
data class BalanceData(
@Json(name = "getCoin")
var amount: Long? = null
)
@JsonClass(generateAdapter = true)
data class AdaliteUnspents(
@Json(name = "Right")
var data: List<AdaliteUtxo>
)
@JsonClass(generateAdapter = true)
data class AdaliteUtxo(
@Json(name = "cuId")
var hash: String? = null,
@Json(name = "cuOutIndex")
var outputIndex: Int? = null,
@Json(name = "cuCoins")
var amountData: BalanceData? = null
)
//@JsonClass(generateAdapter = true)
//data class AdaliteTransaction(
// @Json(name = "ctbId")
// var hash: String? = null
//)

View file

@ -0,0 +1,72 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinAddressFactory
import com.tangem.blockchain.bitcoin.BitcoinAddressValidator
import com.tangem.blockchain.ethereum.EthereumAddressFactory
import com.tangem.blockchain.ethereum.EthereumAddressValidator
import com.tangem.blockchain.cardano.CardanoAddressFactory
import com.tangem.blockchain.cardano.CardanoAddressValidator
import com.tangem.blockchain.stellar.StellarAddressFactory
import java.math.BigDecimal
enum class Blockchain(
val id: String,
val currency: String,
val decimals: Byte,
val fullName: String,
val pendingTransactionTimeout: Int
) {
Unknown("", "", 0, "", 0),
Bitcoin("BTC", "BTC", 8, "Bitcoin", 0),
BitcoinTestnet("BTC", "BTC", 8, "Bitcoin Testnet", 0),
Ethereum("ETH", "ETH", 18, "Ethereum", 0),
Rootstock("", "", 18, "", 0),
Cardano("CARDANO", "ADA", 6, "Cardano", 0),
Ripple("", "", 6, "", 0),
Binance("", "", 8, "", 0),
Stellar("XLM", "XLM", 7, "Stellar", 0);
fun roundingMode(): Int = when (this) {
Bitcoin, Ethereum, Rootstock, Binance -> BigDecimal.ROUND_DOWN
Cardano -> BigDecimal.ROUND_UP
else -> BigDecimal.ROUND_HALF_UP
}
fun makeAddress(walletPublicKey: ByteArray): String {
return when (this) {
Unknown -> throw Exception("unsupported blockchain")
Bitcoin -> BitcoinAddressFactory.makeAddress(walletPublicKey)
BitcoinTestnet -> BitcoinAddressFactory.makeAddress(walletPublicKey, testNet = true)
Ethereum -> EthereumAddressFactory.makeAddress(walletPublicKey)
// Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey)
Cardano -> CardanoAddressFactory.makeAddress(walletPublicKey)
// Ripple -> RippleAddressFactory.makeAddress(cardPublicKey)
// Binance -> BinanceAddressFactory.makeAddress(cardPublicKey)
Stellar -> StellarAddressFactory.makeAddress(walletPublicKey)
else -> throw Exception("unsupported blockchain")
}
}
fun validateAddress(address: String): Boolean {
return when (this) {
Unknown -> throw Exception("unsupported 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)
else -> throw Exception("unsupported blockchain")
}
}
companion object {
private val values = values()
fun fromId(id: String): Blockchain = values.find { it.id == id } ?: Unknown
fun fromName(name: String): Blockchain = values.find { it.name == name } ?: Unknown
fun fromCurrency(currency: String): Blockchain = values.find { it.currency == currency }
?: Unknown
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.blockchain.common
import java.math.BigDecimal
import java.util.*
interface Wallet {
val config: WalletConfig
val address: String
val exploreUrl: String?
val shareUrl: String?
}
class WalletConfig(
val allowFeeSelection: Boolean,
val allowFeeInclusion: Boolean,
var allowExtract: Boolean = false,
var allowLoad: Boolean = false
)
data class Amount(
val currencySymbol: String,
var value: BigDecimal? = null,
val address: String? = null,
val decimals: Byte,
val type: AmountType = AmountType.Coin
) {
constructor(
value: BigDecimal?,
blockchain: Blockchain,
address: String? = null,
type: AmountType = AmountType.Coin
) : this(blockchain.currency, value, address, blockchain.decimals, type)
}
data class TransactionData(
val amount: Amount,
val fee: Amount?,
val sourceAddress: String,
val destinationAddress: String,
var status: TransactionStatus = TransactionStatus.Unconfirmed,
var date: Calendar? = null
)
enum class AmountType { Coin, Token, Reserve }
enum class TransactionStatus { Confirmed, Unconfirmed }
enum class ValidationError { WrongAmount, WrongFee, WrongTotal }
interface TransactionValidator {
fun validateTransaction(amount: Amount, fee: Amount?): EnumSet<ValidationError>
}

View file

@ -0,0 +1,26 @@
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
import kotlinx.coroutines.flow.Flow
interface WalletManager {
var wallet: Wallet
val blockchain: Blockchain
suspend fun update()
}
interface TransactionSender {
suspend fun send(transactionData: TransactionData, signer: TransactionSigner) : SimpleResult
}
interface TransactionSigner {
suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse>
}
interface FeeProvider {
suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>>
}

View file

@ -0,0 +1,74 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.ethereum.Chain
import com.tangem.blockchain.ethereum.EthereumWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.stellar.StellarWalletManager
import com.tangem.commands.Card
object WalletManagerFactory {
fun makeWalletManager(card: Card): WalletManager? {
val walletPublicKey: ByteArray = card.walletPublicKey ?: return null
val blockchainName: String = card.cardData?.blockchainName ?: return null
when {
blockchainName == Blockchain.Bitcoin.id -> {
return BitcoinWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true)
)
}
blockchainName == Blockchain.BitcoinTestnet.id -> {
return BitcoinWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true),
isTestNet = true
)
}
blockchainName == Blockchain.Ethereum.id -> {
val chain = Chain.Mainnet
return EthereumWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true),
chain = chain
)
}
blockchainName == Blockchain.Stellar.id -> {
val token = getToken(card)
return StellarWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, token == null),
token = token
)
}
blockchainName == Blockchain.Cardano.id -> {
return CardanoWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(false, true)
)
}
else -> return null
}
}
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.toByte())
}
}
data class Token(
val symbol: String,
val contractAddress: String,
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,12 @@
package com.tangem.blockchain.common.extensions
import android.util.Base64
import org.bitcoinj.core.Base58
fun ByteArray.encodeBase58(): String {
return Base58.encode(this)
}
fun ByteArray.encodeBase64NoWrap(): String {
return Base64.encodeToString(this, Base64.NO_WRAP)
}

View file

@ -0,0 +1,66 @@
package com.tangem.blockchain.common.extensions
import com.tangem.CardManager
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.commands.SignResponse
import com.tangem.tasks.TaskEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun <T> retryIO(
times: Int = 3,
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()
}
class Signer(private val cardManager: CardManager) : TransactionSigner {
override suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse> = coroutineScope {
async {
suspendCancellableCoroutine<TaskEvent<SignResponse>> { continuation ->
cardManager.sign(hashes, cardId) { if (continuation.isActive) continuation.resume(it) }
}
}.await()
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.blockchain.common.extensions
import org.bitcoinj.core.AddressFormatException
import org.bitcoinj.core.Base58
fun String.decodeBase58(): ByteArray? {
return try {
Base58.decode(this)
} catch (exception: AddressFormatException) {
null
}
}

View file

@ -0,0 +1,46 @@
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/"
const val API_ADALITE = "https://explorer2.adalite.io"
const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com"

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
)

View file

@ -0,0 +1,24 @@
package com.tangem.blockchain.stellar
import org.stellar.sdk.KeyPair
class StellarAddressFactory {
companion object {
fun makeAddress(cardPublicKey: ByteArray): String {
val kp = KeyPair.fromPublicKey(cardPublicKey)
return kp.accountId
}
}
}
class StellarAddressValidator {
companion object {
fun validate(address: String): Boolean {
return try {
KeyPair.fromAccountId(address) != null
} catch (exception: IllegalArgumentException) {
false
}
}
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.blockchain.stellar
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_STELLAR
import com.tangem.blockchain.common.network.API_STELLAR_TESTNET
import com.tangem.blockchain.stellar.StellarWalletManager.Companion.STROOPS_IN_XLM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.stellar.sdk.Network
import org.stellar.sdk.Server
import org.stellar.sdk.Transaction
import org.stellar.sdk.requests.ErrorResponse
import java.io.IOException
import java.math.BigDecimal
class StellarNetworkManager(isTestNet: Boolean) {
val network: Network = if (isTestNet) Network.TESTNET else Network.PUBLIC
private val stellarServer by lazy {
Server(if (isTestNet) API_STELLAR_TESTNET else API_STELLAR)
}
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val response = stellarServer.submitTransaction(Transaction.fromEnvelopeXdr(transaction, network))
if (response.isSuccess) {
SimpleResult.Success
} else {
val trResult: String? = response.extras?.resultCodes?.transactionResultCode +
(response.extras?.resultCodes?.operationsResultCodes?.getOrNull(0) ?: "")
SimpleResult.Failure(Exception(trResult ?: "transaction failed"))
}
} catch (error: Exception) {
SimpleResult.Failure(error)
}
}
suspend fun checkIsAccountCreated(address: String): Boolean {
try {
stellarServer.accounts().account(address)
return true
} catch (errorResponse: ErrorResponse) {
if (errorResponse.code == 404) return false
return false
} catch (exception: IOException) {
return false
}
}
suspend fun getInfo(accountId: String, assetCode: String? = null): Result<StellarResponse> {
return try {
coroutineScope {
val accountResponseDefered = async(Dispatchers.IO) { stellarServer.accounts().account(accountId) }
val ledgerResponseDeferred = async(Dispatchers.IO) {
val latestLedger: Int = stellarServer.root().coreLatestLedger
stellarServer.ledgers().ledger(latestLedger.toLong())
}
val accountResponse = accountResponseDefered.await()
val balance = accountResponse.balances
.find { it.assetType == "native" }
?.balance?.toBigDecimal()
?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found"))
val assetBalance = if (assetCode == null) {
null
} else {
accountResponse.balances
.find { it.assetType != "native" && it.assetCode == assetCode }
?.balance?.toBigDecimal()
?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found"))
}
val sequence = accountResponse.sequenceNumber
val ledgerResponse = ledgerResponseDeferred.await()
val baseFee = ledgerResponse.baseFeeInStroops.toBigDecimal().divide(STROOPS_IN_XLM)
val baseReserve = ledgerResponse.baseReserveInStroops.toBigDecimal().divide(STROOPS_IN_XLM)
Result.Success(StellarResponse(
baseFee,
baseReserve,
assetBalance,
balance,
sequence
))
}
} catch (error: Exception) {
Result.Failure(error)
}
}
}
data class StellarResponse(
val baseFee: BigDecimal,
val baseReserve: BigDecimal,
val assetBalance: BigDecimal?,
val balance: BigDecimal,
val sequence: Long
)

View file

@ -0,0 +1,84 @@
package com.tangem.blockchain.stellar
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.stellar.StellarWalletManager.Companion.BASE_FEE
import com.tangem.blockchain.stellar.StellarWalletManager.Companion.STROOPS_IN_XLM
import com.tangem.common.extensions.hexToBytes
import org.stellar.sdk.*
import org.stellar.sdk.xdr.AccountID
import org.stellar.sdk.xdr.DecoratedSignature
import org.stellar.sdk.xdr.Signature
import org.stellar.sdk.xdr.SignatureHint
import java.util.*
class StellarTransactionBuilder(private val newtorkManager: StellarNetworkManager, private val publicKey: ByteArray) {
private lateinit var transaction: Transaction
suspend fun buildToSign(transactionData: TransactionData, sequence: Long, fee: Int): List<ByteArray> {
val destinationKeyPair = KeyPair.fromAccountId(transactionData.destinationAddress)
val sourceKeyPair = KeyPair.fromAccountId(transactionData.sourceAddress)
if (transactionData.amount.type == AmountType.Coin) {
val operation = if (newtorkManager.checkIsAccountCreated(transactionData.sourceAddress)) {
PaymentOperation.Builder(destinationKeyPair.accountId,
AssetTypeNative(),
transactionData.amount.value.toString())
.build()
} else {
CreateAccountOperation.Builder(destinationKeyPair.accountId, transactionData.amount.value.toString()).build()
}
return serializeOperation(operation, sourceKeyPair, sequence, fee)
} else if (transactionData.amount.type == AmountType.Token) {
val keyPair = KeyPair.fromAccountId(transactionData.amount.address)
val asset = Asset.createNonNativeAsset(transactionData.amount.currencySymbol, keyPair.accountId)
val operation: Operation = if (transactionData.amount.value != null) {
PaymentOperation.Builder(
destinationKeyPair.accountId,
asset,
transactionData.amount.value!!.toPlainString())
.build()
} else {
ChangeTrustOperation.Builder(asset, "900000000000.0000000")
.setSourceAccount(sourceKeyPair.accountId)
.build()
}
return serializeOperation(operation, sourceKeyPair, sequence, fee)
} else {
return emptyList()
}
}
private fun serializeOperation(
operation: Operation, sourceKeyPair: KeyPair,
sequence: Long, fee: Int
): List<ByteArray> {
val accountID = AccountID()
accountID.accountID = sourceKeyPair.xdrPublicKey
val currentTime = Calendar.getInstance().timeInMillis / 1000
val minTime = 0L
val maxTime = currentTime + 60
transaction = Transaction.Builder(
Account(sourceKeyPair.accountId, sequence), newtorkManager.network)
.addOperation(operation)
.addTimeBounds(TimeBounds(minTime, maxTime))
.setOperationFee(fee)
.build()
return listOf<ByteArray>(transaction.hash())
}
fun buildToSend(signature: ByteArray): String {
val hint = publicKey.takeLast(4).toByteArray()
val decoratedSignature = DecoratedSignature().apply {
this.hint = SignatureHint().apply { signatureHint = hint }
this.signature = Signature().apply { this.signature = signature }
}
transaction.signatures.add(decoratedSignature)
return transaction.toEnvelopeXdrBase64()
}
}

View file

@ -0,0 +1,95 @@
package com.tangem.blockchain.stellar
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.tasks.TaskEvent
import java.math.BigDecimal
import java.util.*
class StellarWalletManager(
private val cardId: String,
walletPublicKey: ByteArray,
walletConfig: WalletConfig,
token: Token? = null,
isTestNet: Boolean = false
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain: Blockchain = Blockchain.Stellar
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val networkManager = StellarNetworkManager(isTestNet)
private val builder = StellarTransactionBuilder(networkManager, walletPublicKey)
private var baseFee = BASE_FEE
private var baseReserve = BASE_RESERVE
private var sequence = 0L
init {
if (token != null) currencyWallet.balances[AmountType.Token] =
Amount(
token.symbol,
null,
token.contractAddress,
token.decimals,
AmountType.Token)
}
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: StellarResponse) {
currencyWallet.balances[AmountType.Coin]?.value = data.balance
currencyWallet.balances[AmountType.Token]?.value = data.assetBalance
currencyWallet.balances[AmountType.Reserve]?.value = data.baseReserve
sequence = data.sequence
baseFee = data.baseFee
baseReserve = data.baseReserve
val currentTime = Calendar.getInstance().timeInMillis
currencyWallet.pendingTransactions.forEach { transaction ->
if (transaction.date?.timeInMillis ?: 0 - currentTime > 10) {
transaction.status = TransactionStatus.Confirmed
}
}
}
private fun updateError(error: Throwable?) {
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val hashes = builder.buildToSign(transactionData, sequence, baseFee.toStroops())
when (val signerResponse = signer.sign(hashes.toTypedArray(), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = builder.buildToSend(signerResponse.data.signature)
return networkManager.sendTransaction(transactionToSend)
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
return Result.Success(listOf(
Amount(baseFee, blockchain)
))
}
private fun BigDecimal.toStroops(): Int {
return this.multiply(STROOPS_IN_XLM).toInt()
}
companion object {
val STROOPS_IN_XLM = 10000000.toBigDecimal()
val BASE_FEE = 0.00001.toBigDecimal()
val BASE_RESERVE = 0.5.toBigDecimal()
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.blockchain.wallets
import com.tangem.blockchain.common.*
import java.util.*
class CurrencyWallet(
override val config: WalletConfig,
override val address: String,
override val exploreUrl: String? = null,
override val shareUrl: String? = null,
val pendingTransactions: MutableList<TransactionData> = mutableListOf(),
val balances: MutableMap<AmountType, Amount> = mutableMapOf(),
val isTestnet: Boolean = false
) : Wallet, TransactionValidator {
override fun validateTransaction(amount: Amount, fee: Amount?): EnumSet<ValidationError> {
TODO("not implemented")
}
}

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Blockchain</string>
</resources>

View file

@ -0,0 +1,25 @@
package com.tangem.blockchain.bitcoin
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
import org.junit.jupiter.api.Assertions.*
class BitcoinAddressTest {
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "04752A727E14BBA5BD73B6714D72500F61FFD11026AD1196D2E1C54577CBEEAC3D11FC68A64700F8D533F4E311964EA8FB3AA26C588295F2133868D69C3E628693".hexToBytes()
val expected = "1D3vYSjCvzrsVVK5bNaPTjU3NxcN7NNXMN"
Truth.assertThat(BitcoinAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "1D3vYSjCvzrsVVK5bNaPTjU3NxcN7NNXMN"
Truth.assertThat(BitcoinAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.blockchain.common
import com.google.common.truth.Truth
import com.tangem.CardEnvironment
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.ethereum.EthereumWalletManager
import com.tangem.blockchain.stellar.StellarWalletManager
import com.tangem.commands.ReadCommand
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class WalletManagerFactoryTest {
@Test
fun createBitcoinWalletManager() {
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(BitcoinWalletManager::class.java)
}
@Test
fun createEthereumWalletManager() {
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(EthereumWalletManager::class.java)
}
@Test
fun createStellarWalletManager() {
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(StellarWalletManager::class.java)
}
@Test
fun createCardanoWalletManager() {
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(CardanoWalletManager::class.java)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.blockchain.ethereum
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class EthereumAddressTest {
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "04BAEC8CD3BA50FDFE1E8CF2B04B58E17041245341CD1F1C6B3A496B48956DB4C896A6848BCF8FCFC33B88341507DD25E5F4609386C68086C74CF472B86E5C3820".hexToBytes()
val expected = "0xc63763572d45171e4c25ca0818b44e5dd7f5c15b"
Truth.assertThat(EthereumAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "0xc63763572d45171e4c25ca0818b44e5dd7f5c15b"
Truth.assertThat(EthereumAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.blockchain.stellar
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class StellarAddressTest {
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "EC5387D8B38BD9EF80BDBC78D0D7E1C53F08E269436C99D5B3C2DF4B2CE73012".hexToBytes()
val expected = "GDWFHB6YWOF5T34AXW6HRUGX4HCT6CHCNFBWZGOVWPBN6SZM44YBFUDZ"
Truth.assertThat(StellarAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "GDWFHB6YWOF5T34AXW6HRUGX4HCT6CHCNFBWZGOVWPBN6SZM44YBFUDZ"
Truth.assertThat(StellarAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -1 +1 @@
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-demo'
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-demo', ':blockchain'

View file

@ -18,7 +18,7 @@ sourceCompatibility = "8"
targetCompatibility = "8"
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.3.61'
ext.dokka_version = '0.10.0'
repositories {
mavenCentral()

View file

@ -1,5 +1,6 @@
package com.tangem.common.extensions
import org.spongycastle.crypto.digests.RIPEMD160Digest
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
@ -34,3 +35,11 @@ fun ByteArray.toDate(): Date {
fun ByteArray.calculateSha512(): ByteArray = MessageDigest.getInstance("SHA-512").digest(this)
fun ByteArray.calculateSha256(): ByteArray = MessageDigest.getInstance("SHA-256").digest(this)
fun ByteArray.calculateRipemd160(): ByteArray {
val digest = RIPEMD160Digest()
digest.update(this, 0, this.size)
val out = ByteArray(20)
digest.doFinal(out, 0)
return out
}