Updated on 2026-08-14
This commit is contained in:
commit
faece7f6b8
53 changed files with 4671 additions and 2 deletions
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
|
@ -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
1
blockchain/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
70
blockchain/build.gradle
Normal file
70
blockchain/build.gradle
Normal 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
21
blockchain/proguard-rules.pro
vendored
Normal 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
|
||||
1
blockchain/src/main/AndroidManifest.xml
Normal file
1
blockchain/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<manifest package="com.tangem.blockchain" />
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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/"
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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>?
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
//)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
3
blockchain/src/main/res/values/strings.xml
Normal file
3
blockchain/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Blockchain</string>
|
||||
</resources>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue