Updated on 2026-08-14
This commit is contained in:
parent
c883a1443c
commit
63338bc540
22 changed files with 684 additions and 59 deletions
|
|
@ -1,9 +1,10 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.0"
|
||||
buildToolsVersion "29.0.2"
|
||||
|
||||
|
||||
defaultConfig {
|
||||
|
|
@ -23,6 +24,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'org.slf4j:slf4j-jdk14:1.7.25'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -34,15 +39,29 @@ dependencies {
|
|||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
|
||||
implementation 'com.squareup.moshi:moshi:1.9.2'
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2")
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.3"
|
||||
|
||||
implementation 'org.bitcoinj:bitcoinj-core:0.15.2'
|
||||
implementation 'com.github.komputing:kethereum:0.79.5'
|
||||
implementation 'com.github.stellar:java-stellar-sdk:0.11.0'
|
||||
|
||||
ext.kethereum_version = '0.79.5'
|
||||
implementation "com.github.walleth.kethereum:functions:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:keccak_shortcut:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:wallet:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:crypto_impl_spongycastle:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:crypto:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:crypto_api:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:model:$kethereum_version"
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
||||
testImplementation "com.google.truth:truth:1.0"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.blockchain.bitcoin
|
||||
|
||||
import com.tangem.blockchain.extensions.calculateRipemd160
|
||||
|
||||
import com.tangem.common.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import org.bitcoinj.core.AddressFormatException
|
||||
import org.bitcoinj.core.Base58
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.blockchain.bitcoin
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.toCanonicalised
|
||||
import com.tangem.blockchain.common.extensions.toCanonicalised
|
||||
import org.bitcoinj.core.*
|
||||
import org.bitcoinj.crypto.TransactionSignature
|
||||
import org.bitcoinj.script.Script
|
||||
|
|
@ -17,7 +17,7 @@ class BitcoinTransactionBuilder(private val testNet: Boolean) {
|
|||
|
||||
fun calculateChange(transactionData: TransactionData) : Long {
|
||||
val fullAmount = unspentOutputs.map { it.amount }.sum()
|
||||
return fullAmount - (transactionData.amount.value!!.toLong() + (transactionData.fee?.value!!.toLong()))
|
||||
return fullAmount - (transactionData.amount.value!!.toLong() + (transactionData.fee?.value?.toLong() ?: 0))
|
||||
}
|
||||
|
||||
fun buildToSign(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
package com.tangem.blockchain.bitcoin
|
||||
|
||||
import android.util.Log
|
||||
import com.tangem.blockchain.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.blockchain.wallets.CurrencyWallet
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.tasks.TaskEvent
|
||||
import org.bitcoinj.core.NetworkParameters
|
||||
import org.bitcoinj.core.Transaction
|
||||
import java.math.BigDecimal
|
||||
|
||||
class BitcoinWalletManager(
|
||||
private val cardId: String,
|
||||
|
|
@ -18,36 +25,89 @@ class BitcoinWalletManager(
|
|||
|
||||
override val blockchain = if (isTestNet) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
|
||||
private val address = blockchain.makeAddress(walletPublicKey)
|
||||
override var wallet: Wallet = CurrencyWallet(walletConfig, address)
|
||||
private val currencyWallet = CurrencyWallet(walletConfig, address)
|
||||
override var wallet: Wallet = currencyWallet
|
||||
private val transactionBuilder = BitcoinTransactionBuilder(isTestNet)
|
||||
private val networkManager = BitcoinNetworkManager(isTestNet)
|
||||
|
||||
override suspend fun update() {
|
||||
val response = networkManager.getInfo(address)
|
||||
when (response) {
|
||||
is Result.Success -> updateWallet(response.data)
|
||||
is Result.Failure -> updateError(response.error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update() {
|
||||
transactionBuilder.unspentOutputs = listOf()
|
||||
private fun updateWallet(response: BitcoinAddressResponse) {
|
||||
Log.d(this::class.java.simpleName, "Balance is ${response.balance.toString()}")
|
||||
currencyWallet.balances[AmountType.Coin]?.value = response.balance.toBigDecimal()
|
||||
transactionBuilder.unspentOutputs = response.unspentTransactions
|
||||
if (response.hasUnconfirmed) {
|
||||
if (currencyWallet.pendingTransactions.isEmpty()) {
|
||||
currencyWallet.pendingTransactions.add(TransactionData(
|
||||
Amount(blockchain.currency, decimals = blockchain.decimals),
|
||||
null,
|
||||
"unknown",
|
||||
currencyWallet.address))
|
||||
} else {
|
||||
currencyWallet.pendingTransactions.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateError(error: Throwable?) {
|
||||
Log.e(this::class.java.simpleName, error?.message ?: "")
|
||||
}
|
||||
|
||||
|
||||
override fun getEstimateSize(transactionData: TransactionData): Int {
|
||||
override suspend fun getEstimateSize(transactionData: TransactionData): Int {
|
||||
val transaction: Transaction = transactionData.toBitcoinJTransaction(
|
||||
NetworkParameters.fromID(NetworkParameters.ID_MAINNET),
|
||||
transactionBuilder.unspentOutputs,
|
||||
transactionBuilder.calculateChange(transactionData)
|
||||
)
|
||||
)
|
||||
var size: Int = transaction.unsafeBitcoinSerialize().size
|
||||
size += transaction.inputs.sumBy { 130 }
|
||||
return size
|
||||
}
|
||||
|
||||
override fun send(transactionData: TransactionData, signer: TransactionSigner) {
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val hashes = transactionBuilder.buildToSign(transactionData)
|
||||
signer.sign(hashes.toTypedArray(), cardId) {
|
||||
when (it) {
|
||||
is TaskEvent.Event -> transactionBuilder.buildToSend(it.data.signature, walletPublicKey)
|
||||
when (val signerResponse = signer.sign(hashes.toTypedArray(), cardId)) {
|
||||
is TaskEvent.Event -> {
|
||||
val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature, walletPublicKey)
|
||||
return networkManager.sendTransaction(transactionToSend.toHexString())
|
||||
}
|
||||
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFee(amount: Amount, source: String, destination: String): List<Amount> {
|
||||
return BitcoinServer.getFee()
|
||||
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
|
||||
when (val result = networkManager.getFee()) {
|
||||
is Result.Failure -> return result
|
||||
is Result.Success -> {
|
||||
val bytesInKb = BigDecimal(1024)
|
||||
val size = getEstimateSize(TransactionData(amount, null, source, destination)).toBigDecimal()
|
||||
val minFee = result.data.minimalPerKb / bytesInKb * size
|
||||
val normalFee = result.data.normalPerKb / bytesInKb * size
|
||||
val priorityFee = result.data.priorityPerKb / bytesInKb * size
|
||||
return Result.Success(
|
||||
listOf(
|
||||
Amount(blockchain.currency,
|
||||
minFee,
|
||||
source,
|
||||
blockchain.decimals),
|
||||
Amount(blockchain.currency,
|
||||
normalFee,
|
||||
source,
|
||||
blockchain.decimals),
|
||||
Amount(blockchain.currency,
|
||||
priorityFee,
|
||||
source,
|
||||
blockchain.decimals)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.blockchain.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.bitcoin.UnspentTransaction
|
||||
import com.tangem.blockchain.bitcoin.network.api.BlockchainInfoApi
|
||||
import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi
|
||||
import com.tangem.blockchain.bitcoin.network.api.EstimatefeeApi
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.blockchain.common.network.API_BLOCKCHAIN_INFO
|
||||
import com.tangem.blockchain.common.network.API_BLOCKCYPHER
|
||||
import com.tangem.blockchain.common.network.API_ESTIMATEFEE
|
||||
import com.tangem.blockchain.common.network.createRetrofitInstance
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
||||
class BitcoinNetworkManager(private val isTestNet: Boolean) : BitcoinProvider {
|
||||
|
||||
private val blockcypherProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCYPHER)
|
||||
.create(BlockcypherApi::class.java)
|
||||
BlockcypherProvider(api, isTestNet)
|
||||
}
|
||||
|
||||
private val blockchainInfoProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCHAIN_INFO)
|
||||
.create(BlockchainInfoApi::class.java)
|
||||
val estimateFeeApi = createRetrofitInstance(API_ESTIMATEFEE)
|
||||
.create(EstimatefeeApi::class.java)
|
||||
BlockchainInfoProvider(api, estimateFeeApi)
|
||||
}
|
||||
|
||||
private var bitcoinProvider: BitcoinProvider = blockchainInfoProvider
|
||||
|
||||
private fun changeProvider() {
|
||||
bitcoinProvider = if (bitcoinProvider == blockchainInfoProvider) {
|
||||
blockcypherProvider
|
||||
} else {
|
||||
blockchainInfoProvider
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
val result = bitcoinProvider.getInfo(address)
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return bitcoinProvider.getInfo(address)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
val result = bitcoinProvider.getFee()
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return bitcoinProvider.getFee()
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
val result = bitcoinProvider.sendTransaction(transaction)
|
||||
when (result) {
|
||||
is SimpleResult.Success -> return result
|
||||
is SimpleResult.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return bitcoinProvider.sendTransaction(transaction)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BitcoinAddressResponse(
|
||||
val balance: Long,
|
||||
val hasUnconfirmed: Boolean,
|
||||
val unspentTransactions: List<UnspentTransaction>)
|
||||
|
||||
data class BitcoinFee(
|
||||
val minimalPerKb: BigDecimal,
|
||||
val normalPerKb: BigDecimal,
|
||||
val priorityPerKb: BigDecimal
|
||||
)
|
||||
|
|
@ -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,78 @@
|
|||
package com.tangem.blockchain.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.bitcoin.UnspentTransaction
|
||||
import com.tangem.blockchain.bitcoin.network.api.BlockchainInfoApi
|
||||
import com.tangem.blockchain.bitcoin.network.api.EstimatefeeApi
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.blockchain.common.extensions.retryIO
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
|
||||
class BlockchainInfoProvider(
|
||||
private val blockchainApi: BlockchainInfoApi,
|
||||
private val estimatefeeApi: EstimatefeeApi
|
||||
) : BitcoinProvider {
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
return try {
|
||||
coroutineScope {
|
||||
val addressDeferred = retryIO { async { blockchainApi.getAddress(address) } }
|
||||
val unspentsDeferred = retryIO { async { blockchainApi.getUnspents(address) } }
|
||||
|
||||
val addressData = addressDeferred.await()
|
||||
val unspents = unspentsDeferred.await()
|
||||
val unconfinedTransactions = addressData.transactions?.find { it.blockHeight == 0L } != null
|
||||
|
||||
val bitcoinUnspents = unspents.unspentOutputs.map {
|
||||
UnspentTransaction(
|
||||
it.amount!!,
|
||||
it.outputIndex!!.toLong(),
|
||||
it.hash!!.toByteArray(),
|
||||
it.outputScript!!.toByteArray())
|
||||
}
|
||||
|
||||
Result.Success(
|
||||
BitcoinAddressResponse(
|
||||
addressData.finalBalance
|
||||
?: 0L, unconfinedTransactions, bitcoinUnspents))
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Result.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
return try {
|
||||
coroutineScope {
|
||||
val minFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeeMinimal() } }
|
||||
val normalFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeeNormal() } }
|
||||
val priorityFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeePriority() } }
|
||||
|
||||
val minFee = minFeeDeferred.await()
|
||||
val normalFee = normalFeeDeferred.await()
|
||||
val priorityFee = priorityFeeDeferred.await()
|
||||
|
||||
Result.Success(BitcoinFee(
|
||||
minFee.toBigDecimal(),
|
||||
normalFee.toBigDecimal(),
|
||||
priorityFee.toBigDecimal()))
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Result.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
return try {
|
||||
retryIO { blockchainApi.sendTransaction(transaction) }
|
||||
SimpleResult.Success
|
||||
} catch (exception: Exception) {
|
||||
SimpleResult.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.blockchain.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.bitcoin.UnspentTransaction
|
||||
import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherBody
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherFee
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherResponse
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.blockchain.common.extensions.retryIO
|
||||
|
||||
class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : BitcoinProvider {
|
||||
|
||||
private val blockchain = "btc"
|
||||
|
||||
private val network = if (isTestNet) {
|
||||
BlockcypherNetwork.Test.network
|
||||
} else {
|
||||
BlockcypherNetwork.Main.network
|
||||
}
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
try {
|
||||
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) }
|
||||
val unspents = addressData.txrefs!!.map {
|
||||
UnspentTransaction(
|
||||
it.amount!!,
|
||||
it.outputIndex!!.toLong(),
|
||||
it.hash!!.toByteArray(),
|
||||
it.outputScript!!.toByteArray()
|
||||
)
|
||||
}
|
||||
return Result.Success(BitcoinAddressResponse(
|
||||
addressData.balance!!,
|
||||
addressData.unconfirmedBalance != 0L,
|
||||
unspents))
|
||||
|
||||
} catch (error: Exception) {
|
||||
return Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
try {
|
||||
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) }
|
||||
return Result.Success(
|
||||
BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal() / satoshiInBtc,
|
||||
receivedFee.normalFeePerKb!!.toBigDecimal() / satoshiInBtc,
|
||||
receivedFee.priorityFeePerKb!!.toBigDecimal() / satoshiInBtc)
|
||||
)
|
||||
} catch (error: Exception) {
|
||||
return Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
try {
|
||||
retryIO {
|
||||
api.sendTransaction(
|
||||
blockchain, network, BlockcypherBody(transaction), BlockcypherToken.getToken())
|
||||
}
|
||||
return SimpleResult.Success
|
||||
} catch (error: Exception) {
|
||||
return SimpleResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object BlockcypherToken {
|
||||
private val tokens = listOf(
|
||||
"aa8184b0e0894b88a5688e01b3dc1e82",
|
||||
"56c4ca23c6484c8f8864c32fde4def8d",
|
||||
"66a8a37c5e9d4d2c9bb191acfe7f93aa")
|
||||
|
||||
fun getToken(): String = tokens.random()
|
||||
}
|
||||
|
||||
private enum class BlockcypherNetwork(val network: String) {
|
||||
Main("main"),
|
||||
Test("test3")
|
||||
}
|
||||
|
||||
val satoshiInBtc = 100000000.toBigDecimal()
|
||||
|
|
@ -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,38 @@
|
|||
package com.tangem.blockchain.bitcoin.network.api
|
||||
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherBody
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherFee
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherResponse
|
||||
import com.tangem.blockchain.bitcoin.network.response.BlockcypherTx
|
||||
import retrofit2.http.*
|
||||
|
||||
interface BlockcypherApi {
|
||||
@GET("v1/{blockchain}/{network}")
|
||||
fun getFee(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String
|
||||
): BlockcypherFee
|
||||
|
||||
@GET("v1/{blockchain}/{network}/addrs/{address}?unspentOnly=true&includeScript=true")
|
||||
fun getAddressData(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String,
|
||||
@Path("address") address: String
|
||||
): BlockcypherResponse
|
||||
|
||||
@GET("v1/{blockchain}/{network}/txs/{txHash}?includeHex=true")
|
||||
fun getTransactions(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String,
|
||||
@Path("txHash") txHash: String
|
||||
): BlockcypherTx
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v1/{blockchain}/{network}/txs/push")
|
||||
fun sendTransaction(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Path("network") network: String,
|
||||
@Body blockcypherBody: BlockcypherBody,
|
||||
@Query("token") token: String
|
||||
): BlockcypherResponse
|
||||
}
|
||||
|
|
@ -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,52 @@
|
|||
package com.tangem.blockchain.bitcoin.network.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
data class BlockcypherResponse(
|
||||
@Json(name = "address")
|
||||
var address: String? = null,
|
||||
|
||||
@Json(name = "balance")
|
||||
var balance: Long? = null,
|
||||
|
||||
@Json(name = "unconfirmed_balance")
|
||||
var unconfirmedBalance: Long? = null,
|
||||
|
||||
@Json(name = "txrefs")
|
||||
var txrefs: List<BlockcypherTxref>? = null
|
||||
)
|
||||
|
||||
data class BlockcypherTxref(
|
||||
@Json(name = "tx_hash")
|
||||
var hash: String? = null,
|
||||
|
||||
@Json(name = "tx_output_n")
|
||||
var outputIndex: Int? = null,
|
||||
|
||||
@Json(name = "value")
|
||||
var amount: Long? = null,
|
||||
|
||||
@Json(name = "confirmations")
|
||||
var confirmations: Long? = null,
|
||||
|
||||
@Json(name = "script")
|
||||
var outputScript: String? = null
|
||||
)
|
||||
|
||||
data class BlockcypherTx(
|
||||
@Json(name = "hex")
|
||||
var hex: String? = null
|
||||
)
|
||||
|
||||
data class BlockcypherFee(
|
||||
@Json(name = "low_fee_per_kb")
|
||||
var minFeePerKb: Long? = null,
|
||||
|
||||
@Json(name = "medium_fee_per_kb")
|
||||
var normalFeePerKb: Long? = null,
|
||||
|
||||
@Json(name = "high_fee_per_kb")
|
||||
var priorityFeePerKb: Long? = null
|
||||
)
|
||||
|
||||
data class BlockcypherBody(val tx: String)
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.blockchain.bitcoin.BitcoinAddressValidator
|
|||
import com.tangem.blockchain.eth.EthereumAddressFactory
|
||||
import com.tangem.blockchain.eth.EthereumAddressValidator
|
||||
import com.tangem.blockchain.stellar.StellarAddressFactory
|
||||
import com.tangem.blockchain.stellar.StellarAddressValidator
|
||||
import java.math.BigDecimal
|
||||
|
||||
enum class Blockchain(
|
||||
|
|
@ -13,10 +12,11 @@ enum class Blockchain(
|
|||
val currency: String,
|
||||
val decimals: Byte,
|
||||
val fullName: String,
|
||||
val pendingTransactionTimeout: Int) {
|
||||
val pendingTransactionTimeout: Int
|
||||
) {
|
||||
Unknown("", "", 0, "", 0),
|
||||
Bitcoin("", "", 8, "", 0),
|
||||
BitcoinTestnet("", "", 8, "", 0),
|
||||
Bitcoin("btc", "", 8, "", 0),
|
||||
BitcoinTestnet("btc", "", 8, "", 0),
|
||||
Ethereum("", "", 18, "", 0),
|
||||
Rootstock("", "", 18, "", 0),
|
||||
Cardano("", "", 6, "", 0),
|
||||
|
|
@ -36,11 +36,12 @@ enum class Blockchain(
|
|||
Bitcoin -> BitcoinAddressFactory.makeAddress(cardPublicKey)
|
||||
BitcoinTestnet -> BitcoinAddressFactory.makeAddress(cardPublicKey, testNet = true)
|
||||
Ethereum -> EthereumAddressFactory.makeAddress(cardPublicKey)
|
||||
Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey)
|
||||
Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey)
|
||||
Ripple -> RippleAddressFactory.makeAddress(cardPublicKey)
|
||||
Binance -> BinanceAddressFactory.makeAddress(cardPublicKey)
|
||||
// Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey)
|
||||
// Cardano -> CardanoAddressFactory.makeAddress(cardPublicKey)
|
||||
// Ripple -> RippleAddressFactory.makeAddress(cardPublicKey)
|
||||
// Binance -> BinanceAddressFactory.makeAddress(cardPublicKey)
|
||||
Stellar -> StellarAddressFactory.makeAddress(cardPublicKey)
|
||||
else -> throw Exception("unsupported blockchain")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,11 +51,12 @@ enum class Blockchain(
|
|||
Bitcoin -> BitcoinAddressValidator.validate(address)
|
||||
BitcoinTestnet -> BitcoinAddressValidator.validate(address, testNet = true)
|
||||
Ethereum -> EthereumAddressValidator.validate(address)
|
||||
Rootstock -> RootstockAddressValidator.validate(address)
|
||||
Cardano -> CardanoAddressValidator.validate(address)
|
||||
Ripple -> RippleAddressValidator.validate(address)
|
||||
Binance -> BinanceAddressValidator.validate(address)
|
||||
Stellar -> StellarAddressValidator.validate(address)
|
||||
// Rootstock -> RootstockAddressValidator.validate(address)
|
||||
// Cardano -> CardanoAddressValidator.validate(address)
|
||||
// Ripple -> RippleAddressValidator.validate(address)
|
||||
// Binance -> BinanceAddressValidator.validate(address)
|
||||
// Stellar -> StellarAddressValidator.validate(address)
|
||||
else -> throw Exception("unsupported blockchain")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ class WalletConfig(
|
|||
|
||||
data class Amount(
|
||||
val currencySymbol: String,
|
||||
val value: BigDecimal?,
|
||||
val address: String,
|
||||
var value: BigDecimal? = null,
|
||||
val address: String? = null,
|
||||
val decimals: Byte,
|
||||
val type: AmountType = AmountType.Coin
|
||||
)
|
||||
|
|
@ -29,11 +29,14 @@ data class TransactionData(
|
|||
val amount: Amount,
|
||||
val fee: Amount?,
|
||||
val sourceAddress: String,
|
||||
val destinationAddress: String
|
||||
val destinationAddress: String,
|
||||
var status: TransactionStatus = TransactionStatus.Uncomfirmed
|
||||
)
|
||||
|
||||
enum class AmountType { Coin, Token, Reserve }
|
||||
|
||||
enum class TransactionStatus {Confirmed, Uncomfirmed}
|
||||
|
||||
enum class ValidationError { WrongAmount, WrongFee, WrongTotal }
|
||||
|
||||
interface TransactionValidator {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.blockchain.common
|
||||
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.commands.SignResponse
|
||||
import com.tangem.tasks.TaskEvent
|
||||
|
||||
|
|
@ -7,22 +9,21 @@ interface WalletManager {
|
|||
var wallet: Wallet
|
||||
val blockchain: Blockchain
|
||||
|
||||
fun update()
|
||||
suspend fun update()
|
||||
}
|
||||
|
||||
interface TransactionEstimator {
|
||||
fun getEstimateSize(transactionData: TransactionData): Int
|
||||
suspend fun getEstimateSize(transactionData: TransactionData): Int
|
||||
}
|
||||
|
||||
interface TransactionSender {
|
||||
fun send(transactionData: TransactionData, signer: TransactionSigner)
|
||||
suspend fun send(transactionData: TransactionData, signer: TransactionSigner) : SimpleResult
|
||||
}
|
||||
|
||||
interface TransactionSigner {
|
||||
fun sign(hashes: Array<ByteArray>, cardId: String,
|
||||
callback: (result: TaskEvent<SignResponse>) -> Unit)
|
||||
suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse>
|
||||
}
|
||||
|
||||
interface FeeProvider {
|
||||
fun getFee(amount: Amount, source: String, destination: String): List<Amount>
|
||||
suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>>
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.blockchain.common
|
||||
|
||||
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
|
||||
import com.tangem.blockchain.eth.Chain
|
||||
import com.tangem.blockchain.eth.EthereumWalletManager
|
||||
import com.tangem.blockchain.stellar.StellarWalletManager
|
||||
import com.tangem.commands.Card
|
||||
|
|
@ -8,8 +9,8 @@ import com.tangem.commands.Card
|
|||
object WalletManagerFactory {
|
||||
|
||||
fun makeWalletManager(card: Card): WalletManager? {
|
||||
val walletPublicKey = card.walletPublicKey ?: return null
|
||||
val blockchainName = card.cardData?.blockchainName ?: return null
|
||||
val walletPublicKey: ByteArray = card.walletPublicKey ?: return null
|
||||
val blockchainName: String = card.cardData?.blockchainName ?: return null
|
||||
|
||||
when {
|
||||
blockchainName.contains("btc") || blockchainName.contains("bitcoin") -> {
|
||||
|
|
@ -17,13 +18,19 @@ object WalletManagerFactory {
|
|||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, true),
|
||||
isTestNet = blockchainName.contains("test"))
|
||||
isTestNet = isTestNet(blockchainName))
|
||||
}
|
||||
blockchainName.contains("eth") -> {
|
||||
val chain = if (isTestNet(blockchainName)) {
|
||||
Chain.EthereumClassicTestnet
|
||||
} else {
|
||||
Chain.EthereumClassicMainnet
|
||||
}
|
||||
return EthereumWalletManager(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, true))
|
||||
walletConfig = WalletConfig(true, true),
|
||||
chain = chain)
|
||||
}
|
||||
blockchainName.contains("xlm") -> {
|
||||
val token = getToken(card)
|
||||
|
|
@ -32,17 +39,19 @@ object WalletManagerFactory {
|
|||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, token == null),
|
||||
token = token,
|
||||
isTestNet = blockchainName.contains("test"))
|
||||
isTestNet = isTestNet(blockchainName))
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTestNet(blockchainName: String) = blockchainName.contains("test")
|
||||
|
||||
private fun getToken(card: Card): Token? {
|
||||
val symbol = card.cardData?.tokenSymbol ?: return null
|
||||
val contractAddress = card.cardData?.tokenContractAddress ?: return null
|
||||
val decimals = card.cardData?.tokenDecimal ?: return null
|
||||
return Token(symbol, contractAddress, decimals)
|
||||
return Token(symbol, contractAddress, decimals.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,5 +59,5 @@ object WalletManagerFactory {
|
|||
data class Token(
|
||||
val symbol: String,
|
||||
val contractAddress: String,
|
||||
val decimals: Int
|
||||
val decimals: Byte
|
||||
)
|
||||
|
|
@ -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,49 @@
|
|||
package com.tangem.blockchain.common.extensions
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.IOException
|
||||
|
||||
suspend fun <T> retryIO(
|
||||
times: Int = Int.MAX_VALUE,
|
||||
initialDelay: Long = 100,
|
||||
maxDelay: Long = 1000,
|
||||
factor: Double = 2.0,
|
||||
block: suspend () -> T
|
||||
): T {
|
||||
var currentDelay = initialDelay
|
||||
repeat(times - 1) {
|
||||
try {
|
||||
return block()
|
||||
} catch (e: IOException) {
|
||||
|
||||
|
||||
}
|
||||
delay(currentDelay)
|
||||
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
|
||||
}
|
||||
return block() // last attempt
|
||||
}
|
||||
|
||||
|
||||
//suspend fun <T: Any> handleRequest(requestFunc: suspend () -> T): Result<T> {
|
||||
// return try {
|
||||
// Result.success(requestFunc.invoke())
|
||||
// } catch (he: HttpException) {
|
||||
// Result.failure(he)
|
||||
//// HttpException
|
||||
//// SocketTimeoutException
|
||||
//// IOException
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
sealed class Result<out T : Any> {
|
||||
data class Success<out T : Any>(val data: T) : Result<T>()
|
||||
data class Failure(val error: Throwable?) : Result<Nothing>()
|
||||
}
|
||||
|
||||
sealed class SimpleResult {
|
||||
object Success : SimpleResult()
|
||||
data class Failure(val error: Throwable?) : SimpleResult()
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.blockchain.common.network
|
||||
|
||||
import com.tangem.blockchain.BuildConfig
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
private val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder().apply {
|
||||
if (BuildConfig.DEBUG) addInterceptor(createHttpLoggingInterceptor())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
val logging = HttpLoggingInterceptor()
|
||||
logging.level = HttpLoggingInterceptor.Level.BODY
|
||||
return logging
|
||||
}
|
||||
|
||||
|
||||
fun createRetrofitInstance(baseUrl: String): Retrofit =
|
||||
Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
|
||||
|
||||
const val API_TANGEM = "https://verify.tangem.com/"
|
||||
const val API_COINMARKETCAP = "https://pro-api.coinmarketcap.com/"
|
||||
const val API_INFURA = "https://mainnet.infura.io/"
|
||||
const val API_SOCHAIN_V2 = "https://chain.so/"
|
||||
const val API_ESTIMATEFEE = "https://estimatefee.com/"
|
||||
const val API_UPDATE_VERSION = "https://raw.githubusercontent.com/"
|
||||
const val API_ROOTSTOCK = "https://public-node.rsk.co/"
|
||||
const val API_BLOCKCYPHER = "https://api.blockcypher.com/"
|
||||
const val API_BINANCE = "https://dex.binance.org/"
|
||||
const val API_BINANCE_TESTNET = "https://testnet-dex.binance.org/"
|
||||
const val API_MATIC_TESTNET = "https://testnet2.matic.network/"
|
||||
const val API_STELLAR = "https://horizon.stellar.org/"
|
||||
const val API_STELLAR_RESERVE = "https://horizon.sui.li/"
|
||||
const val API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/"
|
||||
const val API_BLOCKCHAIN_INFO = "https://blockchain.info/"
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.blockchain.extensions
|
||||
|
||||
import org.spongycastle.crypto.digests.RIPEMD160Digest
|
||||
|
||||
fun ByteArray.calculateRipemd160(): ByteArray {
|
||||
val digest = RIPEMD160Digest()
|
||||
digest.update(this, 0, this.size)
|
||||
val out = ByteArray(20)
|
||||
digest.doFinal(out, 0)
|
||||
return out
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ class CurrencyWallet(
|
|||
override val address: String,
|
||||
override val exploreUrl: String? = null,
|
||||
override val shareUrl: String? = null,
|
||||
val pendingTransactions: List<TransactionData> = listOf(),
|
||||
val balances: MutableList<Amount> = mutableListOf(),
|
||||
val pendingTransactions: MutableList<TransactionData> = mutableListOf(),
|
||||
val balances: MutableMap<AmountType, Amount> = mutableMapOf(),
|
||||
val isTestnet: Boolean = false
|
||||
) : Wallet, TransactionValidator {
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue