Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-20 19:27:46 +03:00
parent 07b30e1f02
commit a4de111290
31 changed files with 2365 additions and 240 deletions

View file

@ -10,6 +10,7 @@ public enum Blockchain {
Bitcoin("BTC", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Bitcoin"),
BitcoinTestNet("BTC/test", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin_testnet, "Bitcoin Testnet"),
Ethereum("ETH", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"),
EthereumId("ETH/ID", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum ID"),
EthereumTestNet("ETH/test", "ETH", 1.0, R.drawable.ic_logo_ethereum_testnet, "Ethereum Testnet"),
Token("Token", "ETH", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"),
NftToken("NftToken", "", 1.0, R.drawable.tangem2, "Ethereum"),

View file

@ -98,6 +98,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
super.onViewCreated(view, savedInstanceState)
rippleBackgroundNfc.startRippleAnimation()
// navigateToDestination(R.id.action_main_to_emptyIdFragment)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
@ -274,13 +275,19 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
card.status == TangemCard.Status.Loaded -> lastTag?.let {
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin != null) {
engineCoin.defineWallet()
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY,
R.id.action_main_to_loadedWalletFragment, bundle)
if (card.isIDCard) {
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateToDestination(R.id.action_main_to_idFragment, bundle)
} else {
engineCoin.defineWallet()
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY,
R.id.action_main_to_loadedWalletFragment, bundle)
}
} else {
showUnkownBlockchainWarning()
}

View file

@ -0,0 +1,46 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.PictureResult
import com.otaliastudios.cameraview.controls.Facing
import com.tangem.ui.fragment.BaseFragment
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_camera.*
class CameraFragment : BaseFragment() {
override val layoutId = R.layout.fragment_camera
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cvCamera?.setLifecycleOwner(viewLifecycleOwner)
cvCamera?.addCameraListener(object : CameraListener() {
override fun onPictureTaken(result: PictureResult) { // Picture was taken!
result.toBitmap(200, 200) {
navigateBackWithResult(Activity.RESULT_OK, bundleOf(PHOTO_KEY to it))
}
}
})
ivTakePicture?.setOnClickListener { cvCamera?.takePictureSnapshot() }
ivFlipCamera?.setOnClickListener { flipCamera() }
}
private fun flipCamera() {
if (cvCamera?.facing == Facing.BACK) {
cvCamera?.facing = Facing.FRONT
} else {
cvCamera?.facing = Facing.BACK
}
}
companion object{
const val PHOTO_KEY = "photo"
}
}

View file

@ -0,0 +1,465 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.BitmapFactory
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.tangem.App
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.data.network.ServerApiCommon
import com.tangem.server_android.ServerApiTangem
import com.tangem.server_android.model.CardVerifyAndGetInfo
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.VerifyCardTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.wallet.LoadedWalletViewModel
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.*
import kotlinx.android.synthetic.main.fragment_id.*
import kotlinx.android.synthetic.main.layout_btn_details.*
import kotlinx.android.synthetic.main.layout_id.*
import kotlinx.android.synthetic.main.layout_tangem_card.*
import kotlinx.android.synthetic.main.layout_tangem_card.rlProgressBar
import net.i2p.crypto.eddsa.Utils
import java.util.*
import kotlin.concurrent.timerTask
class IdFragment : BaseFragment(), NfcAdapter.ReaderCallback,
CardProtocol.Notifications, SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
val TAG: String = IdFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_id
private lateinit var viewModel: LoadedWalletViewModel
private lateinit var ctx: TangemContext
private lateinit var mpSecondScanSound: MediaPlayer
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private var serverApiTangem: ServerApiTangem = ServerApiTangem()
private var lastTag: Tag? = null
private var lastReadSuccess = true
private var verifyCardTask: VerifyCardTask? = null
private var requestPIN2Count = 0
private var timerHideErrorAndMessage: Timer? = null
private var newPIN = ""
private var newPIN2 = ""
private var photo: ByteArray? = null
private var cardProtocol: CardProtocol? = null
private var refreshAction: Runnable? = null
private var hasIdInfo: Boolean? = null
private var requestCounter: Int = 0
set(value) {
field = value
LOG.i(TAG, "requestCounter, set $field")
if (field <= 0) {
LOG.e(TAG, "+++++++++++ FINISH REFRESH")
if (srl != null && srl.isRefreshing)
srl.isRefreshing = false
} else if (srl != null && !srl.isRefreshing)
srl.isRefreshing = true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
lastTag = activity?.intent?.getParcelableExtra(Constant.EXTRA_LAST_DISCOVERED_TAG)
if (arguments?.containsKey(NfcAdapter.EXTRA_TAG) == true) {
val tag = arguments!!.getParcelable<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) onTagDiscovered(tag)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpSecondScanSound = MediaPlayer.create(activity, R.raw.scan_card_sound)
tvIdNumber?.text = "ID # ${Utils.bytesToHex(ctx.card.cid)}"
if (hasIdInfo == false) rlToolButtons?.visibility = View.VISIBLE
// set listeners
srl.setOnRefreshListener { refresh() }
btnDetails.setOnClickListener {
if (cardProtocol != null) {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateForResult(Constant.REQUEST_CODE_VERIFY_CARD,
R.id.action_idFragment_to_verifyCard, bundle)
} else {
(activity as MainActivity).toastHelper
.showSingleToast(context, getString(R.string.general_notification_scan_again_to_verify))
}
}
btnIssueNewId?.setOnClickListener {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateToDestination(R.id.action_idFragment_to_issueNewIdFragment, bundle)
}
if (hasIdInfo == null) tvBalanceLine1?.text = "Reading... Hold the card firmly"
btnNewScan.setOnClickListener {
// navigateToDestination(R.id.action_loadedWalletFragment_to_main)
navigateUp()
}
// request card verify and get info listener
val cardVerifyAndGetInfoListener: ServerApiTangem.CardVerifyAndGetInfoListener = object : ServerApiTangem.CardVerifyAndGetInfoListener {
override fun onSuccess(cardVerifyAndGetArtworkResponse: CardVerifyAndGetInfo.Response?) {
LOG.i(TAG, "cardVerifyAndGetInfoListener onSuccess")
if (activity == null || !UtilHelper.isOnline(activity!!)) return
val result = cardVerifyAndGetArtworkResponse?.results!![0]
if (result.error != null) {
ctx.card!!.isOnlineVerified = false
return
}
ctx.card!!.isOnlineVerified = result.passed
requestCounter--
updateViews()
if (!result.passed) return
if (App.localStorage.checkBatchInfoChanged(ctx.card!!, result)) {
LOG.w(TAG, "Batch ${result.batch} info changed to '$result'")
ivTangemCard.setImageBitmap(App.localStorage.getCardArtworkBitmap(ctx.card!!))
App.localStorage.applySubstitution(ctx.card!!)
refresh()
}
}
override fun onFail(message: String?) {
LOG.i(TAG, "cardVerifyAndGetInfoListener onFail")
if (activity == null || !UtilHelper.isOnline(activity!!)) return
requestCounter--
updateViews()
}
}
serverApiTangem.setCardVerifyAndGetInfoListener(cardVerifyAndGetInfoListener)
refresh()
startVerify(lastTag)
}
override fun onPause() {
super.onPause()
if (timerHideErrorAndMessage != null) {
timerHideErrorAndMessage!!.cancel()
timerHideErrorAndMessage = null
}
}
override fun onStop() {
srl?.removeCallbacks(refreshAction)
super.onStop()
}
override fun onTagDiscovered(tag: Tag) {
startVerify(tag)
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE }
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
verifyCardTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar?.post {
rlProgressBar?.visibility = View.GONE
this.cardProtocol = cardProtocol
if (!cardProtocol.card.isWalletPublicKeyValid)
refresh()
else
updateViews()
if (cardProtocol.card.isIDCard) {
if (cardProtocol.card.hasIDCardData()) {
Toast.makeText(activity, "ID card: ${cardProtocol.card.idCardData.fullName}", Toast.LENGTH_SHORT).show()
ctx.card = cardProtocol.card
tvBalanceLine1?.visibility = View.VISIBLE
val idCardData = ctx.card.idCardData
tvFullName.text = idCardData.fullName
tvBirthDate.text = idCardData.birthday
tvGender.text = "Sex: ${idCardData.gender}"
ivPhoto.setImageBitmap(BitmapFactory.decodeByteArray(idCardData.photo, 0, idCardData.photo.size))
val inputs = "${idCardData.fullName};${idCardData.birthday}${idCardData.gender}"
val info = inputs.toByteArray() + idCardData.photo
ctx.card.idHash = Util.calculateSHA256(info)
refresh()
} else {
Toast.makeText(activity, "Empty ID card", Toast.LENGTH_SHORT).show()
rlToolButtons?.visibility = View.VISIBLE
tvBalanceLine1?.text = "Empty ID card".toUpperCase()
tvBalanceLine1?.setTextSize(18F)
tvBalanceLine1?.visibility = View.VISIBLE
hasIdInfo = false
}
}
mpSecondScanSound.start()
}
} else {
// remove last UIDs because of error and no card read
rlProgressBar?.post {
lastReadSuccess = false
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported)
if (!NoExtendedLengthSupportDialog.allReadyShowed)
activity?.supportFragmentManager?.let { NoExtendedLengthSupportDialog().show(it, NoExtendedLengthSupportDialog.TAG) }
else
Toast.makeText(activity, R.string.general_notification_scan_again, Toast.LENGTH_SHORT).show()
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
verifyCardTask = null
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadWait(msec: Int) {
WaitSecurityDelayDialog.onReadWait(activity as AppCompatActivity?, msec)
}
override fun onReadBeforeRequest(timeout: Int) {
WaitSecurityDelayDialog.onReadBeforeRequest(activity as AppCompatActivity?, timeout)
}
override fun onReadAfterRequest() {
WaitSecurityDelayDialog.onReadAfterRequest(activity)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
}
fun updateViews() {
if (activity == null || this.view == null) return
if (timerHideErrorAndMessage != null) {
timerHideErrorAndMessage!!.cancel()
timerHideErrorAndMessage = null
}
if (ctx.card?.idCardData?.photo != null && (photo == null)) {
photo = ctx.card.idCardData.photo
ivPhoto.setImageBitmap(BitmapFactory.decodeByteArray(photo, 0, photo!!.size))
}
if (ctx.hasError()) {
tvError?.visibility = View.VISIBLE
tvError?.text = ctx.error
} else {
tvError?.visibility = View.GONE
tvError?.text = ""
}
if (ctx.message == null || ctx.message.isEmpty()) {
tvMessage?.text = ""
tvMessage?.visibility = View.GONE
} else {
tvMessage?.text = ctx.message
tvMessage?.visibility = View.VISIBLE
}
if (tvError?.visibility == View.VISIBLE || tvMessage?.visibility == View.VISIBLE) {
timerHideErrorAndMessage = Timer()
timerHideErrorAndMessage?.schedule(
timerTask {
activity?.runOnUiThread {
tvMessage?.visibility = View.GONE
tvError?.visibility = View.GONE
// clear only already viewed messages
if (tvMessage?.text == ctx.message) ctx.message = null
if (tvError?.text == ctx.error) ctx.error = null
}
},
5000)
}
if (srl.isRefreshing && hasIdInfo != null && hasIdInfo != false) {
tvBalanceLine1.setTextColor(resources.getColor(R.color.primary))
tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain)
tvBalanceLine2.text = ""
} else if (ctx.card?.hasIDCardData() != false) {
val validator = BalanceValidator()
// TODO why attest=false?
validator.check(ctx, false)
context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) }
tvBalanceLine1?.text = getString(validator.firstLine)
}
if (ctx.card.hasIDCardData()) {
val idCardData = ctx.card.idCardData
tvFullName.text = idCardData.fullName
tvBirthDate.text = idCardData.birthday
tvGender.text = "Sex: ${idCardData.gender}"
ivPhoto.setImageBitmap(BitmapFactory.decodeByteArray(idCardData.photo, 0, idCardData.photo.size))
rlToolButtons?.visibility = View.GONE
}
}
private fun refresh() {
if (ctx.card == null) return
// clear all card data and request again
ctx.coinData.clearInfo()
ctx.error = null
ctx.message = null
LOG.w(TAG, "============= START REFRESH")
requestCounter = 0
srl?.isRefreshing = true
updateViews()
// Bitcoin, Litecoin, BitcoinCash, Stellar
if (ctx.blockchain == Blockchain.Bitcoin || ctx.blockchain == Blockchain.BitcoinTestNet ||
ctx.blockchain == Blockchain.Litecoin || ctx.blockchain == Blockchain.BitcoinCash ||
ctx.blockchain == Blockchain.Stellar || ctx.blockchain == Blockchain.StellarTestNet || ctx.blockchain == Blockchain.StellarAsset) {
ctx.coinData.setIsBalanceEqual(true)
}
requestVerifyAndGetInfo()
if (ctx.card.hasIDCardData()) requestBalanceAndUnspentTransactions()
if (::viewModel.isInitialized)
viewModel.requestRateInfo(ctx)
if (requestCounter == 0) {
// if no connection and no requests posted
srl?.isRefreshing = false
updateViews()
}
}
private fun requestBalanceAndUnspentTransactions() {
if (UtilHelper.isOnline(context as Activity)) {
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine?.defineWallet()
requestCounter++
coinEngine!!.requestBalanceAndUnspentTransactions(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
LOG.i(TAG, "requestBalanceAndUnspentTransactions onComplete: $success, request counter $requestCounter")
if (activity == null) return
requestCounter--
if (!success) {
LOG.e(TAG, "requestBalanceAndUnspentTransactions ctx.error: " + ctx.error)
}
updateViews()
}
override fun onProgress() {
if (activity == null) return
LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress")
updateViews()
}
override fun allowAdvance(): Boolean {
return try {
context?.let { UtilHelper.isOnline(it) }!!
} catch (e: KotlinNullPointerException) {
e.printStackTrace()
false
}
}
}
)
} else {
ctx.error = getString(R.string.general_error_no_connection)
updateViews()
}
}
private fun requestVerifyAndGetInfo() {
if (UtilHelper.isOnline(context as Activity)) {
if ((ctx.card!!.isOnlineVerified == null || !ctx.card!!.isOnlineVerified)) {
LOG.i(TAG, "requestVerifyAndGetInfo")
requestCounter++
serverApiTangem.cardVerifyAndGetInfo(ctx.card)
}
} else {
ctx.error = getString(R.string.general_error_no_connection)
updateViews()
}
}
private fun startVerify(tag: Tag?) {
try {
val isoDep = IsoDep.get(tag)
val uid = tag?.id
val sUID = Util.byteArrayToHexString(uid)
if (ctx.card.uid != sUID || cardProtocol != null) {
(activity as MainActivity).nfcManager.ignoreTag(isoDep.tag)
return
}
if (lastReadSuccess)
isoDep.timeout = 1000
else
isoDep.timeout = 65000
verifyCardTask = VerifyCardTask(ctx.card, NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, App.firmwaresStorage, this)
verifyCardTask?.start()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View file

@ -0,0 +1,253 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import com.tangem.tangem_card.util.Util
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import com.tangem.wallet.ethID.EthIdEngine
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_issue_new_id.*
import java.io.ByteArrayOutputStream
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*
class IssueNewIdFragment : BaseFragment(), NavigationResultListener {
lateinit var ctx: TangemContext
private var photo: Bitmap? = null
private var photoInBytes: ByteArray? = null
private var gender: Gender? = null
private var userIdData: UserIdData? = null
override val layoutId = R.layout.fragment_issue_new_id
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
ivPhoto?.setOnClickListener {
navigateForResult(GET_PHOTO_REQUEST_CODE, R.id.action_issueNewIdFragment_to_cameraFragment)
}
if (photo != null) ivPhoto?.setImageBitmap(photo)
btnConfirm?.setOnClickListener {
if (!checkIfInfoProvided()) {
Toast.makeText(context, "Fill in all data first", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
try {
processData()
} catch (exception: Exception) {
Toast.makeText(context, "Check for correctness of data", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val bundle = bundleOf(USER_ID_DATA to userIdData)
ctx.saveToBundle(bundle)
navigateToDestination(R.id.action_issueNewIdFragment_to_validateIdFragment, bundle)
}
rbFemale?.setOnClickListener { onRadioButtonClicked(it) }
rbMale?.setOnClickListener { onRadioButtonClicked(it) }
DateInputMask(etBirthDate).listen()
// etBirthDate?.addTextChangedListener(MaskWatcher("##/##/####"))
}
private fun onRadioButtonClicked(view: View) {
if (view is RadioButton) {
when (view.getId()) {
R.id.rbFemale ->
if (view.isChecked) {
gender = Gender.F
}
R.id.rbMale ->
if (view.isChecked) {
gender = Gender.M
}
}
}
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == GET_PHOTO_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
photo = data?.getParcelable<Bitmap>(CameraFragment.PHOTO_KEY)
ivPhoto?.setImageBitmap(photo)
val out = ByteArrayOutputStream()
photo!!.compress(Bitmap.CompressFormat.JPEG, 100, out)
photoInBytes = out.toByteArray()
}
}
}
private fun checkIfInfoProvided(): Boolean {
return (etName.text.isNotBlank() && etSurname.text.isNotBlank() && etBirthDate.text.isNotBlank() &&
(photo != null) && (gender != null))
}
private fun processData() {
val name = etName.text.toString()
val lastName = etSurname.text.toString()
val birthDate = convertDate(etBirthDate.text)
userIdData = UserIdData(name, lastName, birthDate, gender.toString(), photo!!)
val issuerExpireDate = processIssueExpireDate()
ctx.card.setTlvIDCardData(
"$name $lastName",
birthDate,
gender.toString(),
photoInBytes,
issuerExpireDate.issueDate,
issuerExpireDate.expireDate,
(CoinEngineFactory.create(ctx) as? EthIdEngine)?.approvalAddress
)
val inputs = "$name $lastName;$birthDate${gender.toString()}"
val info = inputs.toByteArray() + photoInBytes!!
ctx.card.idHash = Util.calculateSHA256(info)
}
private fun convertDate(inputDate: CharSequence): String {
val calendar = Calendar.getInstance()
calendar.clear()
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH)
try {
calendar.time = sdf.parse(inputDate.toString())
} catch (exception: Error) {
etBirthDate.setTextColor(ContextCompat.getColor(requireContext(), R.color.bg_err))
}
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.timeZone = SimpleTimeZone(0, "UTC")
val dateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH)
dateFormat.timeZone = SimpleTimeZone(0, "UTC")
return dateFormat.format(calendar.time)
}
private fun processIssueExpireDate(): IssuerExpireDate {
val dt = Date()
val issueDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt)
dt.year = dt.year + 10
val expireDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt)
return IssuerExpireDate(issueDate, expireDate)
}
data class IssuerExpireDate(val issueDate: String, val expireDate: String)
companion object {
const val SEPARATOR = ";"
const val COMPUTED_PUB_KEY = "ComputedPubKey"
const val GET_PHOTO_REQUEST_CODE = "GetPhoto"
const val USER_ID_DATA = "UserIdData"
}
}
enum class Gender { M, F }
@Parcelize
data class UserIdData(
val firstName: String,
val lastName: String,
val birthDate: String,
val gender: String,
val photo: Bitmap
) : Parcelable
class DateInputMask(val input: EditText) {
fun listen() {
input.addTextChangedListener(dateEntryWatcher)
}
private val dateEntryWatcher = object : TextWatcher {
var edited = false
val dividerCharacter = "/"
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (edited) {
edited = false
return
}
var working = getEditText()
working = manageDateDivider(working, 2, start, before)
working = manageDateDivider(working, 5, start, before)
edited = true
input.setText(working)
input.setSelection(input.text.length)
}
private fun manageDateDivider(working: String, position: Int, start: Int, before: Int): String {
if (working.length == position) {
return if (before <= position && start < position)
working + dividerCharacter
else
working.dropLast(1)
}
return working
}
private fun getEditText(): String {
return if (input.text.length >= 10)
input.text.toString().substring(0, 10)
else
input.text.toString()
}
override fun afterTextChanged(s: Editable) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
}
}
class MaskWatcher(private val mask: String) : TextWatcher {
private var isRunning = false
private var isDeleting = false
override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
isDeleting = count > after
}
override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(editable: Editable) {
if (isRunning || isDeleting) {
return
}
isRunning = true
val editableLength = editable.length
if (editableLength < mask.length) {
if (mask[editableLength] != '#') {
editable.append(mask[editableLength])
} else if (mask[editableLength - 1] != '#') {
editable.insert(editableLength - 1, mask, editableLength - 1, editableLength)
}
}
if (editableLength > mask.length) editable.delete(editable.lastIndex, editableLength)
isRunning = false
}
companion object {
fun buildCpf(): MaskWatcher {
return MaskWatcher("###.###.###-##")
}
}
}

View file

@ -0,0 +1,323 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
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.tangem.App
import com.tangem.Constant
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.OneTouchSignTask
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.data.PINStorage
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.asBundle
import com.tangem.ui.SignTransactionFragment
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
class ValidateIdFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = ValidateIdFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_validate_id
private lateinit var ctx: TangemContext
private lateinit var mpFinishSignSound: MediaPlayer
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var signTransactionTask: OneTouchSignTask? = null
private lateinit var amount: CoinEngine.Amount
private lateinit var fee: CoinEngine.Amount
private var isIncludeFee = true
private var outAddressStr: String? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
}
override fun onPause() {
signTransactionTask?.cancel(true)
super.onPause()
}
override fun onStop() {
signTransactionTask?.cancel(true)
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
val uid = tag.id
val sUID = Util.byteArrayToHexString(uid)
if (lastReadSuccess)
isoDep.timeout = ctx.card.pauseBeforePIN2 + 5000
else
isoDep.timeout = ctx.card.pauseBeforePIN2 + 65000
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine?.defineWallet()
coinEngine?.setOnNeedSendTransaction { tx ->
if (tx != null) {
val data = Bundle()
ctx.saveToBundle(data)
data.putByteArray(Constant.EXTRA_TX, tx)
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION_,
R.id.action_validateIdFragment_to_writeIdFragment,
data)
}
}
coinEngine?.requestBalanceAndUnspentTransactions(object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean?) {
val transactionToSign = coinEngine.constructTransaction(null, null, true, null)
val transaction = object : OneTouchSignTask.TransactionToSign{
override fun isIssuerCanSignTransaction(card: TangemCard?): Boolean {
return false
}
override fun isIssuerCanSignData(card: TangemCard?): Boolean {
return false
}
override fun getHashAlgToSign(card: TangemCard?): String {
return transactionToSign.hashAlgToSign
}
override fun getRawDataToSign(card: TangemCard?): ByteArray {
return transactionToSign.rawDataToSign
}
override fun isSigningOnCardSupported(card: TangemCard?): Boolean {
return true
}
override fun onSignCompleted(card: TangemCard?, signature: ByteArray?) {
transactionToSign.onSignCompleted(signature)
}
override fun getHashesToSign(card: TangemCard?): Array<ByteArray> {
return transactionToSign.hashesToSign
}
override fun getIssuerTransactionSignature(card: TangemCard?, dataToSignByIssuer: ByteArray?): ByteArray {
return transactionToSign.getIssuerTransactionSignature(dataToSignByIssuer)
}
}
PINStorage.setPIN2(PINStorage.getDefaultPIN2())
signTransactionTask = OneTouchSignTask(NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, this@ValidateIdFragment, transaction)
signTransactionTask?.start()
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return true
}
})
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar?.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
signTransactionTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar?.post { rlProgressBar?.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
} else {
lastReadSuccess = false
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show()
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
signTransactionTask = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
}
}

View file

@ -0,0 +1,285 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
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.tangem.App
import com.tangem.Constant
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.WriteIDCardTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.asBundle
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.tangemAccess.fragment_sign_transaction.*
class WriteIdFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = WriteIdFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_write_id
private lateinit var ctx: TangemContext
private lateinit var tx: ByteArray
private lateinit var mpFinishSignSound: MediaPlayer
private var idWasWritten = false
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var writeIDCardTask: WriteIDCardTask? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
tx = arguments?.getByteArray(Constant.EXTRA_TX)!!
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
tvCardID.text = ctx.card!!.cidDescription
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
tvProgressBar?.text = "Writing..."
}
override fun onPause() {
writeIDCardTask?.cancel(true)
super.onPause()
}
override fun onStop() {
writeIDCardTask?.cancel(true)
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
val uid = tag.id
val sUID = Util.byteArrayToHexString(uid)
if (sUID == ctx.card.uid || !idWasWritten) {
if (lastReadSuccess)
isoDep.timeout = ctx.card.pauseBeforePIN2 + 5000
else
isoDep.timeout = ctx.card.pauseBeforePIN2 + 65000
writeIDCardTask = WriteIDCardTask(ctx.card, NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, ctx.card.idCardData, ctx.card.issuer.privateDataKey, this)
writeIDCardTask?.start()
} else
(activity as MainActivity).nfcManager.ignoreTag(isoDep.tag)
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar?.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
writeIDCardTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar?.post { rlProgressBar?.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
idWasWritten = true
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine!!.requestSendTransaction(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
if (success) {
navigateUp(R.id.main)
} else
navigateUp(R.id.issueNewIdFragment)
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return UtilHelper.isOnline(requireContext())
}
},
tx
)
} else {
lastReadSuccess = false
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
if (!idWasWritten) Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show()
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
writeIDCardTask = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.wallet.cardano.CardanoEngine
import com.tangem.wallet.ducatus.DucatusEngine
import com.tangem.wallet.eos.EosEngine
import com.tangem.wallet.eth.EthEngine
import com.tangem.wallet.ethID.EthIdEngine
import com.tangem.wallet.ltc.LtcEngine
import com.tangem.wallet.matic.MaticTokenEngine
import com.tangem.wallet.nftToken.NftTokenEngine
@ -39,6 +40,7 @@ object CoinEngineFactory {
Blockchain.Bitcoin, Blockchain.BitcoinTestNet -> BtcEngine()
Blockchain.BitcoinCash -> BtcCashEngine()
Blockchain.Ethereum, Blockchain.EthereumTestNet -> EthEngine()
Blockchain.EthereumId -> EthIdEngine()
Blockchain.Token -> TokenEngine()
Blockchain.NftToken -> NftTokenEngine()
Blockchain.Litecoin -> LtcEngine()
@ -67,6 +69,8 @@ object CoinEngineFactory {
BtcEngine(context)
else if (Blockchain.Ethereum == context.blockchain || Blockchain.EthereumTestNet == context.blockchain)
EthEngine(context)
else if (Blockchain.EthereumId == context.blockchain)
EthIdEngine(context)
else if (Blockchain.Token == context.blockchain)
TokenEngine(context)
else if (Blockchain.NftToken == context.blockchain)

View file

@ -36,6 +36,9 @@ public class TangemContext {
return Blockchain.Token;
}
}
if (blockchain == Blockchain.Ethereum && card.isIDCard()) {
return Blockchain.EthereumId;
}
if ((blockchain == Blockchain.Rootstock) && card.isToken()) {
return Blockchain.RootstockToken;
}

View file

@ -4,9 +4,6 @@ import android.net.Uri;
import android.text.InputFilter;
import android.util.Log;
import com.google.common.primitives.Bytes;
import com.tangem.App;
import com.tangem.Constant;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiInfura;
@ -18,11 +15,11 @@ import com.tangem.data.network.model.InfuraResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
import com.tangem.tangem_card.util.Util;
import com.tangem.util.CryptoUtil;
import com.tangem.util.DecimalDigitsInputFilter;
import com.tangem.wallet.BTCUtils;
import com.tangem.wallet.BalanceValidator;
import com.tangem.wallet.BuildConfig;
import com.tangem.wallet.CoinData;
import com.tangem.wallet.CoinEngine;
import com.tangem.wallet.ECDSASignatureETH;
@ -32,7 +29,6 @@ import com.tangem.wallet.R;
import com.tangem.wallet.TangemContext;
import com.tangem.wallet.eth.EthData;
import org.bitcoin.Secp256k1Context;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.SignatureDecodeException;
import org.bitcoinj.crypto.ChildNumber;
@ -44,21 +40,19 @@ import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import io.github.novacrypto.bip32.ExtendedPublicKey;
public class EthIdEngine extends CoinEngine {
private static final String TAG = com.tangem.wallet.eth.EthEngine.class.getSimpleName();
public EthIdData coinData = null;
String approvalAddress = ""; //TODO
byte[] approvalPubKey = new byte[1]; //TODO
byte[] approvalPubKey = Util.hexToBytes("04EAD74FEEE4061044F46B19EB654CEEE981E9318F0C8FE99AF5CDB9D779D2E52BB51EA2D14545E0B323F7A90CF4CC72753C973149009C10DB2D83DCEC28487729"); //TODO
public String approvalAddress = calculateAddress(approvalPubKey); //TODO
public EthIdEngine(TangemContext ctx) throws Exception {
super(ctx);
if (ctx.getCoinData() == null) {
coinData = new EthIdData();
ctx.setCoinData(coinData);
} else if (ctx.getCoinData() instanceof EthData) {
} else if (ctx.getCoinData() instanceof EthIdData) {
coinData = (EthIdData) ctx.getCoinData();
} else {
throw new Exception("Invalid type of Blockchain data for " + this.getClass().getSimpleName());
@ -74,29 +68,29 @@ public class EthIdEngine extends CoinEngine {
}
private int getChainIdNum() {
return ctx.getBlockchain() == Blockchain.Ethereum ? EthTransaction.ChainEnum.Mainnet.getValue() : EthTransaction.ChainEnum.Rinkeby.getValue();
return EthTransaction.ChainEnum.Mainnet.getValue();
}
@Override
public boolean awaitingConfirmation() {
return !coinData.getUnconfirmedTXCount().equals(coinData.getConfirmedTXCount()) || App.pendingTransactionsStorage.hasTransactions(ctx.getCard());
return false;
}
@Override
public Amount getBalance() {
if (!hasBalanceInfo()) {
return null;
}
return convertToAmount(coinData.getBalanceInInternalUnits());
return null;
}
@Override
public String getBalanceHTML() {
Amount balance = getBalance();
if (balance != null) {
return balance.toDescriptionString(getDecimals());
if (coinData != null) {
if (coinData.isHasApprovalTx()) {
return "VERIFIED";
} else {
return "NOT REGISTERED";
}
} else {
return "";
return "NOT REGISTERED";
}
}
@ -107,9 +101,7 @@ public class EthIdEngine extends CoinEngine {
@Override
public boolean isBalanceNotZero() {
if (coinData == null) return false;
if (coinData.getBalanceInInternalUnits() == null) return false;
return coinData.getBalanceInInternalUnits().notZero();
return true;
}
@Override
@ -124,7 +116,7 @@ public class EthIdEngine extends CoinEngine {
@Override
public CoinData createCoinData() {
return new EthData();
return new EthIdData();
}
@Override
@ -132,14 +124,6 @@ public class EthIdEngine extends CoinEngine {
return "";
}
// BigDecimal convertToEth(String value) {
// BigInteger m = new BigInteger(value, 10);
// BigDecimal n = new BigDecimal(m);
// BigDecimal d = n.divide(new BigDecimal("1000000000000000000"));
// d = d.setScale(8, RoundingMode.DOWN);
// return d;
// }
@Override
public boolean validateAddress(String address) {
if (address == null || address.isEmpty()) {
@ -157,43 +141,6 @@ public class EthIdEngine extends CoinEngine {
return true;
}
// public String getBalanceValue(TangemCard mCard) {
// String dec = coinData.getBalanceInInternalUnits();
// BigDecimal d = convertToEth(dec);
// String s = d.toString();
//
// String pattern = "#0.##################"; // If you like 4 zeros
// DecimalFormat myFormatter = new DecimalFormat(pattern);
// String output = myFormatter.format(d);
// return output;
// }
// public static String getAmountEquivalentDescription(Amount amount, double rateValue) {
// if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0)
// return "";
//
// if (rateValue > 0) {
// BigDecimal biRate = new BigDecimal(rateValue);
// BigDecimal exchangeCurs = biRate.multiply(amount);
// exchangeCurs = exchangeCurs.setScale(2, RoundingMode.DOWN);
// return "USD " + exchangeCurs.toString();
// } else {
// return "";
// }
// }
// public static String getAmountEquivalentDescriptionETH(Double amount, float rate) {
// if (amount == 0)
// return "";
// amount = amount / 100000;
// if (rate > 0) {
// return String.format("USD%.2f", amount * rate);
// } else {
// return "";
// }
//
// }
@Override
public String getBalanceEquivalent() {
@ -232,7 +179,7 @@ public class EthIdEngine extends CoinEngine {
@Override
public boolean hasBalanceInfo() {
return coinData.getBalanceInInternalUnits() != null;
return true;
}
@Override
@ -273,88 +220,27 @@ public class EthIdEngine extends CoinEngine {
@Override
public boolean checkNewTransactionAmount(Amount amount) {
if (BuildConfig.FLAVOR == Constant.FLAVOR_TANGEM_CARDANO) {
return true;
}
Amount balance = getBalance();
if (balance == null || amount.compareTo(balance) > 0) {
return false;
}
return true;
}
@Override
public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) {
// Long fee = null;
// Long amount = null;
// try {
// amount = mCard.internalUnitsFromString(amountValue);
// fee = mCard.internalUnitsFromString(feeValue);
// } catch (Exception e) {
// e.printStackTrace();
// return false;
// }
//
// if (fee == null || amount == null)
// return false;
//
// if (fee == 0 || amount == 0)
// return false;
//
//
// if (fee < minFeeInInternalUnits)
// return false;
try {
BigDecimal cardBalance = getBalance();
if (isFeeIncluded && (amount.compareTo(cardBalance) > 0 || amount.compareTo(fee) < 0))
return false;
if (!isFeeIncluded && amount.add(fee).compareTo(cardBalance) > 0)
return false;
} catch (NumberFormatException e) {
e.printStackTrace();
}
return true;
}
@Override
public boolean validateBalance(BalanceValidator balanceValidator) {
if (getBalance() == null) {
if (coinData != null && coinData.isHasApprovalTx()) {
balanceValidator.setScore(100);
balanceValidator.setFirstLine(R.string.verified);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance);
return false;
} else {
balanceValidator.setScore(0);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_unknown_balance);
balanceValidator.setFirstLine(R.string.not_registered);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance);
return false;
}
if (!coinData.getUnconfirmedTXCount().equals(coinData.getConfirmedTXCount())) {
balanceValidator.setScore(0);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_transaction_in_progress);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_wait_for_confirmation);
return false;
}
if (coinData.isBalanceReceived()) {
balanceValidator.setScore(100);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_balance);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain);
if (getBalance().isZero()) {
balanceValidator.setFirstLine(R.string.balance_validator_first_line_empty_wallet);
balanceValidator.setSecondLine(R.string.empty_string);
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
return true;
}
@Override
@ -392,29 +278,7 @@ public class EthIdEngine extends CoinEngine {
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) {
Log.e(TAG, "Construct transaction " + amountValue.toString() + " with fee " + feeValue.toString() + (IncFee ? " including" : " excluding"));
BigInteger nonceValue = coinData.getConfirmedTXCount();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
BigInteger weiFee = convertToInternalAmount(feeValue).toBigIntegerExact();
BigInteger weiAmount = convertToInternalAmount(amountValue).toBigIntegerExact();
if (IncFee) {
weiAmount = weiAmount.subtract(weiFee);
}
BigInteger gasPrice = weiFee.divide(BigInteger.valueOf(21000));
BigInteger gasLimit = BigInteger.valueOf(21000);
Integer chainId = this.getChainIdNum();
String to = targetAddress;
if (to.startsWith("0x") || to.startsWith("0X")) {
to = to.substring(2);
}
final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
final EthTransaction tx = constructIdTxForSign(coinData.getApprovalAddressNonce());
return new SignTask.TransactionToSign() {
@Override
@ -451,14 +315,14 @@ public class EthIdEngine extends CoinEngine {
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64));
s = CryptoUtil.toCanonicalised(s);
boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), pbKey);
boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), approvalPubKey);
if (!f) {
Log.e(this.getClass().getSimpleName() + "-CHECK", "sign Failed.");
}
tx.signature = new ECDSASignatureETH(r, s);
int v = tx.BruteRecoveryID2(tx.signature, for_hash, pbKey);
int v = tx.BruteRecoveryID2(tx.signature, for_hash, approvalPubKey);
if (v != 27 && v != 28) {
Log.e(this.getClass().getSimpleName(), "invalid v");
throw new Exception("Error in " + this.getClass().getSimpleName() + " - invalid v");
@ -486,7 +350,7 @@ public class EthIdEngine extends CoinEngine {
//TODO: change request logic, 2000 tx max
if (blockcypherResponse.getTxrefs() != null) {
for (BlockcypherTxref txref : blockcypherResponse.getTxrefs()) {
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txref.getTx_hash());
serverApiBlockcypher.requestData("ETH", ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txref.getTx_hash());
}
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
@ -494,6 +358,8 @@ public class EthIdEngine extends CoinEngine {
} else {
blockchainRequestsCallbacks.onProgress();
}
} else {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
}
} catch (Exception e) {
e.printStackTrace();
@ -516,7 +382,11 @@ public class EthIdEngine extends CoinEngine {
public void onSuccess(BlockcypherTx response) {
Log.i(TAG, "onSuccess: BlockcypherTx");
try {
if (response.getAddesses().contains(approvalAddress)) {
String address = approvalAddress;
if (address.startsWith("0x") || address.startsWith("0X")) {
address = address.substring(2);
}
if (response.getAddesses().contains(address)) {
coinData.setHasApprovalTx(true);
}
} catch (Exception e) {
@ -543,7 +413,7 @@ public class EthIdEngine extends CoinEngine {
serverApiBlockcypher.setResponseListener(blockcypherListener);
serverApiBlockcypher.setTxResponseListener(txListener);
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
serverApiBlockcypher.requestData("ETH", ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
requestApprovalAddressNonce();
}
@ -599,7 +469,7 @@ public class EthIdEngine extends CoinEngine {
@Override
public void defineWallet() throws CardProtocol.TangemException {
try {
String wallet = calculateAddress(calculateCKDpub(ctx.getIdHash)); //TODO
String wallet = calculateAddress(calculateCKDpub(ctx.getCard().getIdHash()));
ctx.getCoinData().setWallet(wallet);
} catch (Exception e) {
ctx.getCoinData().setWallet("ERROR");
@ -630,7 +500,7 @@ public class EthIdEngine extends CoinEngine {
to = to.substring(2);
}
return EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
return EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
}
private boolean checkSignature(byte[] signature, long nonce) throws SignatureDecodeException {
@ -651,6 +521,7 @@ public class EthIdEngine extends CoinEngine {
Long count = Long.valueOf(pending, 16);
coinData.setApprovalAddressNonce(count);
}
@Override
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
@ -658,6 +529,6 @@ public class EthIdEngine extends CoinEngine {
};
serverApiInfura.setResponseListener(responseListener);
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, coinData.getWallet(), "", "");
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, approvalAddress, "", "");
}
}