Updated on 2026-08-14
This commit is contained in:
parent
794e24c69c
commit
a21044feee
17 changed files with 252 additions and 134 deletions
|
|
@ -24,10 +24,6 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'org.slf4j:slf4j-jdk14:1.7.25'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -49,7 +45,7 @@ dependencies {
|
|||
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.11.0'
|
||||
implementation 'com.github.stellar:java-stellar-sdk:0.13.0'
|
||||
|
||||
ext.kethereum_version = '0.79.5'
|
||||
implementation "com.github.walleth.kethereum:functions:$kethereum_version"
|
||||
|
|
@ -59,6 +55,8 @@ dependencies {
|
|||
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'
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import java.security.MessageDigest
|
|||
|
||||
class BitcoinAddressFactory {
|
||||
companion object {
|
||||
fun makeAddress(cardPublicKey: ByteArray, testNet: Boolean = false): String {
|
||||
fun makeAddress(walletPublicKey: ByteArray, testNet: Boolean = false): String {
|
||||
val netSelectionByte = if (testNet) 0x6f.toByte() else 0x00.toByte()
|
||||
val hash1 = cardPublicKey.calculateSha256().calculateRipemd160()
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,66 +1,68 @@
|
|||
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.io.ByteArrayOutputStream
|
||||
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> = listOf()
|
||||
|
||||
fun calculateChange(transactionData: TransactionData) : Long {
|
||||
val fullAmount = unspentOutputs.map { it.amount }.sum()
|
||||
return fullAmount - (transactionData.amount.value!!.toLong() + (transactionData.fee?.value?.toLong() ?: 0))
|
||||
}
|
||||
var unspentOutputs: List<UnspentTransaction>? = null
|
||||
|
||||
fun buildToSign(
|
||||
transactionData: TransactionData): List<ByteArray> {
|
||||
transactionData: TransactionData): Result<List<ByteArray>> {
|
||||
|
||||
val change: Long = calculateChange(transactionData)
|
||||
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)
|
||||
transaction = transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs!!, change)
|
||||
|
||||
val hashesForSign: MutableList<ByteArray> = mutableListOf()
|
||||
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 hashesForSign
|
||||
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)
|
||||
}
|
||||
val serializer = BitcoinSerializer(networkParameters, false)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
serializer.serialize(transaction, outputStream)
|
||||
return outputStream.toByteArray()
|
||||
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 signature = TransactionSignature(r, s.toCanonicalised())
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkParameters?,
|
||||
unspentOutputs: List<UnspentTransaction>,
|
||||
change: Long): Transaction {
|
||||
change: BigDecimal): Transaction {
|
||||
val transaction = Transaction(networkParameters)
|
||||
for (utxo in unspentOutputs) {
|
||||
transaction.addInput(Sha256Hash.wrap(utxo.hash), utxo.outputIndex, Script(utxo.outputScript))
|
||||
|
|
@ -68,9 +70,9 @@ internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkPar
|
|||
transaction.addOutput(
|
||||
Coin.parseCoin(this.amount.value!!.toPlainString()),
|
||||
Address.fromString(networkParameters, this.destinationAddress))
|
||||
if (change != 0L) {
|
||||
if (change != 0.toBigDecimal()) {
|
||||
transaction.addOutput(
|
||||
Coin.parseCoin(change.toString()),
|
||||
Coin.parseCoin(change.toPlainString()),
|
||||
Address.fromString(networkParameters,
|
||||
this.sourceAddress))
|
||||
}
|
||||
|
|
@ -78,7 +80,7 @@ internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkPar
|
|||
}
|
||||
|
||||
class UnspentTransaction(
|
||||
val amount: Long,
|
||||
val amount: BigDecimal,
|
||||
val outputIndex: Long,
|
||||
val hash: ByteArray,
|
||||
val outputScript: ByteArray
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ 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 org.bitcoinj.core.NetworkParameters
|
||||
import org.bitcoinj.core.Transaction
|
||||
import java.math.BigDecimal
|
||||
|
||||
class BitcoinWalletManager(
|
||||
|
|
@ -39,8 +38,8 @@ class BitcoinWalletManager(
|
|||
}
|
||||
|
||||
private fun updateWallet(response: BitcoinAddressResponse) {
|
||||
Log.d(this::class.java.simpleName, "Balance is ${response.balance.toString()}")
|
||||
currencyWallet.balances[AmountType.Coin]?.value = response.balance.toBigDecimal()
|
||||
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()) {
|
||||
|
|
@ -59,26 +58,31 @@ class BitcoinWalletManager(
|
|||
Log.e(this::class.java.simpleName, error?.message ?: "")
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getEstimateSize(transactionData: TransactionData): Int {
|
||||
val transaction: Transaction = transactionData.toBitcoinJTransaction(
|
||||
NetworkParameters.fromID(NetworkParameters.ID_MAINNET),
|
||||
transactionBuilder.unspentOutputs,
|
||||
transactionBuilder.calculateChange(transactionData)
|
||||
)
|
||||
var size: Int = transaction.unsafeBitcoinSerialize().size
|
||||
size += transaction.inputs.sumBy { 130 }
|
||||
return size
|
||||
override suspend fun getEstimateSize(transactionData: TransactionData): Result<Int> {
|
||||
val buildTransactionResult = transactionBuilder.buildToSign(transactionData)
|
||||
when (buildTransactionResult) {
|
||||
is Result.Failure -> return buildTransactionResult
|
||||
is Result.Success -> {
|
||||
val hashes = buildTransactionResult.data
|
||||
val finalTransaction = transactionBuilder.buildToSend(ByteArray(64 * hashes.size) {1}, walletPublicKey)
|
||||
return Result.Success(finalTransaction.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val hashes = transactionBuilder.buildToSign(transactionData)
|
||||
when (val signerResponse = signer.sign(hashes.toTypedArray(), cardId)) {
|
||||
is TaskEvent.Event -> {
|
||||
val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature, walletPublicKey)
|
||||
return networkManager.sendTransaction(transactionToSend.toHexString())
|
||||
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)
|
||||
}
|
||||
}
|
||||
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,28 +90,32 @@ class BitcoinWalletManager(
|
|||
when (val result = networkManager.getFee()) {
|
||||
is Result.Failure -> return result
|
||||
is Result.Success -> {
|
||||
val bytesInKb = BigDecimal(1024)
|
||||
val size = getEstimateSize(TransactionData(amount, null, source, destination)).toBigDecimal()
|
||||
val minFee = result.data.minimalPerKb / bytesInKb * size
|
||||
val normalFee = result.data.normalPerKb / bytesInKb * size
|
||||
val priorityFee = result.data.priorityPerKb / bytesInKb * size
|
||||
return Result.Success(
|
||||
listOf(
|
||||
Amount(blockchain.currency,
|
||||
minFee,
|
||||
source,
|
||||
blockchain.decimals),
|
||||
Amount(blockchain.currency,
|
||||
normalFee,
|
||||
source,
|
||||
blockchain.decimals),
|
||||
Amount(blockchain.currency,
|
||||
priorityFee,
|
||||
source,
|
||||
blockchain.decimals)
|
||||
)
|
||||
val sizeResult = getEstimateSize(
|
||||
TransactionData(amount,
|
||||
Amount(1.toBigDecimal().divide(SATOSHI_IN_BTC), blockchain),
|
||||
address, destination)
|
||||
)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -85,12 +85,16 @@ class BitcoinNetworkManager(private val isTestNet: Boolean) : BitcoinProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SATOSHI_IN_BTC = 100000000.toBigDecimal()
|
||||
}
|
||||
}
|
||||
|
||||
data class BitcoinAddressResponse(
|
||||
val balance: Long,
|
||||
val balance: BigDecimal,
|
||||
val hasUnconfirmed: Boolean,
|
||||
val unspentTransactions: List<UnspentTransaction>
|
||||
val unspentTransactions: List<UnspentTransaction>?
|
||||
)
|
||||
|
||||
data class BitcoinFee(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
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
|
||||
|
||||
|
|
@ -27,16 +29,16 @@ class BlockchainInfoProvider(
|
|||
|
||||
val bitcoinUnspents = unspents.unspentOutputs.map {
|
||||
UnspentTransaction(
|
||||
it.amount!!,
|
||||
it.amount!!.toBigDecimal().divide(SATOSHI_IN_BTC),
|
||||
it.outputIndex!!.toLong(),
|
||||
it.hash!!.toByteArray(),
|
||||
it.outputScript!!.toByteArray())
|
||||
it.hash!!.hexToBytes(),
|
||||
it.outputScript!!.hexToBytes())
|
||||
}
|
||||
|
||||
Result.Success(
|
||||
BitcoinAddressResponse(
|
||||
addressData.finalBalance
|
||||
?: 0L, unconfirmedTransactions, bitcoinUnspents))
|
||||
addressData.finalBalance?.toBigDecimal()?.divide(SATOSHI_IN_BTC)
|
||||
?: 0.toBigDecimal(), unconfirmedTransactions, bitcoinUnspents))
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Result.Failure(exception)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
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.response.BlockcypherBody
|
||||
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 {
|
||||
|
||||
|
|
@ -22,16 +24,16 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
|
|||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
try {
|
||||
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) }
|
||||
val unspents = addressData.txrefs!!.map {
|
||||
val unspents = addressData.txrefs?.map {
|
||||
UnspentTransaction(
|
||||
it.amount!!,
|
||||
it.amount!!.toBigDecimal().divide(SATOSHI_IN_BTC),
|
||||
it.outputIndex!!.toLong(),
|
||||
it.hash!!.toByteArray(),
|
||||
it.outputScript!!.toByteArray()
|
||||
it.hash!!.hexToBytes(),
|
||||
it.outputScript!!.hexToBytes()
|
||||
)
|
||||
}
|
||||
return Result.Success(BitcoinAddressResponse(
|
||||
addressData.balance!!,
|
||||
addressData.balance!!.toBigDecimal().divide(SATOSHI_IN_BTC),
|
||||
addressData.unconfirmedBalance != 0L,
|
||||
unspents))
|
||||
|
||||
|
|
@ -44,9 +46,9 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
|
|||
try {
|
||||
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) }
|
||||
return Result.Success(
|
||||
BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal() / satoshiInBtc,
|
||||
receivedFee.normalFeePerKb!!.toBigDecimal() / satoshiInBtc,
|
||||
receivedFee.priorityFeePerKb!!.toBigDecimal() / satoshiInBtc)
|
||||
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)
|
||||
|
|
@ -78,6 +80,4 @@ private object BlockcypherToken {
|
|||
private enum class BlockcypherNetwork(val network: String) {
|
||||
Main("main"),
|
||||
Test("test3")
|
||||
}
|
||||
|
||||
val satoshiInBtc = 100000000.toBigDecimal()
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.blockchain.bitcoin.network.api
|
||||
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherBody
|
||||
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
|
||||
|
|
@ -8,20 +8,20 @@ import retrofit2.http.*
|
|||
|
||||
interface BlockcypherApi {
|
||||
@GET("v1/{blockchain}/{network}")
|
||||
fun getFee(
|
||||
suspend fun getFee(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String
|
||||
): BlockcypherFee
|
||||
|
||||
@GET("v1/{blockchain}/{network}/addrs/{address}?unspentOnly=true&includeScript=true")
|
||||
fun getAddressData(
|
||||
suspend fun getAddressData(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String,
|
||||
@Path("address") address: String
|
||||
): BlockcypherResponse
|
||||
|
||||
@GET("v1/{blockchain}/{network}/txs/{txHash}?includeHex=true")
|
||||
fun getTransactions(
|
||||
suspend fun getTransactions(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String,
|
||||
@Path("txHash") txHash: String
|
||||
|
|
@ -29,10 +29,13 @@ interface BlockcypherApi {
|
|||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v1/{blockchain}/{network}/txs/push")
|
||||
fun sendTransaction(
|
||||
suspend fun sendTransaction(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String,
|
||||
@Body blockcypherBody: BlockcypherBody,
|
||||
@Query("token") token: String
|
||||
): BlockcypherResponse
|
||||
}
|
||||
): BlockcypherTx
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlockcypherBody(val tx: String)
|
||||
|
|
@ -1,52 +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")
|
||||
var address: String? = null,
|
||||
val address: String? = null,
|
||||
|
||||
@Json(name = "balance")
|
||||
var balance: Long? = null,
|
||||
val balance: Long? = null,
|
||||
|
||||
@Json(name = "unconfirmed_balance")
|
||||
var unconfirmedBalance: Long? = null,
|
||||
val unconfirmedBalance: Long? = null,
|
||||
|
||||
@Json(name = "txrefs")
|
||||
var txrefs: List<BlockcypherTxref>? = null
|
||||
val txrefs: List<BlockcypherTxref>? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlockcypherTxref(
|
||||
@Json(name = "tx_hash")
|
||||
var hash: String? = null,
|
||||
val hash: String? = null,
|
||||
|
||||
@Json(name = "tx_output_n")
|
||||
var outputIndex: Int? = null,
|
||||
val outputIndex: Int? = null,
|
||||
|
||||
@Json(name = "value")
|
||||
var amount: Long? = null,
|
||||
val amount: Long? = null,
|
||||
|
||||
@Json(name = "confirmations")
|
||||
var confirmations: Long? = null,
|
||||
val confirmations: Long? = null,
|
||||
|
||||
@Json(name = "script")
|
||||
var outputScript: String? = null
|
||||
val outputScript: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlockcypherTx(
|
||||
@Json(name = "hex")
|
||||
var hex: String? = null
|
||||
val hex: String? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlockcypherFee(
|
||||
@Json(name = "low_fee_per_kb")
|
||||
var minFeePerKb: Long? = null,
|
||||
val minFeePerKb: Long? = null,
|
||||
|
||||
@Json(name = "medium_fee_per_kb")
|
||||
var normalFeePerKb: Long? = null,
|
||||
val normalFeePerKb: Long? = null,
|
||||
|
||||
@Json(name = "high_fee_per_kb")
|
||||
var priorityFeePerKb: Long? = null
|
||||
)
|
||||
|
||||
data class BlockcypherBody(val tx: String)
|
||||
val priorityFeePerKb: Long? = null
|
||||
)
|
||||
|
|
@ -2,10 +2,10 @@ 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.eth.EthereumAddressFactory
|
||||
import com.tangem.blockchain.eth.EthereumAddressValidator
|
||||
import com.tangem.blockchain.stellar.StellarAddressFactory
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -17,14 +17,14 @@ enum class Blockchain(
|
|||
val pendingTransactionTimeout: Int
|
||||
) {
|
||||
Unknown("", "", 0, "", 0),
|
||||
Bitcoin("btc", "", 8, "", 0),
|
||||
BitcoinTestnet("btc", "", 8, "", 0),
|
||||
Ethereum("", "", 18, "", 0),
|
||||
Bitcoin("BTC", "BTC", 8, "Bitcoin", 0),
|
||||
BitcoinTestnet("BTC", "BTC", 8, "Bitcoin Testnet", 0),
|
||||
Ethereum("ETH", "ETH", 18, "Ethereum", 0),
|
||||
Rootstock("", "", 18, "", 0),
|
||||
Cardano("", "", 6, "", 0),
|
||||
Cardano("CARDANO", "ADA", 6, "Cardano", 0),
|
||||
Ripple("", "", 6, "", 0),
|
||||
Binance("", "", 8, "", 0),
|
||||
Stellar("", "", 7, "", 0);
|
||||
Stellar("XLM", "XLM", 7, "Stellar", 0);
|
||||
|
||||
fun roundingMode(): Int = when (this) {
|
||||
Bitcoin, Ethereum, Rootstock, Binance -> BigDecimal.ROUND_DOWN
|
||||
|
|
@ -32,17 +32,17 @@ enum class Blockchain(
|
|||
else -> BigDecimal.ROUND_HALF_UP
|
||||
}
|
||||
|
||||
fun makeAddress(cardPublicKey: ByteArray): String {
|
||||
fun makeAddress(walletPublicKey: ByteArray): String {
|
||||
return when (this) {
|
||||
Unknown -> throw Exception("unsupported blockchain")
|
||||
Bitcoin -> BitcoinAddressFactory.makeAddress(cardPublicKey)
|
||||
BitcoinTestnet -> BitcoinAddressFactory.makeAddress(cardPublicKey, testNet = true)
|
||||
Ethereum -> EthereumAddressFactory.makeAddress(cardPublicKey)
|
||||
Bitcoin -> BitcoinAddressFactory.makeAddress(walletPublicKey)
|
||||
BitcoinTestnet -> BitcoinAddressFactory.makeAddress(walletPublicKey, testNet = true)
|
||||
Ethereum -> EthereumAddressFactory.makeAddress(walletPublicKey)
|
||||
// Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey)
|
||||
Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey)
|
||||
Cardano -> CardanoAddressFactory.makeAddress(walletPublicKey)
|
||||
// Ripple -> RippleAddressFactory.makeAddress(cardPublicKey)
|
||||
// Binance -> BinanceAddressFactory.makeAddress(cardPublicKey)
|
||||
Stellar -> StellarAddressFactory.makeAddress(cardPublicKey)
|
||||
Stellar -> StellarAddressFactory.makeAddress(walletPublicKey)
|
||||
else -> throw Exception("unsupported blockchain")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,19 +23,27 @@ data class Amount(
|
|||
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.Uncomfirmed
|
||||
var status: TransactionStatus = TransactionStatus.Unconfirmed,
|
||||
var date: Calendar? = null
|
||||
)
|
||||
|
||||
enum class AmountType { Coin, Token, Reserve }
|
||||
|
||||
enum class TransactionStatus {Confirmed, Uncomfirmed}
|
||||
enum class TransactionStatus { Confirmed, Unconfirmed }
|
||||
|
||||
enum class ValidationError { WrongAmount, WrongFee, WrongTotal }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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
|
||||
|
|
@ -13,7 +14,7 @@ interface WalletManager {
|
|||
}
|
||||
|
||||
interface TransactionEstimator {
|
||||
suspend fun getEstimateSize(transactionData: TransactionData): Int
|
||||
suspend fun getEstimateSize(transactionData: TransactionData): Result<Int>
|
||||
}
|
||||
|
||||
interface TransactionSender {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
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.eth.Chain
|
||||
import com.tangem.blockchain.eth.EthereumWalletManager
|
||||
import com.tangem.blockchain.stellar.StellarWalletManager
|
||||
import com.tangem.commands.Card
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ object WalletManagerFactory {
|
|||
val chain = if (isTestNet(blockchainName)) {
|
||||
Chain.EthereumClassicTestnet
|
||||
} else {
|
||||
Chain.EthereumClassicMainnet
|
||||
Chain.Mainnet
|
||||
}
|
||||
return EthereumWalletManager(
|
||||
cardId = card.cardId,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
package com.tangem.blockchain.common.extensions
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
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 = Int.MAX_VALUE,
|
||||
times: Int = 3,
|
||||
initialDelay: Long = 100,
|
||||
maxDelay: Long = 1000,
|
||||
factor: Double = 2.0,
|
||||
|
|
@ -47,3 +55,12 @@ sealed class 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,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,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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue