Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-30 14:22:36 +03:00
parent 819fbe0d44
commit ed8b4c3910
14 changed files with 2673 additions and 3 deletions

View file

@ -60,6 +60,8 @@ dependencies {
implementation "com.github.walleth.kethereum:crypto_api:$kethereum_version"
implementation "com.github.walleth.kethereum:model:$kethereum_version"
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'

View file

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

View file

@ -0,0 +1,155 @@
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
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)
}
companion object {
private const val PROTOCOL_MAGIC: Long = 764824073
}
}
class UnspentOutput(
val amount: Long,
val outputIndex: Long,
val hash: ByteArray
)

View file

@ -0,0 +1,85 @@
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,
TransactionEstimator,
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 getEstimateSize(transactionData: TransactionData): 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
)
transactionBuilder.buildToSign(dummyTransactionData)
return transactionBuilder.buildToSend(ByteArray(64), walletPublicKey).size
}
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 = getEstimateSize(TransactionData(amount, null, source, destination))
val fee = (a + b * size).toBigDecimal()
return Result.Success(listOf(Amount(blockchain.currency, fee, source, blockchain.decimals)))
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,8 @@ package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinAddressFactory
import com.tangem.blockchain.bitcoin.BitcoinAddressValidator
import com.tangem.blockchain.cardano.CardanoAddressFactory
import com.tangem.blockchain.cardano.CardanoAddressValidator
import com.tangem.blockchain.eth.EthereumAddressFactory
import com.tangem.blockchain.eth.EthereumAddressValidator
import com.tangem.blockchain.stellar.StellarAddressFactory
@ -37,7 +39,7 @@ enum class Blockchain(
BitcoinTestnet -> BitcoinAddressFactory.makeAddress(cardPublicKey, testNet = true)
Ethereum -> EthereumAddressFactory.makeAddress(cardPublicKey)
// Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey)
// Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey)
Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey)
// Ripple -> RippleAddressFactory.makeAddress(cardPublicKey)
// Binance -> BinanceAddressFactory.makeAddress(cardPublicKey)
Stellar -> StellarAddressFactory.makeAddress(cardPublicKey)
@ -52,7 +54,7 @@ enum class Blockchain(
BitcoinTestnet -> BitcoinAddressValidator.validate(address, testNet = true)
Ethereum -> EthereumAddressValidator.validate(address)
// Rootstock -> RootstockAddressValidator.validate(address)
// Cardano -> CardanoAddressValidator.validate(address)
Cardano -> CardanoAddressValidator.validate(address)
// Ripple -> RippleAddressValidator.validate(address)
// Binance -> BinanceAddressValidator.validate(address)
// Stellar -> StellarAddressValidator.validate(address)

View file

@ -1,6 +1,7 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.eth.Chain
import com.tangem.blockchain.eth.EthereumWalletManager
import com.tangem.blockchain.stellar.StellarWalletManager
@ -41,6 +42,13 @@ object WalletManagerFactory {
token = token,
isTestNet = isTestNet(blockchainName))
}
blockchainName.contains("cardano") -> {
return CardanoWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(false, true)
)
}
else -> return null
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.blockchain.common.extensions
import android.util.Base64
import org.bitcoinj.core.Base58
fun ByteArray.encodeBase58(): String {
return Base58.encode(this)
}
fun ByteArray.encodeBase64NoWrap(): String {
return Base64.encodeToString(this, Base64.NO_WRAP)
}

View file

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

View file

@ -41,4 +41,6 @@ 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_BLOCKCHAIN_INFO = "https://blockchain.info/"
const val API_ADALITE = "https://explorer2.adalite.io"
const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com"