Updated on 2026-08-14

This commit is contained in:
Tangem 2020-05-20 14:51:48 +03:00
commit 6d005c98f6
52 changed files with 583 additions and 273 deletions

View file

@ -66,7 +66,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
super.onViewCreated(view, savedInstanceState)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
tvCardID.text = ctx.card.cidDescription
@ -99,7 +99,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
}
override fun onReadWait(msec: Int) {
WaitSecurityDelayDialog.onReadWait(activity, msec)
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
// val readWait = ReadWait()
// readWait.msec = msec
@ -107,7 +107,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
}
override fun onReadBeforeRequest(timeout: Int) {
WaitSecurityDelayDialog.onReadBeforeRequest(activity, timeout)
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
// if (!waitSecurityDelayDialogNew.isAdded)
// waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG)
@ -119,16 +119,16 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
}
override fun onReadAfterRequest() {
WaitSecurityDelayDialog.onReadAfterRequest(activity)
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
// val readAfterRequest = ReadAfterRequest()
// EventBus.getDefault().post(readAfterRequest)
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
progressBar.post {
progressBar?.post {
progressBar.visibility = View.VISIBLE
progressBar.progress = 5
}
@ -179,7 +179,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed)
NoExtendedLengthSupportDialog().show(activity!!.supportFragmentManager, NoExtendedLengthSupportDialog.TAG)
NoExtendedLengthSupportDialog().show(requireActivity().supportFragmentManager, NoExtendedLengthSupportDialog.TAG)
} else
Toast.makeText(context, R.string.general_notification_scan_again_to_verify, Toast.LENGTH_LONG).show()

View file

@ -77,7 +77,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
@ -198,7 +198,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
@ -265,7 +265,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
(activity as MainActivity).toastHelper.showSingleToast(
(activity as? MainActivity)?.toastHelper?.showSingleToast(
context,
getString(R.string.general_notification_scan_again)
)
@ -313,17 +313,17 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
}
}

View file

@ -77,7 +77,7 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener,
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
tvCardID.text = ctx.card!!.cidDescription
@ -137,7 +137,7 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener,
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
@ -279,17 +279,17 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener,
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
}
}

File diff suppressed because one or more lines are too long

View file

@ -9,7 +9,6 @@ import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
@ -80,7 +79,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT), arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY))
@ -165,7 +164,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
@ -234,7 +233,9 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show()
(activity as? MainActivity)?.toastHelper?.showSingleToast(
context, getString(R.string.general_notification_scan_again)
)
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
@ -280,7 +281,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
// if (!waitSecurityDelayDialogNew.isAdded)
// waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG)
@ -293,7 +294,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
// val readAfterRequest = ReadAfterRequest()
// EventBus.getDefault().post(readAfterRequest)
@ -301,7 +302,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
// val readWait = ReadWait()
// readWait.msec = msec

View file

@ -1,68 +1,62 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
import com.tangem.blockchain.common.AddressService
import com.tangem.blockchain.common.Blockchain
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.core.*
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import java.security.MessageDigest
class BitcoinAddressService(private val testNet: Boolean = false): AddressService {
class BitcoinAddressService(private val blockchain: Blockchain) : AddressService {
private val networkParameters: NetworkParameters = when (blockchain) {
Blockchain.Bitcoin -> MainNetParams()
Blockchain.BitcoinTestnet -> TestNet3Params()
Blockchain.Litecoin -> LitecoinMainNetParams()
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
override fun makeAddress(walletPublicKey: ByteArray): 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)
}
val publicKeyHash = walletPublicKey.calculateSha256().calculateRipemd160()
val checksum = byteArrayOf(networkParameters.addressHeader.toByte()).plus(publicKeyHash)
.calculateSha256().calculateSha256()
val result = byteArrayOf(networkParameters.addressHeader.toByte()) + publicKeyHash + checksum.copyOfRange(0, 4)
return Base58.encode(result)
}
override fun validate(address: String): 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)
override fun validate(address: String): Boolean {
return validateLegacyAddress(address) || validateSegwitAddress(address)
}
private fun validateSegwitAddress(address: String): Boolean {
return try {
when (blockchain) {
Blockchain.Bitcoin -> SegwitAddress.fromBech32(MainNetParams(), address)
Blockchain.BitcoinTestnet -> SegwitAddress.fromBech32(TestNet3Params(), address)
Blockchain.Litecoin -> SegwitAddress.fromBech32(LitecoinMainNetParams(), address)
else -> return false
}
true
} catch (e: Exception) {
false
}
}
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 validateLegacyAddress(address: String): Boolean {
return try {
when (blockchain) {
Blockchain.Bitcoin -> LegacyAddress.fromBase58(MainNetParams(), address)
Blockchain.BitcoinTestnet -> LegacyAddress.fromBase58(TestNet3Params(), address)
Blockchain.Litecoin -> LegacyAddress.fromBase58(LitecoinMainNetParams(), address)
else -> return false
}
true
} catch (e: Exception) {
false
}
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
}
}
companion object {
private const val firstLetters = "123nm"
private const val firstLettersNonTestNet = "13"
}
}

View file

@ -1,21 +1,30 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import org.bitcoinj.core.*
import org.bitcoinj.crypto.TransactionSignature
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import org.bitcoinj.script.Script
import org.bitcoinj.script.ScriptBuilder
import java.math.BigDecimal
import java.math.BigInteger
open class BitcoinTransactionBuilder(
private val walletPublicKey: ByteArray, private val testNet: Boolean = false
private val walletPublicKey: ByteArray, blockchain: Blockchain
) {
private lateinit var transaction: Transaction
protected var networkParameters: NetworkParameters? = null
protected var networkParameters = when (blockchain) {
Blockchain.Bitcoin, Blockchain.BitcoinCash -> MainNetParams()
Blockchain.BitcoinTestnet -> TestNet3Params()
Blockchain.Litecoin -> LitecoinMainNetParams()
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
var unspentOutputs: List<BitcoinUnspentOutput>? = null
open fun buildToSign(
@ -25,11 +34,6 @@ open class BitcoinTransactionBuilder(
val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
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() }

View file

@ -74,11 +74,16 @@ open class BitcoinWalletManager(
val minFee = feeResult.data.minimalPerKb.calculateFee(transactionSize)
val normalFee = feeResult.data.normalPerKb.calculateFee(transactionSize)
val priorityFee = feeResult.data.priorityPerKb.calculateFee(transactionSize)
return Result.Success(
listOf(Amount(minFee, blockchain),
Amount(normalFee, blockchain),
Amount(priorityFee, blockchain))
val fees = listOf(Amount(minFee, blockchain),
Amount(normalFee, blockchain),
Amount(priorityFee, blockchain)
)
val minimalFee = transactionSize.movePointLeft(blockchain.decimals())
for (fee in fees) {
if (fee.value!! < minimalFee) fee.value = minimalFee
}
return Result.Success(fees)
}
}
}

View file

@ -1,26 +1,29 @@
package com.tangem.blockchain.blockchains.bitcoin.network
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi
import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi
import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.BlockchainInfoApi
import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.BlockchainInfoProvider
import com.tangem.blockchain.network.blockcypher.BlockcypherApi
import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.EstimatefeeApi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.network.API_BLOCKCHAIN_INFO
import com.tangem.blockchain.network.API_BLOCKCYPHER
import com.tangem.blockchain.network.API_ESTIMATEFEE
import com.tangem.blockchain.network.blockcypher.BlockcypherProvider
import com.tangem.blockchain.network.createRetrofitInstance
import retrofit2.HttpException
import java.io.IOException
import java.math.BigDecimal
class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinProvider {
class BitcoinNetworkManager(blockchain: Blockchain) : BitcoinProvider {
private val blockcypherProvider by lazy {
val api = createRetrofitInstance(API_BLOCKCYPHER)
.create(BlockcypherApi::class.java)
BlockcypherProvider(api, isTestNet)
BlockcypherProvider(api, blockchain)
}
private val blockchainInfoProvider by lazy {
@ -31,10 +34,10 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
BlockchainInfoProvider(api, estimateFeeApi)
}
private var bitcoinProvider: BitcoinProvider = blockchainInfoProvider
private var provider: BitcoinProvider = blockchainInfoProvider
private fun changeProvider() {
bitcoinProvider = if (bitcoinProvider == blockchainInfoProvider) {
provider = if (provider == blockchainInfoProvider) {
blockcypherProvider
} else {
blockchainInfoProvider
@ -42,13 +45,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
}
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
val result = bitcoinProvider.getInfo(address)
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 bitcoinProvider.getInfo(address)
return provider.getInfo(address)
} else {
return result
}
@ -57,13 +60,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
}
override suspend fun getFee(): Result<BitcoinFee> {
val result = bitcoinProvider.getFee()
val result = provider.getFee()
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return bitcoinProvider.getFee()
return provider.getFee()
} else {
return result
}
@ -72,13 +75,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
}
override suspend fun sendTransaction(transaction: String): SimpleResult {
val result = bitcoinProvider.sendTransaction(transaction)
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 bitcoinProvider.sendTransaction(transaction)
return provider.sendTransaction(transaction)
} else {
return result
}

View file

@ -1,7 +1,5 @@
package com.tangem.blockchain.blockchains.bitcoin.network.api
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockchainInfoAddress
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockchainInfoUnspents
import okhttp3.ResponseBody
import retrofit2.http.*

View file

@ -1,8 +1,9 @@
package com.tangem.blockchain.blockchains.bitcoin.network
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi
import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult

View file

@ -1,4 +1,4 @@
package com.tangem.blockchain.blockchains.bitcoin.network.response
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.blockchain.blockchains.bitcoin.network.api
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
import retrofit2.http.GET

View file

@ -12,7 +12,7 @@ import com.tangem.blockchain.network.blockchair.BlockchairProvider
import com.tangem.blockchain.network.createRetrofitInstance
class BitcoinCashNetworkManager : BitcoinProvider {
private val blockchain: Blockchain = Blockchain.BitcoinCash
private val blockchain = Blockchain.BitcoinCash
private val blockchairProvider by lazy {
val api = createRetrofitInstance(API_BLOCKCHAIR).create(BlockchairApi::class.java)

View file

@ -2,6 +2,7 @@ package com.tangem.blockchain.blockchains.bitcoincash
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
@ -13,8 +14,8 @@ import org.bitcoinj.script.ScriptBuilder
import java.math.BigDecimal
import java.math.BigInteger
class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray)
: BitcoinTransactionBuilder(walletPublicKey) {
class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray, blockchain: Blockchain)
: BitcoinTransactionBuilder(walletPublicKey, blockchain) {
private lateinit var transaction: BitcoinCashTransaction
@ -25,7 +26,6 @@ class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray)
val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
networkParameters = NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
transaction = transactionData.toBitcoinCashTransaction(networkParameters, unspentOutputs!!, change)
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }

View file

@ -10,8 +10,8 @@ import java.math.BigDecimal
class BitcoinCashWalletManager(
cardId: String,
wallet: Wallet,
private val transactionBuilder: BitcoinCashTransactionBuilder,
private val networkManager: BitcoinCashNetworkManager
transactionBuilder: BitcoinCashTransactionBuilder,
networkManager: BitcoinCashNetworkManager
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val minimalFee = BigDecimal("0.00001")

View file

@ -22,7 +22,7 @@ class EthereumTransactionBuilder(private val walletPublicKey: ByteArray, blockch
private val chainId = when (blockchain) {
Blockchain.Ethereum -> Chain.Mainnet.id
Blockchain.RSK -> Chain.RskMainnet.id
else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumTransactionBuilder")
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
fun buildToSign(transactionData: TransactionData, nonce: BigInteger?): TransactionToSign? {

View file

@ -23,7 +23,7 @@ class EthereumNetworkManager(blockchain: Blockchain) {
val baseUrl = when (blockchain) {
Blockchain.Ethereum -> API_INFURA + infuraPath
Blockchain.RSK -> API_RSK
else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumNetworkManager")
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
createRetrofitInstance(baseUrl).create(EthereumApi::class.java)
}
@ -31,7 +31,7 @@ class EthereumNetworkManager(blockchain: Blockchain) {
private val apiKey = when (blockchain) {
Blockchain.Ethereum -> INFURA_API_KEY
Blockchain.RSK -> ""
else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumNetworkManager")
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
private val provider: EthereumProvider by lazy { EthereumProvider(api, apiKey) }

View file

@ -0,0 +1,69 @@
package com.tangem.blockchain.blockchains.litecoin;
import org.bitcoinj.core.Utils;
import org.bitcoinj.params.AbstractBitcoinNetParams;
import org.bitcoinj.params.MainNetParams;
import org.spongycastle.util.encoders.Hex;
import static com.google.common.base.Preconditions.checkState;
public class LitecoinMainNetParams extends AbstractBitcoinNetParams {
public static final int MAINNET_MAJORITY_WINDOW = MainNetParams.MAINNET_MAJORITY_WINDOW;
public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = MainNetParams.MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = MainNetParams.MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
public LitecoinMainNetParams() {
super();
id = "org.bitcoinj.litecoin_mainnet";
// Genesis hash is 12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2
packetMagic = 0xfbc0b6db;
maxTarget = Utils.decodeCompactBits(0x1e0fffffL);
port = 9333;
addressHeader = 48;
p2shHeader = 50;
segwitAddressHrp = "ltc";
dumpedPrivateKeyHeader = 176;
spendableCoinbaseDepth = 100;
subsidyDecreaseBlockCount = 840000;
genesisBlock.setTime(1317972665L);
genesisBlock.setDifficultyTarget(0x1e0ffff0L);
genesisBlock.setNonce(2084524493);
String genesisHash = genesisBlock.getHashAsString();
checkState(genesisHash.equals("5155a7ed2219a75c0735c58b5d459c6d07d97917570e27b9d1d4546fb8431381"));
alertSigningKey = Hex.decode("040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9");
majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
majorityWindow = MAINNET_MAJORITY_WINDOW;
dnsSeeds = new String[]{
"dnsseed.litecointools.com",
"dnsseed.litecoinpool.org",
"dnsseed.ltc.xurious.com",
"dnsseed.koin-project.com",
"dnsseed.weminemnc.com"
};
bip32HeaderP2PKHpub = 0x0488B21E;
bip32HeaderP2PKHpriv = 0x0488ADE4;
}
@Override
public String getPaymentProtocolId() {
return PAYMENT_PROTOCOL_ID_MAINNET;
}
private static LitecoinMainNetParams instance;
public static synchronized LitecoinMainNetParams get() {
if (instance == null) {
instance = new LitecoinMainNetParams();
}
return instance;
}
}

View file

@ -0,0 +1,85 @@
package com.tangem.blockchain.blockchains.litecoin
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
import com.tangem.blockchain.network.blockcypher.BlockcypherProvider
import com.tangem.blockchain.network.blockcypher.BlockcypherApi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.network.API_BLOCKCHAIR
import com.tangem.blockchain.network.API_BLOCKCYPHER
import com.tangem.blockchain.network.blockchair.BlockchairApi
import com.tangem.blockchain.network.blockchair.BlockchairProvider
import com.tangem.blockchain.network.createRetrofitInstance
import retrofit2.HttpException
import java.io.IOException
class LitecoinNetworkManager : BitcoinProvider {
private val blockchain = Blockchain.Litecoin
private val blockchairProvider by lazy {
val api = createRetrofitInstance(API_BLOCKCHAIR)
.create(BlockchairApi::class.java)
BlockchairProvider(api, blockchain)
}
private val blockcypherProvider by lazy {
val api = createRetrofitInstance(API_BLOCKCYPHER)
.create(BlockcypherApi::class.java)
BlockcypherProvider(api, blockchain)
}
private var provider: BitcoinProvider = blockchairProvider
private fun changeProvider() {
provider = if (provider == blockchairProvider) blockcypherProvider else blockchairProvider
}
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
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
}
}
}
}
override suspend fun getFee(): Result<BitcoinFee> {
val result = provider.getFee()
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.getFee()
} else {
return result
}
}
}
}
override 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
}
}
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.blockchain.blockchains.litecoin
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.Result
import java.math.BigDecimal
class LitecoinWalletManager(
cardId: String,
wallet: Wallet,
transactionBuilder: BitcoinTransactionBuilder,
networkManager: LitecoinNetworkManager
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val minimalFee = BigDecimal("0.00001")
when (val result = super.getFee(amount, destination)) {
is Result.Success -> {
for (fee in result.data) {
if (fee.value!! < minimalFee) fee.value = minimalFee
}
return result
}
is Result.Failure -> return result
}
}
}

View file

@ -17,6 +17,7 @@ enum class Blockchain(
Bitcoin("BTC", "BTC", "Bitcoin"),
BitcoinTestnet("BTC/test", "BTCt", "Bitcoin Testnet"),
BitcoinCash("BCH", "BCH", "Bitcoin Cash"),
Litecoin("LTC", "LTC", "Litecoin"),
Ethereum("ETH", "ETH", "Ethereum"),
RSK("RSK", "RBTC", "RSK"),
Cardano("CARDANO", "ADA", "Cardano"),
@ -26,7 +27,7 @@ enum class Blockchain(
Stellar("XLM", "XLM", "Stellar");
fun decimals(): Int = when (this) {
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet -> 8
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin -> 8
Cardano, XRP -> 6
Ethereum, RSK -> 18
Stellar -> 7
@ -43,8 +44,7 @@ enum class Blockchain(
private fun getAddressService(): AddressService = when (this) {
Unknown -> throw Exception("unsupported blockchain")
Bitcoin -> BitcoinAddressService()
BitcoinTestnet -> BitcoinAddressService(true)
Bitcoin, BitcoinTestnet, Litecoin -> BitcoinAddressService(this)
BitcoinCash -> BitcoinCashAddressService()
Ethereum, RSK -> EthereumAddressService()
Cardano -> CardanoAddressService()
@ -66,6 +66,7 @@ enum class Blockchain(
Bitcoin -> "https://blockchain.info/address/$address"
BitcoinTestnet -> "https://live.blockcypher.com/btc-testnet/address/$address"
BitcoinCash -> "https://blockchair.com/bitcoin-cash/address/$address"
Litecoin -> "https://live.blockcypher.com/ltc/address/$address"
Cardano -> "https://cardanoexplorer.com/address/$address"
Ethereum -> if (token == null) {
"https://etherscan.io/address/"

View file

@ -15,6 +15,8 @@ import com.tangem.blockchain.blockchains.cardano.network.CardanoNetworkManager
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionBuilder
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.blockchains.litecoin.LitecoinNetworkManager
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarNetworkManager
import com.tangem.blockchain.blockchains.stellar.StellarTransactionBuilder
@ -42,24 +44,31 @@ object WalletManagerFactory {
Blockchain.Bitcoin -> {
return BitcoinWalletManager(
cardId, wallet,
BitcoinTransactionBuilder(walletPublicKey),
BitcoinNetworkManager()
BitcoinTransactionBuilder(walletPublicKey, blockchain),
BitcoinNetworkManager(blockchain)
)
}
Blockchain.BitcoinTestnet -> {
return BitcoinWalletManager(
cardId, wallet,
BitcoinTransactionBuilder(walletPublicKey, true),
BitcoinNetworkManager(true)
BitcoinTransactionBuilder(walletPublicKey, blockchain),
BitcoinNetworkManager(blockchain)
)
}
Blockchain.BitcoinCash -> {
return BitcoinCashWalletManager(
cardId, wallet,
BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey()),
BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey(), blockchain),
BitcoinCashNetworkManager()
)
}
Blockchain.Litecoin -> {
return LitecoinWalletManager(
cardId, wallet,
BitcoinTransactionBuilder(walletPublicKey, blockchain),
LitecoinNetworkManager()
)
}
Blockchain.Ethereum, Blockchain.RSK -> {
return EthereumWalletManager(
cardId, wallet,

View file

@ -15,7 +15,8 @@ import java.math.RoundingMode
class BlockchairProvider(private val api: BlockchairApi, blockchain: Blockchain) : BitcoinProvider {
private val blockchainPath = when (blockchain) {
Blockchain.BitcoinCash -> "bitcoin-cash"
else -> throw Exception("${blockchain.fullName} blockchain is not supported by BlockchairProvider")
Blockchain.Litecoin -> "litecoin"
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
private val decimals = blockchain.decimals()

View file

@ -1,9 +1,6 @@
package com.tangem.blockchain.blockchains.bitcoin.network.api
package com.tangem.blockchain.network.blockcypher
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherResponse
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherTx
import retrofit2.http.*
interface BlockcypherApi {

View file

@ -1,30 +1,35 @@
package com.tangem.blockchain.blockchains.bitcoin.network
package com.tangem.blockchain.network.blockcypher
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherBody
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherResponse
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
import com.tangem.blockchain.network.blockcypher.BlockcypherApi
import com.tangem.blockchain.network.blockcypher.BlockcypherBody
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.retryIO
import com.tangem.common.extensions.hexToBytes
class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : BitcoinProvider {
class BlockcypherProvider(private val api: BlockcypherApi, blockchain: Blockchain) : BitcoinProvider {
private val blockchain = "btc"
private val decimals = Blockchain.Bitcoin.decimals()
private val network = if (isTestNet) {
BlockcypherNetwork.Test.network
} else {
BlockcypherNetwork.Main.network
private val blockchainPath = when (blockchain) {
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "btc"
Blockchain.Litecoin -> "ltc"
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
private val network = when (blockchain) {
Blockchain.BitcoinTestnet -> "test3"
else -> "main"
}
private val decimals = blockchain.decimals()
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
try {
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) }
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchainPath, network, address) }
val unspents = addressData.txrefs?.map {
BitcoinUnspentOutput(
it.amount!!.toBigDecimal().movePointLeft(decimals),
@ -45,7 +50,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
override suspend fun getFee(): Result<BitcoinFee> {
return try {
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) }
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchainPath, network) }
Result.Success(
BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal().movePointLeft(decimals),
receivedFee.normalFeePerKb!!.toBigDecimal().movePointLeft(decimals),
@ -60,7 +65,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
return try {
retryIO {
api.sendTransaction(
blockchain, network, BlockcypherBody(transaction), BlockcypherToken.getToken())
blockchainPath, network, BlockcypherBody(transaction), BlockcypherToken.getToken())
}
SimpleResult.Success
} catch (error: Exception) {
@ -76,9 +81,4 @@ private object BlockcypherToken {
"66a8a37c5e9d4d2c9bb191acfe7f93aa")
fun getToken(): String = tokens.random()
}
private enum class BlockcypherNetwork(val network: String) {
Main("main"),
Test("test3")
}

View file

@ -1,4 +1,4 @@
package com.tangem.blockchain.blockchains.bitcoin.network.response
package com.tangem.blockchain.network.blockcypher
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,12 +1,13 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
class BitcoinAddressTest {
private val addressService = BitcoinAddressService()
private val addressService = BitcoinAddressService(Blockchain.Bitcoin)
@Test
fun makeAddressFromCorrectPublicKey() {

View file

@ -0,0 +1,28 @@
package com.tangem.blockchain.blockchains.litecoin
import com.google.common.truth.Truth
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
class LitecoinAddressTest {
private val addressService = BitcoinAddressService(Blockchain.Litecoin)
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF65".hexToBytes()
val expected = "LeweDi2SMmishGyCqQN2972qNByaSRdcfT"
Truth.assertThat(addressService.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "LeweDi2SMmishGyCqQN2972qNByaSRdcfT"
Truth.assertThat(addressService.validate(address))
.isTrue()
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
import com.tangem.blockchain.blockchains.xrp.XrpWalletManager
import com.tangem.commands.ReadCommand
@ -92,4 +93,15 @@ internal class WalletManagerFactoryTest {
Truth.assertThat(walletManager)
.isInstanceOf(BitcoinCashWalletManager::class.java)
}
@Test
fun createLitecoinWalletManager() {
val data = "0108BB00000000000023200754414E47454D00020102800A322E3432642053444B000341043539F86A40ADD04CE165764A761FD3E4D251028615D2A573B1C3AE652E60AFDBFAF02E3239E89EF2C43FA448A327557ADC5AF36376A0574570F6DBD20113514A0A04041E76310C618102FFFF8A0101820407E40414830B54414E47454D2053444B0084034C5443864004BDEAD0117544886346CB47F7CA84ABA8C34239502F23D28595A4B16CAD72F7DE506BA818B86A649C2BB945986D4574993B3B755B47CBEE31C4FB931F6748183041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A00701006041044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF6562040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(LitecoinWalletManager::class.java)
}
}

View file

@ -284,7 +284,7 @@ public class TLV {
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), ProductMask.getDescription(iValue));
} catch (Exception e) {
e.printStackTrace();
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value));
return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value));
}
} else {
return String.format("%s[]: [[NULL]]", tag.name());

View file

@ -19,6 +19,8 @@ import com.tangem.crypto.pbkdf2Hash
*/
interface CardSessionRunnable<T : CommandResponse> {
val performPreflightRead: Boolean
/**
* The starting point for custom business logic.
* Implement this interface and use [TangemSdk.startSessionWithRunnable] to run.
@ -55,6 +57,8 @@ class CardSession(
*/
private var isBusy = false
private var performPreflightRead = true
/**
* This metod starts a card session, performs preflight [ReadCommand],
* invokes [CardSessionRunnable.run] and closes the session.
@ -64,6 +68,8 @@ class CardSession(
fun <T : CardSessionRunnable<R>, R : CommandResponse> startWithRunnable(
runnable: T, callback: (result: CompletionResult<R>) -> Unit) {
performPreflightRead = runnable.performPreflightRead
start { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
@ -104,6 +110,11 @@ class CardSession(
callback(this, error)
}
if (!performPreflightRead) {
callback(this, null)
return
}
preflightRead() { result ->
when (result) {
is CompletionResult.Failure -> {
@ -174,7 +185,7 @@ class CardSession(
* Stops the current session on error.
* @param error An error that will be shown.
*/
private fun stopWithError(error: Exception) {
private fun stopWithError(error: TangemSdkError) {
if (!isBusy) return
reader.closeSession()
@ -187,7 +198,7 @@ class CardSession(
}
if (error !is TangemSdkError.UserCancelled) {
Log.e(tag, "Finishing with error: $errorMessage")
viewDelegate.onError(errorMessage)
viewDelegate.onError(error)
} else {
Log.i(tag, "User cancelled NFC session")
}

View file

@ -40,7 +40,7 @@ interface SessionViewDelegate {
/**
* It is called when some error occur during NFC session.
*/
fun onError(errorMessage: String)
fun onError(error: TangemSdkError)
/**
* It is called when a user is expected to enter pin code.

View file

@ -307,7 +307,7 @@ class TangemSdk(
* card response in the form of [DepersonalizeResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
* */
fun depersonalize(cardId: String, initialMessage: Message? = null,
fun depersonalize(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit) {
startSessionWithRunnable(DepersonalizeCommand(), cardId, initialMessage, callback)
}

View file

@ -101,7 +101,10 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
* Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand].
* This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign.
*/
class TooManyhHashesInOneTransaction : TangemSdkError(40906)
class TooManyHashesInOneTransaction : TangemSdkError(40906)
//Write Extra Issuer Data Errors
class ExendedDataSizeTooLarge : TangemSdkError(41101)
//General Errors
class NotPersonalized() : TangemSdkError(40001)
@ -114,6 +117,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
*/
class VerificationFailed : TangemSdkError(40005)
class DataSizeTooLarge : TangemSdkError(40006)
/**
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
* (when the card's requires it), but the counter is missing.

View file

@ -19,6 +19,8 @@ interface CommandResponse
*/
abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
override val performPreflightRead: Boolean = true
/**
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
* then creates [CommandApdu] with this data.

View file

@ -112,7 +112,7 @@ class SignCommand(private val hashes: Array<ByteArray>)
private fun checkForErrors() {
if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes()
if (hashes.size > 10) throw TangemSdkError.TooManyhHashesInOneTransaction()
if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction()
if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual()
}

View file

@ -94,7 +94,7 @@ class WriteIssuerExtraDataCommand(
return true
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge()))
return true
}
if (!isCounterValid(issuerDataCounter, card)) {

View file

@ -1,12 +1,8 @@
package com.tangem.commands.personalization
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -21,17 +17,19 @@ data class DepersonalizeResponse(val success: Boolean) : CommandResponse
*/
class DepersonalizeCommand : Command<DepersonalizeResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
return true
}
return false
}
override val performPreflightRead = false
// override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
// if (session.environment.card?.status == CardStatus.NotPersonalized) {
// callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
// return true
// }
// if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
// callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
// return true
// }
// return false
// }
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(

View file

@ -11,6 +11,8 @@ import com.tangem.common.CompletionResult
class CreateWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val performPreflightRead = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val curve = session.environment.card?.curve
if (curve == null) {

View file

@ -13,6 +13,8 @@ import com.tangem.common.CompletionResult
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val performPreflightRead = true
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card

View file

@ -9,8 +9,8 @@ android {
applicationId "com.tangem.devkit"
minSdkVersion 21
targetSdkVersion 29
versionCode 3
versionName "1.1"
versionCode 4
versionName "1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View file

@ -1,26 +1,14 @@
package com.tangem.devkit.ucase.domain.actions
import com.tangem.devkit._arch.structure.Id
import com.tangem.devkit._arch.structure.PayloadHolder
import com.tangem.devkit._arch.structure.abstraction.findItem
import com.tangem.devkit.ucase.domain.paramsManager.ActionCallback
import com.tangem.devkit.ucase.variants.TlvId
import com.tangem.devkit.ucase.domain.paramsManager.triggers.afterAction.AfterScanModifier
/**
[REDACTED_AUTHOR]
*/
class DepersonalizeAction : BaseAction() {
override fun executeMainAction(payload: PayloadHolder, attrs: AttrForAction, callback: ActionCallback) {
val item = attrs.itemList.findItem(TlvId.CardId) ?: return
val cardId = item.viewModel.data as? String ?: return
attrs.tangemSdk.depersonalize(cardId) { handleResult(payload, it, null, attrs, callback) }
}
override fun getActionByTag(payload: PayloadHolder, id: Id, attrs: AttrForAction): ((ActionCallback) -> Unit)? {
return when (id) {
TlvId.CardId -> { callback -> ScanAction().executeMainAction(payload, attrs, callback) }
else -> null
}
attrs.tangemSdk.depersonalize { handleResult(payload, it, AfterScanModifier(), attrs, callback) }
}
}

View file

@ -10,13 +10,7 @@ import com.tangem.devkit.ucase.variants.TlvId
*/
class ScanItemsManager : BaseItemsManager(ScanAction())
class DepersonalizeItemsManager : BaseItemsManager(DepersonalizeAction()) {
init {
setItemChangeConsequences(CardIdConsequence())
setItems(listOf(EditTextItem(TlvId.CardId, null)))
}
}
class DepersonalizeItemsManager : BaseItemsManager(DepersonalizeAction())
class SignItemsManager : BaseItemsManager(SignAction()) {

View file

@ -1,8 +1,18 @@
package com.tangem.devkit.ucase.variants.depersonalize.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import com.tangem.commands.Card
import com.tangem.devkit.R
import com.tangem.devkit.ucase.domain.paramsManager.ItemsManager
import com.tangem.devkit.ucase.domain.paramsManager.managers.DepersonalizeItemsManager
import com.tangem.devkit.ucase.ui.BaseCardActionFragment
import com.tangem.devkit.ucase.variants.responses.ui.ResponseFragment
import ru.dev.gbixahue.eu4d.lib.android._android.views.dpToPx
/**
[REDACTED_AUTHOR]
@ -10,4 +20,37 @@ import com.tangem.devkit.ucase.ui.BaseCardActionFragment
class DepersonalizeActionFragment : BaseCardActionFragment() {
override val itemsManager: ItemsManager by lazy { DepersonalizeItemsManager() }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val howToUseView = createHowToUse()
contentContainer.layoutParams = FrameLayout.LayoutParams(-1, -1, Gravity.CENTER)
contentContainer.addView(howToUseView)
}
private fun createHowToUse(): View {
val fl = FrameLayout(requireContext())
fl.layoutParams = FrameLayout.LayoutParams(-1, -1, Gravity.CENTER)
val tv = TextView(requireContext()).apply {
val padding = dpToPx(16f).toInt()
setPadding(padding, 0, padding, 0)
gravity = Gravity.CENTER
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
setText(R.string.htu_depersonalize_action)
}
fl.addView(tv)
return fl
}
override fun initViews() {
swrLayout.isEnabled = false
actionFab.setOnClickListener { actionVM.invokeMainAction() }
}
override fun handleResponseCardData(card: Card) {
super.handleResponseCardData(card)
val bundle = ResponseFragment.setTittle(R.string.fg_name_response_depersonalization)
navigateTo(R.id.action_nav_card_action_to_response_screen, bundle, null)
}
}

View file

@ -38,10 +38,11 @@ class ItemTypes {
)
val hiddenList = mutableListOf<Id>(
CardNumberId.Series, CardNumberId.BatchId, PinsId.Pin3, SigningMethodId.SignExternal,
CardNumberId.Series, CardNumberId.BatchId, SigningMethodId.SignExternal,
SignHashExPropId.CryptoExKey, SignHashExPropId.CheckPin3, SettingsMaskId.OneApdu,
SettingsMaskId.UseBlock, SettingsMaskId.ProtectIssuerDataAgainstReplay,
SignHashExPropId.RequireTerminalCertSig, SignHashExPropId.RequireTerminalTxSig
SignHashExPropId.RequireTerminalCertSig, SignHashExPropId.RequireTerminalTxSig,
BlockId.Pins, PinsId.Pin, PinsId.Pin2, PinsId.Pin3, PinsId.Cvc
)
val oftenUsedList = listOf<Id>(

View file

@ -30,5 +30,6 @@
<string name="info_action_user_write_data"> This command writes to the card User Data and User Counter fields.</string>
<string name="info_action_user_write_protected_data"> This command writes to the card User Protected Data and User Protected Counter fields.</string>
<string name="htu_depersonalize_action">To depersonalize the card, press the button at the right bottom corner of the screen</string>
</resources>

View file

@ -6,12 +6,10 @@ import android.view.HapticFeedbackConstants
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.Log
import com.tangem.LoggerInterface
import com.tangem.Message
import com.tangem.SessionViewDelegate
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.hide
import com.tangem.tangem_sdk_new.extensions.localizedDescription
import com.tangem.tangem_sdk_new.extensions.show
import com.tangem.tangem_sdk_new.nfc.NfcReader
import com.tangem.tangem_sdk_new.ui.TouchCardAnimation
@ -142,14 +140,17 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
postUI(300) { readingDialog?.dismiss() }
}
override fun onError(errorMessage: String) {
override fun onError(error: TangemSdkError) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.hide()
readingDialog?.flError?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
readingDialog?.tvTaskText?.text = errorMessage
readingDialog?.tvTaskText?.text = activity.getString(
R.string.error_message,
error.code.toString(), activity.getString(error.localizedDescription())
)
performHapticFeedback()
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.tangem_sdk_new.extensions
import com.tangem.TangemSdkError
import com.tangem.tangem_sdk_new.R
fun TangemSdkError.localizedDescription(): Int {
return when (this) {
is TangemSdkError.TagLost -> R.string.error_tag_lost
is TangemSdkError.ExtendedLengthNotSupported -> R.string.error_operation
is TangemSdkError.SerializeCommandError -> R.string.error_operation
is TangemSdkError.DeserializeApduFailed -> R.string.error_operation
is TangemSdkError.EncodingFailedTypeMismatch -> R.string.error_operation
is TangemSdkError.EncodingFailed -> R.string.error_operation
is TangemSdkError.DecodingFailedMissingTag -> R.string.error_operation
is TangemSdkError.DecodingFailedTypeMismatch -> R.string.error_operation
is TangemSdkError.DecodingFailed -> R.string.error_operation
is TangemSdkError.UnknownStatus -> R.string.error_operation
is TangemSdkError.ErrorProcessingCommand -> R.string.error_operation
is TangemSdkError.InvalidState -> R.string.error_operation
is TangemSdkError.InsNotSupported -> R.string.error_operation
is TangemSdkError.InvalidParams -> R.string.error_operation
is TangemSdkError.NeedEncryption -> R.string.error_operation
is TangemSdkError.AlreadyPersonalized -> R.string.error_already_personalized
is TangemSdkError.CannotBeDepersonalized -> R.string.error_cannot_be_depersonalized
is TangemSdkError.Pin1Required -> R.string.error_operation
is TangemSdkError.AlreadyCreated -> R.string.error_already_created
is TangemSdkError.PurgeWalletProhibited -> R.string.error_purge_prohibited
is TangemSdkError.Pin1CannotBeChanged -> R.string.error_pin1_cannot_be_changed
is TangemSdkError.Pin2CannotBeChanged -> R.string.error_pin2_cannot_be_changed
is TangemSdkError.Pin1CannotBeDefault -> R.string.error_pin1_cannot_be_default
is TangemSdkError.NoRemainingSignatures -> R.string.error_no_remaining_signatures
is TangemSdkError.EmptyHashes -> R.string.error_empty_hashes
is TangemSdkError.HashSizeMustBeEqual -> R.string.error_cannot_be_signed
is TangemSdkError.CardIsEmpty -> R.string.error_card_is_empty
is TangemSdkError.SignHashesNotAvailable -> R.string.error_cannot_be_signed
is TangemSdkError.TooManyHashesInOneTransaction -> R.string.error_cannot_be_signed
is TangemSdkError.NotPersonalized -> R.string.error_not_personalized
is TangemSdkError.NotActivated -> R.string.error_not_activated
is TangemSdkError.CardIsPurged -> R.string.error_purged
is TangemSdkError.Pin2OrCvcRequired -> R.string.error_operation
is TangemSdkError.VerificationFailed -> R.string.error_verification_failed
is TangemSdkError.DataSizeTooLarge -> R.string.error_data_size_too_large
is TangemSdkError.ExendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended
is TangemSdkError.MissingCounter -> R.string.error_missing_counter
is TangemSdkError.OverwritingDataIsProhibited -> R.string.error_data_cannot_be_written
is TangemSdkError.DataCannotBeWritten -> R.string.error_data_cannot_be_written
is TangemSdkError.MissingIssuerPubicKey -> R.string.error_missing_issuer_public_key
is TangemSdkError.UnknownError -> R.string.error_operation
is TangemSdkError.UserCancelled -> R.string.error_user_cancelled
is TangemSdkError.Busy -> R.string.error_busy
is TangemSdkError.MissingPreflightRead -> R.string.error_operation
is TangemSdkError.WrongCardNumber -> R.string.error_wrong_card_number
is TangemSdkError.WrongCardType -> R.string.error_wrong_card_type
is TangemSdkError.CardError -> R.string.error_card_error
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.toHexString
/**
* Provides NFC communication between an Android application and Tangem card.
@ -76,7 +77,9 @@ class NfcReader : CardReader {
val rawResponse: ByteArray?
try {
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data?.size}")
Log.v(this::class.simpleName!!, "Raw data that is to be send to the card: ${data?.toHexString()}")
rawResponse = isoDep?.transceive(data)
Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}")
} catch (exception: TagLostException) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
isoDep = null

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_message" translatable="false">%s: %s</string>
<string name="error_operation">An error occurred during the operation</string>
<string name="error_tag_lost">Tag lost.</string>
<string name="error_already_personalized">This card has already been personalized</string>
<string name="error_cannot_be_depersonalized">This card cannot be depersonalized</string>
<string name="error_already_created">A wallet has already been created</string>
<string name="error_purge_prohibited">This cards configuration does not allow you to erase the wallet</string>
<string name="error_pin1_cannot_be_changed">PIN1 Access Code cannot be changed</string>
<string name="error_pin2_cannot_be_changed">PIN2 Passcode cannot be changed</string>
<string name="error_pin1_cannot_be_default"> PIN1 Access Code cannot be changed to the default value. Please create a new PIN1 Access Code.</string>
<string name="error_no_remaining_signatures"> You cannot sign data with this wallet because the card has reached its signature limit</string>
<string name="error_empty_hashes">There is nothing to sign here</string>
<string name="error_cannot_be_signed"> This data cannot be signed</string>
<string name="error_card_is_empty">Please create a wallet to sign data</string>
<string name="error_not_personalized">This card has not been personalized</string>
<string name="error_not_activated">This card has not been activated</string>
<string name="error_purged">You cannot sign data with this wallet because the card has been purged</string>
<string name="error_verification_failed">Data verification failed</string>
<string name="error_data_size_too_large">Data size exceeds the max limit of 512 bytes</string>
<string name="error_data_size_too_large_extended">Data size exceeds the max limit of 32 Kbytes</string>
<string name="error_missing_counter">The data cannot be written to the card because there is no counter</string>
<string name="error_data_cannot_be_written">The data cannot be written to the card</string>
<string name="error_missing_issuer_public_key">The issuer public key is missing</string>
<string name="error_user_cancelled">The user cancelled the operation</string>
<string name="error_busy">The CardSession is running a command. Please wait and try again</string>
<string name="error_wrong_card_number">You tapped a different card. Please match the in-app Card ID to the your physical Card ID to continue this process.</string>
<string name="error_wrong_card_type">This card is configured for a different app. Please see the information on your card and download the related app.</string>
<string name="error_card_error">Your card is missing essential data</string>
</resources>