Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-27 17:46:39 +03:00
commit 8ad3d8fb6f
66 changed files with 3819 additions and 393 deletions

195
.idea/navEditor.xml generated
View file

@ -8,13 +8,25 @@
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="cameraFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-397" />
<option name="y" value="645" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="confirmTransactionFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="516" />
<option name="y" value="408" />
<option name="x" value="744" />
<option name="y" value="2390" />
</Point>
</option>
<option name="myPositions">
@ -39,8 +51,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="324" />
<option name="y" value="-682" />
<option name="x" value="500" />
<option name="y" value="105" />
</Point>
</option>
</LayoutPositions>
@ -51,8 +63,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="56" />
<option name="y" value="-379" />
<option name="x" value="256" />
<option name="y" value="105" />
</Point>
</option>
<option name="myPositions">
@ -77,13 +89,65 @@
</LayoutPositions>
</value>
</entry>
<entry key="idFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-272" />
<option name="y" value="1404" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_idFragment_to_issueNewIdFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_idFragment_to_verifyCard">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="issueNewIdFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-394" />
<option name="y" value="1004" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_issueNewIdFragment_to_cameraFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_issueNewIdFragment_to_validateIdFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="loadedWalletFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-91" />
<option name="y" value="411" />
<option name="x" value="256" />
<option name="y" value="1279" />
</Point>
</option>
<option name="myPositions">
@ -128,8 +192,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-642" />
<option name="y" value="5" />
<option name="x" value="256" />
<option name="y" value="2390" />
</Point>
</option>
<option name="myPositions">
@ -154,8 +218,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-425" />
<option name="y" value="250" />
<option name="x" value="12" />
<option name="y" value="1279" />
</Point>
</option>
<option name="myPositions">
@ -165,6 +229,11 @@
<LayoutPositions />
</value>
</entry>
<entry key="action_main_to_idFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_main_to_loadedWalletFragment">
<value>
<LayoutPositions />
@ -190,6 +259,11 @@
<LayoutPositions />
</value>
</entry>
<entry key="action_main_to_tagFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
@ -200,8 +274,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="416" />
<option name="y" value="-239" />
<option name="x" value="988" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
@ -212,8 +286,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-274" />
<option name="y" value="-387" />
<option name="x" value="256" />
<option name="y" value="461" />
</Point>
</option>
</LayoutPositions>
@ -224,8 +298,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="76" />
<option name="y" value="811" />
<option name="x" value="744" />
<option name="y" value="1039" />
</Point>
</option>
</LayoutPositions>
@ -236,8 +310,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="280" />
<option name="y" value="1172" />
<option name="x" value="500" />
<option name="y" value="1992" />
</Point>
</option>
<option name="myPositions">
@ -257,8 +331,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="9" />
<option name="y" value="1151" />
<option name="x" value="500" />
<option name="y" value="1618" />
</Point>
</option>
</LayoutPositions>
@ -269,8 +343,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-214" />
<option name="y" value="1097" />
<option name="x" value="500" />
<option name="y" value="1261" />
</Point>
</option>
</LayoutPositions>
@ -281,8 +355,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="411" />
<option name="x" value="500" />
<option name="y" value="2348" />
</Point>
</option>
<option name="myPositions">
@ -307,8 +381,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="941" />
<option name="y" value="15" />
<option name="x" value="744" />
<option name="y" value="682" />
</Point>
</option>
<option name="myPositions">
@ -328,8 +402,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="328" />
<option name="y" value="807" />
<option name="x" value="744" />
<option name="y" value="2034" />
</Point>
</option>
</LayoutPositions>
@ -340,8 +414,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="1011" />
<option name="y" value="412" />
<option name="x" value="1232" />
<option name="y" value="2433" />
</Point>
</option>
</LayoutPositions>
@ -352,8 +426,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-496" />
<option name="y" value="-375" />
<option name="x" value="256" />
<option name="y" value="923" />
</Point>
</option>
</LayoutPositions>
@ -364,8 +438,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="760" />
<option name="y" value="412" />
<option name="x" value="988" />
<option name="y" value="2433" />
</Point>
</option>
<option name="myPositions">
@ -380,13 +454,46 @@
</LayoutPositions>
</value>
</entry>
<entry key="tagFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="1635" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="validateIdFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-628" />
<option name="y" value="1001" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_validateIdFragment_to_writeIdFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="verifyCard">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="78" />
<option name="y" value="32" />
<option name="x" value="500" />
<option name="y" value="860" />
</Point>
</option>
<option name="myPositions">
@ -411,6 +518,18 @@
</LayoutPositions>
</value>
</entry>
<entry key="writeIdFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-661" />
<option name="y" value="701" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>

View file

@ -164,4 +164,7 @@ dependencies {
implementation 'io.jafka:jeos:0.9.15'
// implementation 'one.block:eosiojava:0.1.0'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.61'
implementation 'io.fotoapparat:fotoapparat:2.6.1'
implementation 'com.otaliastudios:cameraview:2.6.1'
}

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

@ -35,7 +35,10 @@ data class BlockcypherTxref(
data class BlockcypherTx(
@SerializedName("hex")
var hex: String? = null
var hex: String? = null,
@SerializedName("addresses")
var addesses: List<String>? = null
)
data class BlockcypherFee(

View file

@ -7,7 +7,6 @@ import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.Ndef
import android.nfc.tech.NfcV
import android.os.Bundle
import android.text.Spannable
@ -55,7 +54,7 @@ import com.tangem.wallet.BuildConfig
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import com.tangem.wallet.xlmtag.XlmTagEngine
import com.tangem.wallet.xlmTag.XlmTagEngine
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.coroutines.CoroutineScope
@ -98,6 +97,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 +274,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

@ -20,7 +20,7 @@ import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.*
import com.tangem.wallet.xlmtag.XlmTagEngine
import com.tangem.wallet.xlmTag.XlmTagEngine
import kotlinx.android.synthetic.main.fr_loaded_wallet.*
import kotlinx.android.synthetic.main.layout_btn_details.*
import kotlinx.android.synthetic.main.layout_tangem_card.*
@ -67,7 +67,7 @@ class TagFragment : BaseFragment(), NavigationResultListener {
btnExtract.backgroundTintList =
ContextCompat.getColorStateList(requireContext(), R.color.btn_dark)
ivTangemCard.setImageResource(R.drawable.card_default_nft)
ivTangemCard.setImageResource(R.drawable.card_tgslix)
tvBalance.setSingleLine(!engine.needMultipleLinesForBalance())
tvWallet.text = ctx.coinData.wallet
@ -78,7 +78,6 @@ class TagFragment : BaseFragment(), NavigationResultListener {
srl?.setOnRefreshListener { refresh(true) }
requestBalanceAndUnspentTransactions()
// update()
}
@ -129,14 +128,13 @@ class TagFragment : BaseFragment(), NavigationResultListener {
else -> tvBalance.text = ""
}
if (ctx.card!!.tokenSymbol.length > 1) {
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(ctx.blockchainName)
tvBlockchain.text = html
} else
tvBlockchain.text = ctx.blockchainName
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(ctx.blockchainName)
tvBlockchain.text = html
}

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,462 @@
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 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 toast: Toast? = 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 {
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)
toast?.cancel()
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()) {
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 {
rlToolButtons?.visibility = View.VISIBLE
tvBalanceLine1?.text = resources.getString(R.string.id_empty).toUpperCase(Locale.ENGLISH)
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 = Toast.makeText(activity, R.string.general_notification_scan_again, Toast.LENGTH_SHORT)
toast?.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 && ctx.card?.hasIDCardData() != 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 (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,292 @@
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 toast: Toast? = null
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)
toast?.cancel()
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 {
toast?.cancel()
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 = Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_SHORT)
toast?.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)
}
}

File diff suppressed because one or more lines are too long

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
@ -19,7 +20,7 @@ import com.tangem.wallet.token.TokenEngine
import com.tangem.wallet.tezos.TezosEngine
import com.tangem.wallet.xlm.XlmAssetEngine
import com.tangem.wallet.xlm.XlmEngine
import com.tangem.wallet.xlmtag.XlmTagEngine
import com.tangem.wallet.xlmTag.XlmTagEngine
import com.tangem.wallet.xrp.XrpEngine
/**
@ -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;
}
@ -55,14 +58,16 @@ public class TangemContext {
public String getBlockchainName() {
Blockchain blockchain = getBlockchain();
if (blockchain == Blockchain.Token || blockchain == Blockchain.RootstockToken) {
String token = card.getTokenSymbol();
return token + " <br><small><small> " + getBlockchain().getOfficialName() + " smart contract token</small></small>";
return card.getTokenSymbol()+ "<br><small><small> " + getBlockchain().getOfficialName() + " smart contract token</small></small>";
}
if (blockchain == Blockchain.NftToken) {
return card.getTokenSymbol().substring(4) + " <br><small><small> " + getBlockchain().getOfficialName() + " NFT token</small></small>";
return card.getTokenSymbol().substring(4) + "<br><small><small> " + getBlockchain().getOfficialName() + " non-fungible token</small></small>";
}
if (blockchain == Blockchain.StellarAsset) {
return card.getTokenSymbol() + " <br><small><small> " + getBlockchain().getOfficialName() + " asset</small></small>";
return card.getTokenSymbol() + "<br><small><small> " + getBlockchain().getOfficialName() + " asset</small></small>";
}
if (blockchain == Blockchain.StellarTag) {
return "TANGEM TAG<br><small><small> "+ getBlockchain().getOfficialName() + " non-fungible token </small></small>";
}
return blockchain.getOfficialName();
}

View file

@ -275,11 +275,11 @@ public class BtcCashEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -247,11 +247,11 @@ public class BinanceEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.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_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -310,11 +310,11 @@ public class BtcEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -251,11 +251,11 @@ public class CardanoEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -263,11 +263,11 @@ public class DucatusEngine extends BtcEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -293,11 +293,11 @@ public class EosEngine extends CoinEngine {
}
}
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);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// 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;

View file

@ -334,11 +334,11 @@ public class EthEngine extends CoinEngine {
}
}
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);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// 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;

View file

@ -0,0 +1,68 @@
package com.tangem.wallet.ethID;
import android.os.Bundle;
import android.util.Log;
import com.tangem.wallet.CoinData;
public class EthIdData extends CoinData {
private boolean hasApprovalTx = false;
private Long approvalAddressNonce;
private byte[] CKDpub = null;
public boolean isHasApprovalTx() {
return hasApprovalTx;
}
public void setHasApprovalTx(boolean hasApprovalTx) {
this.hasApprovalTx = hasApprovalTx;
}
public long getApprovalAddressNonce() {
return approvalAddressNonce;
}
public void setApprovalAddressNonce(long approvalAddressNonce) {
this.approvalAddressNonce = approvalAddressNonce;
}
public byte[] getCKDpub() {
return CKDpub;
}
public void setCKDpub(byte[] CKDpub) {
this.CKDpub = CKDpub;
}
@Override
public void clearInfo() {
super.clearInfo();
hasApprovalTx = false;
approvalAddressNonce = null;
}
@Override
public void loadFromBundle(Bundle B) {
super.loadFromBundle(B);
if (B.containsKey("ApprovalAddressNonce")) {
approvalAddressNonce = B.getLong("ApprovalAddressNonce");
}
if (B.containsKey("CKDpub")) CKDpub = B.getByteArray("CKDpub");
hasApprovalTx = B.getBoolean("HasApprovalTx");
}
@Override
public void saveToBundle(Bundle B) {
super.saveToBundle(B);
try {
if (approvalAddressNonce != null) B.putLong("ApprovalAddressNonce", approvalAddressNonce);
if (CKDpub != null) B.putByteArray("CKDpub", CKDpub);
B.putBoolean("HasApprovalTx", hasApprovalTx);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
}
}

View file

@ -0,0 +1,534 @@
package com.tangem.wallet.ethID;
import android.net.Uri;
import android.text.InputFilter;
import android.util.Log;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiInfura;
import com.tangem.data.network.model.BlockcypherFee;
import com.tangem.data.network.model.BlockcypherResponse;
import com.tangem.data.network.model.BlockcypherTx;
import com.tangem.data.network.model.BlockcypherTxref;
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.CoinData;
import com.tangem.wallet.CoinEngine;
import com.tangem.wallet.ECDSASignatureETH;
import com.tangem.wallet.EthTransaction;
import com.tangem.wallet.Keccak256;
import com.tangem.wallet.R;
import com.tangem.wallet.TangemContext;
import com.tangem.wallet.eth.EthData;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.SignatureDecodeException;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
public class EthIdEngine extends CoinEngine {
private static final String TAG = com.tangem.wallet.eth.EthEngine.class.getSimpleName();
public EthIdData coinData = null;
private 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 EthIdData) {
coinData = (EthIdData) ctx.getCoinData();
} else {
throw new Exception("Invalid type of Blockchain data for " + this.getClass().getSimpleName());
}
}
public EthIdEngine() {
super();
}
private static int getDecimals() {
return 18;
}
private int getChainIdNum() {
return EthTransaction.ChainEnum.Mainnet.getValue();
}
@Override
public boolean awaitingConfirmation() {
return false;
}
@Override
public Amount getBalance() {
return null;
}
@Override
public String getBalanceHTML() {
if (coinData != null) {
if (coinData.isHasApprovalTx()) {
return "VERIFIED";
} else {
return "NOT REGISTERED";
}
} else {
return "NOT REGISTERED";
}
}
@Override
public String getBalanceCurrency() {
return Blockchain.Ethereum.getCurrency();
}
@Override
public boolean isBalanceNotZero() {
return true;
}
@Override
public String getFeeCurrency() {
return Blockchain.Ethereum.getCurrency();
}
public boolean isNeedCheckNode() {
return false;
}
@Override
public CoinData createCoinData() {
return new EthIdData();
}
@Override
public String getUnspentInputsDescription() {
return "";
}
@Override
public boolean validateAddress(String address) {
if (address == null || address.isEmpty()) {
return false;
}
if (!address.startsWith("0x") && !address.startsWith("0X")) {
return false;
}
if (address.length() != 42) {
return false;
}
return true;
}
@Override
public String getBalanceEquivalent() {
Amount balance = getBalance();
if (balance == null) return "";
return balance.toEquivalentString(coinData.getRate());
}
@Override
public Amount convertToAmount(InternalAmount internalAmount) {
BigDecimal d = internalAmount.divide(new BigDecimal("1000000000000000000"), getDecimals(), RoundingMode.DOWN);
return new Amount(d, ctx.getBlockchain().getCurrency());
}
@Override
public Amount convertToAmount(String strAmount, String currency) {
return new Amount(strAmount, currency);
}
@Override
public InternalAmount convertToInternalAmount(Amount amount) {
return new InternalAmount(amount.multiply(new BigDecimal("1000000000000000000")), "wei");
}
@Override
public InternalAmount convertToInternalAmount(byte[] bytes) {
//throw new Exception("Not implemented");
return null;
}
@Override
public byte[] convertToByteArray(InternalAmount amount) throws Exception {
throw new Exception("Not implemented");
}
@Override
public boolean hasBalanceInfo() {
return true;
}
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
} else {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());
}
}
@Override
public Uri getWalletExplorerUri() {
if (ctx.getBlockchain() == Blockchain.EthereumTestNet)
return Uri.parse("https://rinkeby.etherscan.io/address/" + ctx.getCoinData().getWallet());
else
return Uri.parse("https://etherscan.io/address/" + ctx.getCoinData().getWallet());
}
@Override
public boolean isExtractPossible() {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data);
} else if (!isBalanceNotZero()) {
ctx.setMessage(R.string.general_wallet_empty);
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait);
} else {
return true;
}
return false;
}
@Override
public InputFilter[] getAmountInputFilters() {
return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())};
}
@Override
public boolean checkNewTransactionAmount(Amount amount) {
return true;
}
@Override
public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) {
return true;
}
@Override
public boolean validateBalance(BalanceValidator balanceValidator) {
if (coinData != null && coinData.isHasApprovalTx()) {
balanceValidator.setScore(100);
balanceValidator.setFirstLine(R.string.id_verified);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance);
return false;
} else {
balanceValidator.setScore(0);
balanceValidator.setFirstLine(R.string.id_not_registered);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance);
return false;
}
}
@Override
public String evaluateFeeEquivalent(String fee) {
try {
Amount feeValue = new Amount(fee, ctx.getBlockchain().getCurrency());
return feeValue.toEquivalentString(coinData.getRate());
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
@Override
public String calculateAddress(byte[] pkUncompressed) {
Keccak256 kec = new Keccak256();
int lenPk = pkUncompressed.length;
if (lenPk < 2) {
throw new IllegalArgumentException("Uncompress public key length is invalid");
}
byte[] cleanKey = new byte[lenPk - 1];
for (int i = 0; i < cleanKey.length; ++i) {
cleanKey[i] = pkUncompressed[i + 1];
}
byte[] r = kec.digest(cleanKey);
byte[] address = new byte[20];
for (int i = 0; i < 20; ++i) {
address[i] = r[i + 12];
}
return String.format("0x%s", BTCUtils.toHex(address));
}
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) {
final EthTransaction tx = constructIdTxForSign(coinData.getApprovalAddressNonce());
return new SignTask.TransactionToSign() {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod == TangemCard.SigningMethod.Sign_Hash;
}
@Override
public byte[][] getHashesToSign() {
byte[][] hashesForSign = new byte[1][];
hashesForSign[0] = tx.getRawHash();
return hashesForSign;
}
@Override
public byte[] getRawDataToSign() throws Exception {
throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName());
}
@Override
public String getHashAlgToSign() throws Exception {
throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName());
}
@Override
public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception {
throw new Exception("Transaction validation by issuer not supported in this version");
}
@Override
public byte[] onSignCompleted(byte[] signFromCard) throws Exception {
byte[] for_hash = tx.getRawHash();
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32));
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), 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, approvalPubKey);
if (v != 27 && v != 28) {
Log.e(this.getClass().getSimpleName(), "invalid v");
throw new Exception("Error in " + this.getClass().getSimpleName() + " - invalid v");
}
tx.signature.v = (byte) v;
Log.e(this.getClass().getSimpleName(), " V: " + String.valueOf(v));
byte[] txForSend = tx.getEncoded();
notifyOnNeedSendTransaction(txForSend);
return txForSend;
}
};
}
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() {
@Override
public void onSuccess(String method, BlockcypherResponse blockcypherResponse) {
Log.i(TAG, "onSuccess: " + method);
try {
//TODO: change request logic, 2000 tx max
if (blockcypherResponse.getTxrefs() != null) {
for (BlockcypherTxref txref : blockcypherResponse.getTxrefs()) {
serverApiBlockcypher.requestData("ETH", ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txref.getTx_hash());
}
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError());
} else {
blockchainRequestsCallbacks.onProgress();
}
} else {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
}
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
}
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
}
};
ServerApiBlockcypher.TxResponseListener txListener = new ServerApiBlockcypher.TxResponseListener() {
@Override
public void onSuccess(BlockcypherTx response) {
Log.i(TAG, "onSuccess: BlockcypherTx");
try {
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) {
Log.e(TAG, "onFail: BlockcypherTx" + response);
}
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onFail(String message) {
Log.i(TAG, "onFail: BlockcypherTx " + message);
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
};
serverApiBlockcypher.setResponseListener(blockcypherListener);
serverApiBlockcypher.setTxResponseListener(txListener);
serverApiBlockcypher.requestData("ETH", ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
requestApprovalAddressNonce();
}
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) {
String txStr = String.format("0x%s", BTCUtils.toHex(txForSend));
ServerApiInfura serverApiInfura = new ServerApiInfura();
// request requestData eth gasPrice listener
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) {
if (infuraResponse.getResult() == null || infuraResponse.getResult().isEmpty()) {
ctx.setError("Rejected by node: " + infuraResponse.getError());
blockchainRequestsCallbacks.onComplete(false);
} else {
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}
}
}
@Override
public void onFail(String method, String message) {
if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) {
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInfura.setResponseListener(responseListener);
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr);
}
public int pendingTransactionTimeoutInSeconds() {
return 10;
}
@Override
public boolean needMultipleLinesForBalance() {
return true;
}
@Override
public void defineWallet() throws CardProtocol.TangemException {
try {
String wallet = calculateAddress(calculateCKDpub(ctx.getCard().getIdHash()));
ctx.getCoinData().setWallet(wallet);
} catch (Exception e) {
ctx.getCoinData().setWallet("ERROR");
throw new CardProtocol.TangemException("Can't define wallet address");
}
}
private byte[] calculateCKDpub(byte[] idHash) {
DeterministicKey masterPubKey = HDKeyDerivation.createMasterPubKeyFromBytes(ctx.getCard().getWalletPublicKeyRar(), idHash);
DeterministicKey childPubKey = HDKeyDerivation.deriveChildKey(masterPubKey, new ChildNumber(1, false));
return childPubKey.getPubKey();
}
private EthTransaction constructIdTxForSign(long nonce) {
BigInteger nonceValue = BigInteger.valueOf(nonce);
BigInteger gasPrice = BigInteger.valueOf(10000000000L);
BigInteger gasLimit = BigInteger.valueOf(21000);
BigInteger weiAmount = gasPrice.multiply(gasLimit).add(BigInteger.ONE);
Integer chainId = this.getChainIdNum();
String to = coinData.getWallet();
if (to.startsWith("0x") || to.startsWith("0X")) {
to = to.substring(2);
}
return EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
}
private boolean checkSignature(byte[] signature, long nonce) throws SignatureDecodeException {
byte[] hashForSign = constructIdTxForSign(nonce).getRawHash();
// new Secp256k1Context();
return ECKey.verify(hashForSign, signature, coinData.getCKDpub());
}
private void requestApprovalAddressNonce() {
final ServerApiInfura serverApiInfura = new ServerApiInfura();
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
String pending = infuraResponse.getResult();
pending = pending.substring(2);
// BigInteger count = new BigInteger(pending, 16);
Long count = Long.valueOf(pending, 16);
coinData.setApprovalAddressNonce(count);
}
@Override
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
}
};
serverApiInfura.setResponseListener(responseListener);
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, approvalAddress, "", "");
}
}

View file

@ -275,11 +275,11 @@ public class LtcEngine extends BtcEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().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);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// 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);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -161,18 +161,17 @@ class TezosEngine : CoinEngine {
balanceValidator.setSecondLine(R.string.empty_string)
}
}
if (ctx.card.offlineBalance != null &&
!coinData!!.isBalanceReceived &&
ctx.card.remainingSignatures ==
ctx.card.maxSignatures &&
balance!!.notZero()
) {
balanceValidator.setScore(80)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline
balanceValidator.setSecondLine(
R.string.balance_validator_second_line_internet_to_get_balance
)
}
// if (ctx.card.offlineBalance != null &&
// !coinData!!.isBalanceReceived &&
// ctx.card.remainingSignatures ==
// ctx.card.maxSignatures
// ) {
// balanceValidator.setScore(80)
// balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline
// balanceValidator.setSecondLine(
// R.string.balance_validator_second_line_internet_to_get_balance
// )
// }
return true
} catch (e: java.lang.Exception) {
e.printStackTrace()

View file

@ -275,11 +275,11 @@ public class XlmAssetEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getXlmBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -243,11 +243,11 @@ public class XlmEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.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_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -1,4 +1,4 @@
package com.tangem.wallet.xlmtag
package com.tangem.wallet.xlmTag
import android.os.Bundle
import android.util.Log

View file

@ -1,4 +1,4 @@
package com.tangem.wallet.xlmtag
package com.tangem.wallet.xlmTag
import android.net.Uri
import android.os.StrictMode
@ -53,12 +53,18 @@ class XlmTagEngine : CoinEngine {
}
override fun getBalanceHTML(): String {
return if (hasBalanceInfo() && isBalanceNotZero && coinData!!.fundsFromTrustedSource) {
ctx.getString(R.string.tag_genuine)
} else if (coinData!!.fundsSentToTrustedSource) {
ctx.getString(R.string.tag_claimed)
return if (hasBalanceInfo()) {
if (coinData!!.fundsFromTrustedSource) {
// if (coinData!!.fundsSentToTrustedSource) { //TODO: shut down for example tags to appear genuine, turn on for production
// ctx.getString(R.string.tag_claimed)
// } else {
ctx.getString(R.string.tag_genuine)
// }
} else {
ctx.getString(R.string.tag_not_genuine)
}
} else {
ctx.getString(R.string.tag_not_genuine)
""
}
}
@ -72,7 +78,7 @@ class XlmTagEngine : CoinEngine {
}
override fun hasBalanceInfo(): Boolean {
return if (coinData == null) false else coinData!!.getBalance() != null || coinData!!.isError404
return if (coinData == null) false else coinData!!.isBalanceReceived
}
override fun isExtractPossible(): Boolean {
@ -130,40 +136,26 @@ class XlmTagEngine : CoinEngine {
}
override fun validateBalance(balanceValidator: BalanceValidator): Boolean {
return try {
if (ctx.card.offlineBalance == null && !ctx.coinData.isBalanceReceived || !ctx.coinData.isBalanceReceived && ctx.card.remainingSignatures != ctx.card.maxSignatures) {
if (coinData!!.isError404) {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_no_account
balanceValidator.setSecondLine(R.string.balance_validator_second_line_create_account_instruction)
} else {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance)
return false
}
try {
if (!hasBalanceInfo()) {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_no_connection
balanceValidator.setSecondLine(R.string.balance_validator_second_line_authenticity_not_verified)
return false
}
if (coinData!!.isBalanceReceived && coinData!!.isBalanceEqual) {
if (coinData!!.fundsFromTrustedSource) {
balanceValidator.setScore(100)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance
balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain)
if (coinData!!.getBalance()!!.isZero) {
balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet
balanceValidator.setSecondLine(R.string.empty_string)
}
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_in_blockchain
balanceValidator.setSecondLine(R.string.empty_string)
} else {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_authenticity
balanceValidator.setSecondLine(R.string.empty_string)
}
if (ctx.card.offlineBalance != null && !coinData!!.isBalanceReceived && ctx.card.remainingSignatures == ctx.card.maxSignatures && coinData!!.getBalance()!!.notZero()) {
balanceValidator.setScore(80)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance)
}
true
return true
} catch (e: Exception) {
e.printStackTrace()
false
return false
}
}
@ -310,7 +302,7 @@ class XlmTagEngine : CoinEngine {
val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet)
try {
var operationsPage = server.payments().forAccount(accountKeyPair).order(RequestBuilder.Order.DESC).execute()
var operationsPage = server.payments().forAccount(accountKeyPair).limit(200).order(RequestBuilder.Order.DESC).execute()
Log.e("Stellar", operationsPage.records.toString())
operations.addAll(operationsPage.records)
@ -324,6 +316,8 @@ class XlmTagEngine : CoinEngine {
}
parsePayments(blockchainRequestsCallbacks)
CoroutineScope(Dispatchers.Main).launch { blockchainRequestsCallbacks.onComplete(!ctx.hasError()) }
} catch (e: Exception) {
if (e.message != null) {
ctx.error = e.message
@ -342,53 +336,11 @@ class XlmTagEngine : CoinEngine {
Log.e("Stellar",
"fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " +
"fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource")
blockchainRequestsCallbacks.onComplete(!ctx.hasError())
coinData!!.isBalanceReceived = true
}
override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
CoroutineScope(Dispatchers.IO).launch { requestPayments(blockchainRequestsCallbacks) }
val serverApi = ServerApiStellar(Blockchain.Stellar)
val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener {
override fun onSuccess(request: StellarRequest.Base) {
Log.i(TAG, "onSuccess: " + request.javaClass.simpleName)
if (request is StellarRequest.Balance) {
coinData!!.accountResponse = request.accountResponse
coinData!!.validationNodeDescription = serverApi.currentURL
blockchainRequestsCallbacks.onProgress()
} else if (request is Ledgers) {
coinData!!.setLedgerResponse(request.ledgerResponse)
if (serverApi.isRequestsSequenceCompleted) {
} else {
blockchainRequestsCallbacks.onProgress()
}
} else {
ctx.error = "Invalid request logic"
blockchainRequestsCallbacks.onComplete(false)
}
}
override fun onFail(request: StellarRequest.Base) {
Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error)
if (request.errorResponse != null && request.errorResponse.code == 404) {
coinData!!.isError404 = true
} else {
ctx.error = request.error
}
if (serverApi.isRequestsSequenceCompleted) {
if (ctx.hasError()) {
blockchainRequestsCallbacks.onComplete(false)
} else {
blockchainRequestsCallbacks.onComplete(true)
}
} else {
blockchainRequestsCallbacks.onProgress()
}
}
}
serverApi.setListener(listener)
serverApi.requestData(ctx, StellarRequest.Balance(coinData!!.wallet))
serverApi.requestData(ctx, Ledgers())
}
@Throws(Exception::class)
@ -463,7 +415,7 @@ class XlmTagEngine : CoinEngine {
private val multiplier = BigDecimal("10000000")
const val OPERATIONS_LIMIT = 50
const val OPERATIONS_LIMIT = 200
private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM"
private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2"
}

View file

@ -228,11 +228,11 @@ public class XrpEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#505050"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,4L3,1h2v3h3v2L5,6v3L3,9L3,6L0,6L0,4h3zM6,10L6,7h3L9,4h7l1.83,2L21,6c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2L5,22c-1.1,0 -2,-0.9 -2,-2L3,10h3zM13,19c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5 -5,2.24 -5,5 2.24,5 5,5zM9.8,14c0,1.77 1.43,3.2 3.2,3.2s3.2,-1.43 3.2,-3.2 -1.43,-3.2 -3.2,-3.2 -3.2,1.43 -3.2,3.2z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorAccent"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

View file

@ -0,0 +1,4 @@
<vector android:height="100dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="100dp" android:tint="#505050" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorAccent"
android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
</vector>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.otaliastudios.cameraview.CameraView
android:id="@+id/cvCamera"
android:layout_width="350dp"
android:layout_height="350dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
app:cameraAudio="off"
app:cameraFlash="auto"
app:cameraMode="picture"
app:cameraPictureSizeMaxHeight="200"
app:cameraPictureSizeMaxWidth="200"
app:cameraPictureSizeMinHeight="200"
app:cameraPictureSizeMinWidth="200"
app:cameraPictureSizeSmallest="true"
app:cameraRequestPermissions="true"
app:cameraSnapshotMaxHeight="200"
app:cameraSnapshotMaxWidth="200"
app:cameraUseDeviceOrientation="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/ivTakePicture"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_take_picture"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cvCamera" />
<ImageView
android:id="@+id/ivFlipCamera"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_flip_camera"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivTakePicture" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,254 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
android:padding="0dp"
tools:ignore="contentDescription">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/srl"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/rlAuxButtons"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="false"
android:background="@color/white"
android:orientation="vertical">
<include
android:id="@+id/loCard"
layout="@layout/layout_id" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toBottomOf="@+id/loCard">
<LinearLayout
android:id="@+id/loBalance"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center_vertical"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<TextView
android:id="@+id/tvBalanceLine1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:visibility="visible"
tools:textColor="@color/confirmed" />
<TextView
android:id="@+id/tvBalanceLine2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:fontFamily="@font/maax"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textAlignment="center"
android:visibility="visible" />
</LinearLayout>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.3" />
<LinearLayout
android:id="@+id/loIdentificationData"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:baselineAligned="false"
android:gravity="center_vertical|center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toBottomOf="@+id/guideline">
<TextView
android:id="@+id/tvDemoWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/card_state_empty"
android:textSize="14dp"
android:textStyle="normal"
android:text="@string/id_demo"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tvIdNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/primary_dark"
android:textSize="18dp"
android:textStyle="normal"
tools:text="ID # 203129301239" />
<TextView
android:id="@+id/tvFullName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/primary_dark"
android:textSize="28dp"
android:textStyle="normal"
tools:text="John Doe" />
<TextView
android:id="@+id/tvBirthDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:layout_marginBottom="5dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_1_small"
tools:text="2000-01-01" />
<TextView
android:id="@+id/tvGender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:layout_marginBottom="5dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_1_small"
tools:text="Sex: M" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<RelativeLayout
android:id="@+id/rlMessageAndError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/rlAuxButtons">
<TextView
android:id="@+id/tvMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/msg_okay"
android:elevation="2dp"
android:fontFamily="@font/maax"
android:padding="4dp"
android:textAlignment="center"
android:textColor="@color/black"
android:textStyle="normal|bold"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/tvError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tvMessage"
android:layout_alignParentBottom="false"
android:background="@color/msg_err"
android:elevation="3dp"
android:fontFamily="@font/maax"
android:padding="4dp"
android:shadowColor="@color/cardview_shadow_star"
android:textAlignment="center"
android:textColor="@color/white"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout>
<LinearLayout
android:id="@+id/rlToolButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/rlAuxButtons"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnIssueNewId"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:enabled="true"
android:fontFamily="@font/saira_condensed_bold"
android:text="Issue New Id"
android:textSize="@dimen/text_size_large"/>
</LinearLayout>
<LinearLayout
android:id="@+id/rlAuxButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent">
<include layout="@layout/layout_btn_details" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnNewScan"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:drawableStart="@drawable/ic_nfc_white_24dp"
android:fontFamily="@font/saira_condensed_bold"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="@string/loaded_wallet_btn_new_scan"
android:textColor="@color/primary_dark" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:padding="0dp"
tools:ignore="contentDescription">
<ImageView
android:id="@+id/ivPhoto"
app:layout_constraintHorizontal_bias=".46"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_add_a_photo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/etName"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:inputType="textCapWords"
android:hint="First Name"
android:layout_marginTop="24dp"
android:paddingBottom="16dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/ivPhoto"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<EditText
android:id="@+id/etSurname"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:inputType="textCapWords"
android:hint="Last Name"
android:paddingBottom="16dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/etName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/tvBirthDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:hint="Enter Date of Birth"
android:paddingTop="14dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/etSurname" />
<EditText
android:id="@+id/etBirthDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:hint="dd/mm/yyyy"
android:inputType="date"
android:paddingBottom="16dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/tvBirthDate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="@+id/tvSelectGender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:hint="Select Gender"
android:paddingTop="14dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/etBirthDate" />
<RadioGroup
android:id="@+id/rgGender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSelectGender">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/rbFemale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:text="Female"
android:textAlignment="center"
android:textSize="18sp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/rbMale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:text="Male"
android:textAlignment="center"
android:textSize="18sp" />
</RadioGroup>
<LinearLayout
android:id="@+id/rlToolButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:fontFamily="@font/saira_condensed_bold"
android:text="Confirm"
android:textSize="@dimen/text_size_large" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="Now touch the Issuer Key card"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="to confirm the new ID data"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="Now touch the ID card"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="@dimen/text_size_large"
tools:text="CB02 0000 0002 5000" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="to write the new ID data"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/loCard"
android:layout_width="match_parent"
android:layout_height="300dp"
android:maxHeight="300dp"
android:padding="3dp"
android:layout_marginTop="16dp"
app:layout_constraintHeight_max="350dp"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="contentDescription"
tools:showIn="@layout/fragment_id">
<ImageView
android:id="@+id/ivPhoto"
android:layout_width="300dp"
android:layout_height="300dp"
android:src="@drawable/id_card_default"
android:layout_centerInParent="true"/>
<RelativeLayout
android:id="@+id/rlProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:elevation="3dp"
android:padding="5dp"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.75"
android:background="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:alpha="1"
android:fontFamily="@font/maaxmedium"
android:text="@string/loaded_wallet_status_verifying"
android:textColor="@color/colorPrimary"
android:textSize="12dp"
android:textStyle="bold" />
<ProgressBar
android:layout_width="@dimen/size_progress_bar_big"
android:layout_height="@dimen/size_progress_bar_big"
android:layout_centerInParent="true"
android:alpha="1"
android:elevation="1dp"
android:progressTint="@color/colorPrimary" />
</RelativeLayout>
</RelativeLayout>

View file

@ -86,6 +86,7 @@
android:background="@color/white" />
<TextView
android:id="@+id/tvProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"

View file

@ -31,6 +31,9 @@
<action
android:id="@+id/action_main_to_tagFragment"
app:destination="@id/tagFragment" />
<action
android:id="@+id/action_main_to_idFragment"
app:destination="@id/idFragment" />
</fragment>
<fragment
@ -196,5 +199,43 @@
android:name="com.tangem.ui.fragment.additional.TagFragment"
android:label="fragment_tag"
tools:layout="@layout/fragment_tag" />
<fragment
android:id="@+id/issueNewIdFragment"
android:name="com.tangem.ui.fragment.id.IssueNewIdFragment"
android:label="IssueNewIdFragment" >
<action
android:id="@+id/action_issueNewIdFragment_to_cameraFragment"
app:destination="@id/cameraFragment" />
<action
android:id="@+id/action_issueNewIdFragment_to_validateIdFragment"
app:destination="@id/validateIdFragment" />
</fragment>
<fragment
android:id="@+id/idFragment"
android:name="com.tangem.ui.fragment.id.IdFragment"
android:label="IdFragment" >
<action
android:id="@+id/action_idFragment_to_issueNewIdFragment"
app:destination="@id/issueNewIdFragment" />
<action
android:id="@+id/action_idFragment_to_verifyCard"
app:destination="@id/verifyCard" />
</fragment>
<fragment
android:id="@+id/cameraFragment"
android:name="com.tangem.ui.fragment.id.CameraFragment"
android:label="CameraFragment" />
<fragment
android:id="@+id/validateIdFragment"
android:name="com.tangem.ui.fragment.id.ValidateIdFragment"
android:label="ValidateIdFragment" >
<action
android:id="@+id/action_validateIdFragment_to_writeIdFragment"
app:destination="@id/writeIdFragment" />
</fragment>
<fragment
android:id="@+id/writeIdFragment"
android:name="com.tangem.ui.fragment.id.WriteIdFragment"
android:label="WriteIdFragment" />
</navigation>

View file

@ -85,7 +85,7 @@
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">Aucun porte-monnaie compatible installé</string>
<string name="loaded_wallet_message_wait">Veuillez patienter pendant que la transaction précédente est confirmée dans la blockchain</string>
<string name="loaded_wallet_message_wait">Veuillez patienter que votre précédente transaction soit confirmée dans la blockchai</string>
<string name="loaded_wallet_message_refresh">Impossible d\'obtenir toutes les entrées. Balayez vers le bas pour actualiser.</string>
<string name="loaded_wallet_warning_no_signature">La carte n\'a plus de signature!</string>
<string name="loaded_wallet_warning_dont_forget_pin">Si vous oubliez votre nouveau code PIN, vous perdrez votre argent pour toujours!</string>
@ -132,7 +132,7 @@
<string name="balance_validator_second_line_confirmed_in_blockchain">Solde confirmé dans la blockchain</string>
<string name="balance_validator_second_line_unverified_balance">Solde ne peut pas être vérifié. Balayez vers le bas pour actualiser..</string>
<string name="balance_validator_second_line_wait_for_confirmation">Attendez la confirmation dans la chaîne de blocs</string>
<string name="balance_validator_second_line_wait_for_confirmation">En attente de confirmation dans la blockchain</string>
<string name="balance_validator_second_line_internet_to_verify_online">Restaurez la connexion Internet pour obtenir un solde sécurisé de la chaîne de blocs</string>
<string name="balance_validator_second_line_internet_to_get_balance">Impossible d\'obtenir le
solde de la blockchain. Restaurez la connexion Internet pour être plus confiant.</string>
@ -172,7 +172,7 @@
<!-- SendTransaction -->
<string name="send_transaction_notification_wait">Veuillez patienter pendant l\'envoi du paiement…</string>
<string name="send_transaction_success">La transaction a été signée et envoyée avec succès au noeud de la blockchain. La balance de porte-monnaie sera mise à jour dans un moment</string>
<string name="send_transaction_success">La transaction a été signée et envoyée avec succès au noeud de la blockchain. Le solde de votre porte-monnaie sera mise à jour dès que transaction sera validée sur la blockchain</string>
<string name="send_transaction_error_failed_to_send">Réessayez. Échec d\'envoi de la transaction (%s)</string>
<string name="send_transaction_error_cannot_sign">Impossible de signer la transaction. Veillez à entrer le code PIN2 correct!</string>
<string name="send_transaction_error_wrong_amount">Le montant et les frais sont supérieurs au solde total du porte-monnaie!</string>
@ -196,9 +196,9 @@
<string name="confirm_transaction_btn_fee_normal">Normal</string>
<string name="confirm_transaction_btn_fee_priority">Priorité</string>
<string name="confirm_transaction_btn_including_fee">Frais inclus</string>
<string name="confirm_transaction_btn_not_including_fee">Frais non compris</string>
<string name="confirm_transaction_btn_not_including_fee">Frais non inclus</string>
<string name="confirm_transaction_including_fee">(frais inclus)</string>
<string name="confirm_transaction_not_including_fee">(sans frais)</string>
<string name="confirm_transaction_not_including_fee">(frais non inclus)</string>
<string name="confirm_transaction_error_data_is_outdated">Les données obtenues sont obsolètes! Réessayez</string>
<string name="confirm_transaction_error_cannot_reach_node">Impossible d\'atteindre le nœud de la blockchain active en cours. Réessayez</string>
<string name="confirm_transaction_error_cannot_check_balance">Impossible de vérifier le solde! Pas de connexion avec les noeuds de la blockchain</string>

View file

@ -270,6 +270,12 @@
<string name="tag_not_genuine">NOT GENUINE</string>
<string name="tag_claimed">ALREADY CLAIMED</string>
<!-- Id Card -->
<string name="id_verified">VERIFIED</string>
<string name="id_not_registered">NOT REGISTERED</string>
<string name="id_demo">ID DEMO</string>
<string name="id_empty">Empty ID card</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Save</string>
<string name="pin_save_btn_delete">Delete</string>

View file

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.3.60'
ext.kotlin_version = '1.3.61'
repositories {
google()
jcenter()

View file

@ -47,7 +47,7 @@ class LocalStorage
artworks = HashMap()
}
if (artworks.count() < 31) {
if (artworks.count() < 32) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
@ -78,6 +78,7 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru043, false)
putResourceArtworkToCatalog(R.drawable.card_tg044, false)
putResourceArtworkToCatalog(R.drawable.card_tg046, false)
putResourceArtworkToCatalog(R.drawable.card_tgslix, false)
putResourceArtworkToCatalog(R.drawable.card_bc00, false)
putResourceArtworkToCatalog(R.drawable.card_ff32, true)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -0,0 +1,12 @@
package com.tangem.tangem_card.data
enum class ProductMask(val code: Byte) {
Note(0x01),
Tag(0x02),
Id(0x04);
companion object {
private val values = values()
fun byCode(code: Byte): ProductMask? = values.find { it.code == code }
}
}

View file

@ -1,9 +1,14 @@
package com.tangem.tangem_card.data;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.reader.ProductMask;
import com.tangem.tangem_card.reader.SettingsMask;
import com.tangem.tangem_card.reader.TLV;
import com.tangem.tangem_card.reader.TLVList;
import com.tangem.tangem_card.util.Util;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
@ -30,6 +35,7 @@ public class TangemCard {
private byte[] terminalPublicKey;
private boolean terminalIsLinked = false;
private byte[] tagSignature = null;
private byte[] idHash = null;
public String getBlockchainID() {
return blockchainID;
@ -184,6 +190,14 @@ public class TangemCard {
this.terminalIsLinked = terminalIsLinked;
}
public byte[] getIdHash() {
return idHash;
}
public void setIdHash(byte[] idHash) {
this.idHash = idHash;
}
private int health = 0;
public int getHealth() {
@ -288,6 +302,28 @@ public class TangemCard {
return "";
}
private byte[] issuerDataEx;
private byte[] issuerDataExSignature;
private int issuerDataExCounter;
public byte[] getIssuerDataEx() {
return issuerDataEx;
}
public byte[] getIssuerDataExSignature() {
return issuerDataExSignature;
}
public int getIssuerDataExCounter() {
return issuerDataExCounter;
}
public void setIssuerDataEx(byte[] value, byte[] signature, int counter) {
issuerDataEx = value;
issuerDataExSignature = signature;
issuerDataExCounter = counter;
}
private int pauseBeforePIN2 = 0;
public void setPauseBeforePIN2(int value) {
@ -579,6 +615,101 @@ public class TangemCard {
this.UID = UID;
}
public int productMask = ProductMask.Note;
public void setProductMask(int productMask) {
this.productMask = productMask;
}
public boolean isIDCard() {
try {
return (productMask & ProductMask.IdCard) != 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public TLVList tlvIDCardData = null;
public void setTlvIdCardData(TLVList tlvIDCardData) {
this.tlvIDCardData = tlvIDCardData;
}
public void setTlvIDCardData(
String fullName,
String birthday,
String gender,
byte[] photo,
String issueDate,
String expireDate,
String trustedAddress) {
IDCardData data = new IDCardData();
data.fullName = fullName;
data.birthday = birthday;
data.gender = gender;
data.photo = photo;
data.issueDate = issueDate;
data.expireDate = expireDate;
data.trustedAddress = trustedAddress;
this.tlvIDCardData = data.toTLVList();
}
public boolean hasIDCardData() {
if (tlvIDCardData == null) {
if (issuerDataEx == null) return false;
try {
tlvIDCardData = TLVList.fromBytes(issuerDataEx);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
return tlvIDCardData.hasTag(TLV.Tag.TAG_FullName) && tlvIDCardData.hasTag(TLV.Tag.TAG_Birthday) && tlvIDCardData.hasTag(TLV.Tag.TAG_Gender) && tlvIDCardData.hasTag(TLV.Tag.TAG_Photo)
&& tlvIDCardData.hasTag(TLV.Tag.TAG_IssueDate) && tlvIDCardData.hasTag(TLV.Tag.TAG_ExpireDate) && tlvIDCardData.hasTag(TLV.Tag.TAG_TrustedAddress);
}
public static class IDCardData {
public String fullName;
public String birthday;
public String gender;
public byte[] photo;
public String issueDate;
public String expireDate;
public String trustedAddress;
public TLVList toTLVList() {
TLVList tlvIDCardData = new TLVList();
tlvIDCardData.add(new TLV(TLV.Tag.TAG_FullName, fullName.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_Birthday, birthday.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_Gender, gender.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_Photo, photo));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_IssueDate, issueDate.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_ExpireDate, expireDate.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_TrustedAddress, trustedAddress.getBytes(StandardCharsets.UTF_8)));
return tlvIDCardData;
}
public static IDCardData fromTLVList(TLVList tlvIDCardData) {
IDCardData result = new IDCardData();
result.fullName = tlvIDCardData.getTLV(TLV.Tag.TAG_FullName).getAsString();
result.birthday = tlvIDCardData.getTLV(TLV.Tag.TAG_Birthday).getAsString();
result.gender = tlvIDCardData.getTLV(TLV.Tag.TAG_Gender).getAsString();
result.photo = tlvIDCardData.getTLV(TLV.Tag.TAG_Photo).Value;
result.issueDate = tlvIDCardData.getTLV(TLV.Tag.TAG_IssueDate).getAsString();
result.expireDate = tlvIDCardData.getTLV(TLV.Tag.TAG_ExpireDate).getAsString();
result.trustedAddress = tlvIDCardData.getTLV(TLV.Tag.TAG_TrustedAddress).getAsString();
return result;
}
}
public IDCardData getIDCardData() {
if (!hasIDCardData()) return null;
return IDCardData.fromTLVList(tlvIDCardData);
}
private byte[] offlineBalance;
@ -638,164 +769,6 @@ public class TangemCard {
this.tagSignature = tagSignature;
}
// public Bundle getAsBundle() {
// Bundle B = new Bundle();
// saveToBundle(B);
// return B;
// }
//
// public void saveToBundle(Bundle B) {
// try {
// B.putString("UID", UID);
// B.putByteArray("CID", CID);
// B.putString("PIN", PIN);
// B.putString("PIN2", PIN2.name());
// B.putString("Status", status.name());
// B.putString("Blockchain", blockchainID);
// B.putString("BlockchainName", blockchainName);
// B.putInt("TokensDecimal", tokensDecimal);
// B.putString("TokenSymbol", tokenSymbol);
// B.putString("ContractAddress", contractAddress);
// if (dtPersonalization != null) B.putLong("dtPersonalization", dtPersonalization.getTime());
// B.putInt("RemainingSignatures", remainingSignatures);
// B.putInt("MaxSignatures", maxSignatures);
// B.putInt("Health", health);
// if (settingsMask != null) B.putInt("settingsMask", settingsMask);
// B.putInt("pauseBeforePIN2", pauseBeforePIN2);
// if (signingMethod != null) B.putString("signingMethod", signingMethod.name());
// if (manufacturer != null) B.putString("Manufacturer", manufacturer.name());
// if (encryptionMode != null) B.putString("EncryptionMode", encryptionMode.name());
// if (issuer != null) B.putString("Issuer", issuer.getID());
// if (issuerPublicDataKey != null) B.putByteArray("IssuerPublicDataKey", issuerPublicDataKey);
// if (firmwareVersion != null) B.putString("FirmwareVersion", firmwareVersion);
// if (batch != null) B.putString("Batch", batch);
// B.putBoolean("ManufacturerConfirmed", manufacturerConfirmed);
// B.putBoolean("CardPublicKeyValid", isCardPublicKeyValid());
// B.putByteArray("CardPublicKey", getCardPublicKey());
//
// B.putInt("SignedHashes", getSignedHashes());
// B.putBoolean("WalletPublicKeyValid", isWalletPublicKeyValid());
// if (pbWalletKey != null)
// B.putByteArray("PublicKey", pbWalletKey);
// if (pbWalletKeyRar != null)
// B.putByteArray("PublicKeyRar", pbWalletKeyRar);
//
// if (getOfflineBalance() != null) B.putByteArray("OfflineBalance", getOfflineBalance());
//
// if (getDenomination() != null) B.putByteArray("Denomination", getDenomination());
// if (getDenominationText() != null) B.putString("DenominationText", getDenominationText());
//
// if (getIssuerData() != null && getIssuerDataSignature() != null) {
// B.putByteArray("IssuerData", getIssuerData());
// B.putByteArray("IssuerDataSignature", getIssuerDataSignature());
// B.putBoolean("NeedWriteIssuerData", getNeedWriteIssuerData());
// }
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (onlineVerified != null)
// B.putBoolean("onlineVerified", onlineVerified);
//
// if (onlineValidated != null)
// B.putBoolean("onlineValidated", onlineValidated);
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (onlineVerified != null)
// B.putBoolean("onlineVerified", onlineVerified);
//
// if (onlineValidated != null)
// B.putBoolean("onlineValidated", onlineValidated);
// } catch (Exception e) {
// Log.e("Can't save to bundle ", e.getMessage());
// }
//
// }
//
// public void loadFromBundle(Bundle B) {
// UID = B.getString("UID");
// CID = B.getByteArray("CID");
// PIN = B.getString("PIN");
// PIN2 = PIN2_Mode.valueOf(B.getString("PIN2"));
// status = Status.valueOf(B.getString("Status"));
// blockchainID = B.getString("Blockchain");
// tokensDecimal = B.getInt("TokensDecimal", 18);
// tokenSymbol = B.getString("TokenSymbol", "");
// contractAddress = B.getString("ContractAddress", "");
// if (B.containsKey("BlockchainName"))
// blockchainName = B.getString("BlockchainName", "");
// if (B.containsKey("dtPersonalization")) {
// dtPersonalization = new Date(B.getLong("dtPersonalization"));
// }
// remainingSignatures = B.getInt("RemainingSignatures");
// maxSignatures = B.getInt("MaxSignatures");
// health = B.getInt("health");
// if (B.containsKey("settingsMask")) settingsMask = B.getInt("settingsMask");
// pauseBeforePIN2 = B.getInt("pauseBeforePIN2");
// if (B.containsKey("signingMethod"))
// signingMethod = SigningMethod.valueOf(B.getString("signingMethod"));
// if (B.containsKey("Manufacturer"))
// manufacturer = Manufacturer.valueOf(B.getString("Manufacturer"));
// manufacturerConfirmed = B.getBoolean("ManufacturerConfirmed");
// if (B.containsKey("EncryptionMode"))
// encryptionMode = EncryptionMode.valueOf(B.getString("EncryptionMode"));
// else
// encryptionMode = null;
//
// if (B.containsKey("SignedHashes")) setSignedHashes(B.getInt("SignedHashes"));
//
// if (B.containsKey("Issuer")) issuer = Issuer.FindIssuer(B.getString("Issuer"));
// if (B.containsKey("IssuerPublicDataKey"))
// issuerPublicDataKey = B.getByteArray("IssuerPublicDataKey");
//
// if (B.containsKey("FirmwareVersion")) firmwareVersion = B.getString("FirmwareVersion");
// if (B.containsKey("Batch")) batch = B.getString("Batch");
//
// cardPublicKeyValid = B.getBoolean("CardPublicKeyValid");
// if (B.containsKey("CardPublicKey")) setCardPublicKey(B.getByteArray("CardPublicKey"));
//
// if (B.containsKey("OfflineBalance")) setOfflineBalance(B.getByteArray("OfflineBalance"));
// else clearOfflineBalance();
//
// if (B.containsKey("Denomination") && B.containsKey("DenominationText")) {
// setDenomination(B.getByteArray("Denomination"), B.getString("DenominationText"));
// } else if (B.containsKey("Denomination")) {
// setDenomination(B.getByteArray("Denomination"), "N/A");
// } else clearDenomination();
//
// if (B.containsKey("IssuerData") && B.containsKey("IssuerDataSignature"))
// setIssuerData(B.getByteArray("IssuerData"), B.getByteArray("IssuerDataSignature"));
// else setIssuerData(null, null);
//
// if (B.containsKey("NeedWriteIssuerData"))
// setNeedWriteIssuerData(B.getBoolean("NeedWriteIssuerData"));
//
// walletPublicKeyValid = B.getBoolean("WalletPublicKeyValid");
// if (B.containsKey("PublicKey")) {
// pbWalletKey = B.getByteArray("PublicKey");
// }
// if (B.containsKey("PublicKeyRar")) {
// pbWalletKeyRar = B.getByteArray("PublicKeyRar");
// }
//
// if (B.containsKey("codeConfirmed"))
// codeConfirmed = B.getBoolean("codeConfirmed");
//
// if (B.containsKey("onlineVerified"))
// onlineVerified = B.getBoolean("onlineVerified");
//
// if (B.containsKey("onlineValidated"))
// onlineValidated = B.getBoolean("onlineValidated");
// }
public enum EncryptionMode {
None((byte) 0x0), Fast((byte) 0x1), Strong((byte) 0x2);

View file

@ -1,5 +1,6 @@
package com.tangem.tangem_card.reader;
import com.tangem.tangem_card.data.Issuer;
import com.tangem.tangem_card.data.Manufacturer;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.util.Log;
@ -17,6 +18,7 @@ import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import javax.crypto.KeyAgreement;
@ -99,6 +101,11 @@ public class CardProtocol {
private NfcReader mIsoDep;
public NfcReader getReader()
{
return mIsoDep;
}
public static final String DefaultPIN = "000000";
public static final String DefaultPIN2 = "000";
@ -957,9 +964,11 @@ public class CardProtocol {
}
private void prepareDataForLinkingTerminal(CommandApdu apdu, byte[] data) throws Exception {
byte[] transactionSignature = CardCrypto.Signature(mCard.getTerminalPrivateKey(), data);
apdu.addTLV(TLV.Tag.TAG_Terminal_TransactionSignature, transactionSignature);
addTerminalPublicKeyToApdu(apdu);
if (mCard.getTerminalPrivateKey() != null) {
byte[] transactionSignature = CardCrypto.Signature(mCard.getTerminalPrivateKey(), data);
apdu.addTLV(TLV.Tag.TAG_Terminal_TransactionSignature, transactionSignature);
addTerminalPublicKeyToApdu(apdu);
}
}
/**
@ -1095,6 +1104,12 @@ public class CardProtocol {
if (issuerData == null || issuerDataSignature == null)
throw new TangemException("Invalid answer format (GetIssuerData)");
if( issuerData.Value.length==0 && issuerDataSignature.Value.length==0 )
{
mCard.setIssuerData(null, null);
return new TLVList();
}
ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream();
bsDataToVerify.write(mCard.getCID());
bsDataToVerify.write(issuerData.Value);
@ -1117,6 +1132,185 @@ public class CardProtocol {
}
}
/**
* Series of GET_ISSUER_DATA command to read extra data and verify issuer signature of returned extra data
* See [1] 3.3, 8.7
* This command returns Issuer_Extra_Data data block and its issuers signature.
*
* @return TLVList with issuerData (if success read and verify)
* @throws Exception - if something went wrong
*/
public TLVList run_GetIssuerDataEx(Notifications notifications) throws Exception {
boolean needReadIssuerDataCounter = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
TLV issuerDataSignature = null;
TLV issuerDataCounter = null;
TLV issuerDataSize = null;
ByteArrayOutputStream bsIssuerDataEx = new ByteArrayOutputStream();
do {
CommandApdu rqApdu = StartPrepareCommand(INS.GetIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x01);
rqApdu.addTLV_U16(TLV.Tag.TAG_Offset, bsIssuerDataEx.size());
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
TLV issuerData = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data);
if (issuerData != null && issuerData.Value != null)
bsIssuerDataEx.write(issuerData.Value);
issuerDataSignature = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Signature);
issuerDataCounter = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Counter);
if (issuerDataSize == null)
issuerDataSize = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Size);
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
if (issuerDataSize != null && issuerDataSize.getAsInt() != 0) {
if (notifications != null)
notifications.onReadProgress(this, (int) Math.round(bsIssuerDataEx.size() * 100.0 / issuerDataSize.getAsInt()));
Log.i(logTag, String.format("Read %d/%d bytes", bsIssuerDataEx.size(), issuerDataSize.getAsInt()));
}
} while ((issuerDataSignature == null) || (needReadIssuerDataCounter && (issuerDataCounter == null)));
boolean protectIssuerDataAgainstReplay = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
if (notifications != null) notifications.onReadProgress(this, 100);
if (bsIssuerDataEx.size() > 0 && issuerDataSignature != null && issuerDataSignature.Value != null) {
ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream();
bsDataToVerify.write(mCard.getCID());
bsDataToVerify.write(bsIssuerDataEx.toByteArray());
if (protectIssuerDataAgainstReplay) {
bsDataToVerify.write(issuerDataCounter.Value);
}
try {
if (CardCrypto.VerifySignature(mCard.getIssuerPublicDataKey(), bsDataToVerify.toByteArray(), issuerDataSignature.Value)) {
mCard.setIssuerDataEx(bsIssuerDataEx.toByteArray(), issuerDataSignature.Value, issuerDataCounter.getAsInt());
return TLVList.fromBytes(bsIssuerDataEx.toByteArray());
} else {
throw new TangemException("Invalid issuer data read (signature verification failed)");
}
} catch (Exception e) {
e.printStackTrace();
throw new TangemException("Invalid issuer data read");
}
} else {
mCard.setIssuerDataEx(null, null, issuerDataCounter.getAsInt());
return null;
}
}
/**
* Series of WRITE_ISSUER_DATA command to write extra data:
* 1. start write procedure
* 2. series of writing data blocks
* 3. finalize write procedure
* This command make signatures with provided issuer privateDataKey
* - to start write procedure (sign CID|Counter|Size)
* - to finalize write procedure (sign CID|dataEx|Counter)
* See [1] 3.3, 8.8
*
* @throws Exception - if something went wrong
*/
public void run_WriteIssuerDataEx(Notifications notifications, byte[] dataEx, byte[] issuerPrivateDataKey) throws Exception {
boolean needCounter = readResult.getTLV(TLV.Tag.TAG_SettingsMask) != null && (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
Log.i(logTag, "WriteIssuerDataEx - Start");
int issuerDataCounter = 0;
ByteArrayOutputStream osDataToSign = new ByteArrayOutputStream();
osDataToSign.write(mCard.getCID());
if (needCounter) {
issuerDataCounter = mCard.getIssuerDataExCounter() + 1;
osDataToSign.write(Util.intToByteArray4(issuerDataCounter));
}
osDataToSign.write(Util.intToByteArray2(dataEx.length));
byte[] issuerDataSignature = CardCrypto.Signature(issuerPrivateDataKey, osDataToSign.toByteArray());
CommandApdu rqApdu = StartPrepareCommand(INS.WriteIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x01);
if (needCounter) {
rqApdu.addTLV_U32(TLV.Tag.TAG_Issuer_Data_Counter, issuerDataCounter);
}
rqApdu.addTLV_U16(TLV.Tag.TAG_Size, dataEx.length);
rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature);
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
ResponseApdu rspApdu = SendAndReceive(rqApdu, true);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
if (notifications != null)
notifications.onReadProgress(this, 10);
Log.i(logTag, "WriteIssuerDataEx - Send data");
int offset = 0;
int partSize = 1524;
while (offset < dataEx.length) {
if (dataEx.length - offset < partSize) {
partSize = dataEx.length - offset;
}
byte[] part = Arrays.copyOfRange(dataEx, offset, offset + partSize);
rqApdu = StartPrepareCommand(INS.WriteIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x02);
rqApdu.addTLV_U16(TLV.Tag.TAG_Offset, offset);
rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data, part);
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
rspApdu = SendAndReceive(rqApdu, true);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
offset += partSize;
if (notifications != null)
notifications.onReadProgress(this, 10+(int) Math.round(offset * 80.0 / dataEx.length));
}
Log.i(logTag, "WriteIssuerDataEx - Finalize");
osDataToSign = new ByteArrayOutputStream();
osDataToSign.write(mCard.getCID());
osDataToSign.write(dataEx);
if (needCounter) {
osDataToSign.write(Util.intToByteArray4(issuerDataCounter));
}
issuerDataSignature = CardCrypto.Signature(issuerPrivateDataKey, osDataToSign.toByteArray());
rqApdu = StartPrepareCommand(INS.WriteIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x03);
rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature);
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
rspApdu = SendAndReceive(rqApdu, true);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
if (notifications != null)
notifications.onReadProgress(this, 100);
}
/**
* Execute consecutive READ commands with increasing encryption level from EncryptionMode.None to EncryptionMode.Strong, see {@link TangemCard.EncryptionMode}
* If card requires stricter encryption level it returns SW.NEED_ENCRYPTION status word

View file

@ -0,0 +1,25 @@
package com.tangem.tangem_card.reader;
/**
* Created by dvol on 15.07.2019.
*/
public class ProductMask {
public static final int Note = 0x0001;
public static final int Tag = 0x0002;
public static final int IdCard = 0x0004;
public static String getDescription(int iValue) {
StringBuilder sb = new StringBuilder();
sb.append("[");
if ((iValue & ProductMask.Note) != 0) sb.append("Note, ");
if ((iValue & ProductMask.Tag) != 0)
sb.append("Tag, ");
if ((iValue & ProductMask.IdCard) != 0)
sb.append("IdCard, ");
if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length());
sb.append("]");
return sb.toString();
}
}

View file

@ -29,8 +29,16 @@ public class SettingsMask {
public static final int DisablePrecomputedNDEF = 0x00010000;
public static final int SkipSecurityDelayIfValidatedByIssuer = 0x00020000;
public static final int SkipCheckPIN2andCVCIfValidatedByIssuer = 0x00040000;
public static final int SkipSecurityDelayIfValidatedByLinkedTerminal = 0x00080000;
public static final int RestrictOverwriteIssuerDataEx = 0x00100000;
public static final int RequireTermTxSignature = 0x01000000;
public static final int RequireTermCertSignature = 0x02000000;
public static final int CheckPIN3onCard = 0x04000000;
public static String getDescription(int iValue) {
StringBuilder sb=new StringBuilder();
sb.append("[");
@ -63,6 +71,24 @@ public class SettingsMask {
if ((iValue & SettingsMask.SkipSecurityDelayIfValidatedByLinkedTerminal) != 0)
sb.append("SkipSecurityDelayIfValidatedByLinkedTerminal, ");
if ((iValue & SettingsMask.SkipSecurityDelayIfValidatedByIssuer) != 0)
sb.append("SkipSecurityDelayIfValidatedByIssuer, ");
if ((iValue & SettingsMask.SkipCheckPIN2andCVCIfValidatedByIssuer) != 0)
sb.append("SkipCheckPIN2andCVCIfValidatedByIssuer, ");
if ((iValue & SettingsMask.RestrictOverwriteIssuerDataEx) != 0)
sb.append("RestrictOverwriteIssuerDataEx, ");
if ((iValue & SettingsMask.RequireTermTxSignature) != 0)
sb.append("RequireTermTxSignature, ");
if ((iValue & SettingsMask.RequireTermCertSignature) != 0)
sb.append("RequireTermCertSignature, ");
if ((iValue & SettingsMask.CheckPIN3onCard) != 0)
sb.append("CheckPIN3onCard, ");
if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length());
sb.append("]");
return sb.toString();

View file

@ -44,15 +44,27 @@ public class TLV {
TAG_Session_Key_B(0x1B),
TAG_Pause(0x1C),
TAG_PIN3(0x1D),
TAG_NewPIN3(0x1E),
TAG_CrEx_Key(0x1F),
TAG_Manufacture_ID(0x20),
TAG_Manufacturer_Signature(0x21),
TAG_Mode(0x23),
TAG_Offset(0x24),
TAG_Size(0x25),
TAG_User_Data(0x2A),
TAG_User_ProtectedData(0x2B),
TAG_Issuer_Data_PublicKey(0x30),
TAG_Issuer_Transaction_PublicKey(0x31),
TAG_Issuer_Data(0x32),
TAG_Issuer_Data_Signature(0x33),
TAG_Issuer_Transaction_Signature(0x34),
TAG_Issuer_Data_Counter(0x35),
TAG_Acquirer_PublicKey(0x37),
TAG_IsActivated(0x3A),
TAG_ActivationSeed(0x3B),
@ -65,12 +77,38 @@ public class TLV {
TAG_TrOut_Hash(0x50),
TAG_TrOut_HashSize(0x51),
TAG_TrOut_Raw(0x52),
TAG_TrOut_Amount(0x53),
TAG_Terminal_PaymentFlowVersion(0x54),
TAG_Terminal_Certificate(0x55),
// hash of extra data to transaction (for SIGN_HASH_EXTERNAL)
TAG_TrOut_Extra_Hash(0x56),
TAG_Terminal_Transaction_Signature(0x57),
TAG_Terminal_Certificate_Id(0x5A),
TAG_Terminal_Certificate_Param(0x5B),
TAG_Terminal_Certificate_PublicKey(0x5C),
TAG_Terminal_Certificate_Signature(0x5D),
TAG_Wallet_PublicKey(0x60),
TAG_Signature(0x61),
TAG_RemainingSignatures(0x62),
TAG_SignedHashes(0x63),
TAG_CheckWalletCounter(0x64),
TAG_CrEx_Salt(0x65),
TAG_CrEx_Message(0x66),
TAG_CrEx_CryptedMessage(0x67),
TAG_CrEx_HMAC(0x68),
TAG_CrEx_RequirePIN3(0x69),
TAG_TrOut_Extra_Signature(0x6A),
TAG_SignCommand_ChecksPassed(0x6B),
TAG_Wallet_PrivateKey(0x70),
TAG_Card_PrivateKey(0x71),
TAG_Block_Reason(0x72),
TAG_Firmware(0x80),
TAG_Batch(0x81),
TAG_ManufactureDateTime(0x82),
@ -79,6 +117,10 @@ public class TLV {
TAG_Manufacturer_PublicKey(0x85),
TAG_CardID_Manufacturer_Signature(0x86),
TAG_ProductMask(0x8A),
TAG_PinLessFloorLimit(0x90),
TAG_Token_Symbol(0xA0),
TAG_Token_Contract_Address(0xA1),
TAG_Token_Decimal(0xA2),
@ -87,6 +129,14 @@ public class TLV {
TAG_LastSign_Date(0xC2),
TAG_DenominationText(0xC3),
TAG_FullName (0xD0),
TAG_Birthday (0xD1),
TAG_Photo (0xD2),
TAG_Gender(0xD3),
TAG_IssueDate (0xD4),
TAG_ExpireDate(0xD5),
TAG_TrustedAddress (0xD6),
TAG_Terminal_IsLinked(0x58),
TAG_Terminal_PublicKey(0x5C),
TAG_Terminal_TransactionSignature(0x57);
@ -172,7 +222,7 @@ public class TLV {
}
public String getAsString() {
if( Value.length==0 ) return "";
if (Value.length == 0) return "";
if (Value[Value.length - 1] == 0) {
String s1 = new String(Arrays.copyOfRange(Value, 0, Value.length - 1), Charset.forName("utf-8"));
@ -214,18 +264,29 @@ public class TLV {
return String.format("%s[]: [[NULL]]", tag.name());
}
case TAG_SettingsMask: {
StringBuilder sb=new StringBuilder();
if( Value!=null ) {
StringBuilder sb = new StringBuilder();
if (Value != null) {
try {
int iValue = Util.byteArrayToInt(Value);
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), SettingsMask.getDescription(iValue));
}
catch (Exception e)
{
} catch (Exception e) {
e.printStackTrace();
return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value));
}
}else{
} else {
return String.format("%s[]: [[NULL]]", tag.name());
}
}
case TAG_ProductMask: {
if (Value != null) {
try {
int iValue = Util.byteArrayToInt(Value);
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));
}
} else {
return String.format("%s[]: [[NULL]]", tag.name());
}
}

View file

@ -9,10 +9,11 @@ import com.tangem.tangem_card.util.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
public class TLVList extends ArrayList<TLV> {
public class TLVList extends ArrayList<TLV> implements Serializable {
public String getParsedTLVs(String Prefix) {
String parsed = "";
for (int i = 0; i < size(); i++) {
@ -85,4 +86,8 @@ public class TLVList extends ArrayList<TLV> {
while (tlv != null);
return tlvList;
}
public boolean hasTag(TLV.Tag tag) {
return getTLV(tag)!=null;
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.tangem_card.data.external.PINsProvider;
import com.tangem.tangem_card.reader.CardCrypto;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.reader.NfcReader;
import com.tangem.tangem_card.reader.ProductMask;
import com.tangem.tangem_card.reader.TLV;
import com.tangem.tangem_card.reader.TLVException;
import com.tangem.tangem_card.reader.TLVList;
@ -137,6 +138,12 @@ public class CustomReadCardTask extends Thread {
mCard.setBatch(tlvCardData.getTLV(TLV.Tag.TAG_Batch).getAsHexString());
if( tlvCardData.hasTag(TLV.Tag.TAG_ProductMask)) {
mCard.setProductMask(tlvCardData.getTagAsInt(TLV.Tag.TAG_ProductMask));
}else{
mCard.setProductMask(ProductMask.Note);
}
TLV tokenSymbol = tlvCardData.getTLV(TLV.Tag.TAG_Token_Symbol);
TLV contractAddress = tlvCardData.getTLV(TLV.Tag.TAG_Token_Contract_Address);
TLV tokens_decimal = tlvCardData.getTLV(TLV.Tag.TAG_Token_Decimal);

View file

@ -46,6 +46,10 @@ public class VerifyCardTask extends CustomReadCardTask {
}
mNotifications.onReadProgress(protocol, 90);
if( protocol.getCard().isIDCard() )
{
protocol.run_GetIssuerDataEx(mNotifications);
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tangem_card.tasks;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.data.external.CardDataSubstitutionProvider;
import com.tangem.tangem_card.data.external.PINsProvider;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.reader.NfcReader;
/**
* Created by dvol on 19.01.2020.
*/
public class WriteIDCardTask extends CustomReadCardTask {
public static final String TAG = WriteIDCardTask.class.getSimpleName();
private TangemCard.IDCardData idCardData;
private byte[] issuerPrivateDataKey;
public WriteIDCardTask(TangemCard card, NfcReader reader, CardDataSubstitutionProvider cardDataSubstitutionProvider, PINsProvider pinsProvider, TangemCard.IDCardData idCardData, byte[] issuerPrivateDataKey, CardProtocol.Notifications notifications) {
super(card, reader, cardDataSubstitutionProvider, pinsProvider, notifications);
this.idCardData=idCardData;
this.issuerPrivateDataKey=issuerPrivateDataKey;
}
@Override
public void run_Task() throws Exception {
mNotifications.onReadProgress(protocol, 0);
if (isCancelled) return;
if( !protocol.getCard().isIDCard() )
throw new Exception("It's not an IDCard!");
protocol.run_WriteIssuerDataEx(mNotifications,idCardData.toTLVList().toBytes(),issuerPrivateDataKey);
mNotifications.onReadProgress(protocol, 100);
}
}

View file

@ -10,7 +10,8 @@ data class CardEnvironment(
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionKey: ByteArray? = null
val encryptionKey: ByteArray? = null,
val cvc: ByteArray? = null
) {
companion object {

View file

@ -122,6 +122,37 @@ class CardManager(
runTask(task, cardId, callback)
}
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
* @param cardId CID, Unique Tangem card ID number.
*/
fun createWallet(cardId: String,
callback: (result: TaskEvent<CreateWalletResponse>) -> Unit) {
val createWalletCommand = CreateWalletCommand(cardId)
val task = SingleCommandTask(createWalletCommand)
runTask(task, cardId, callback)
}
/**
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
* If Is_Reusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
* @param cardId CID, Unique Tangem card ID number.
*/
fun purgeWallet(cardId: String,
callback: (result: TaskEvent<PurgeWalletResponse>) -> Unit) {
val purgeWalletCommand = PurgeWalletCommand(cardId)
val task = SingleCommandTask(purgeWalletCommand)
runTask(task, cardId, callback)
}
/**
*/
@ -157,6 +188,6 @@ class CardManager(
}
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
return cardEnvironmentRepository[cardId] ?: CardEnvironment()
return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId)
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @property cardId CID, Unique Tangem card ID number.
*/
class CreateWalletCommand(
private val cardId: String
) : CommandSerializer<CreateWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
if (cardEnvironment.cvc != null) {
tlvData.add(Tlv(TlvTag.Cvc, cardEnvironment.cvc))
}
return CommandApdu(Instruction.CreateWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
CreateWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status),
walletPublicKey = mapper.map(TlvTag.WalletPublicKey)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class PurgeWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus
) : CommandResponse
/**
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
* If Is_Reusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
* @property cardId CID, Unique Tangem card ID number.
*/
class PurgeWalletCommand(
private val cardId: String
) : CommandSerializer<PurgeWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
return CommandApdu(Instruction.PurgeWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
PurgeWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status))
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) {
Pause(0x1C),
ManufactureId(0x20),
ManufacturerSignature(0x21),
ManufacturerSignature(0x86),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),

View file

@ -45,6 +45,8 @@ class MainActivity : AppCompatActivity() {
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
}
}
@ -103,6 +105,32 @@ class MainActivity : AppCompatActivity() {
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
cardManager.purgeWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
btn_create_wallet?.setOnClickListener { _ ->
cardManager.createWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
}
private fun createSampleHashes(): Array<ByteArray> {

View file

@ -9,12 +9,12 @@
<TextView
android:paddingBottom="48dp"
android:layout_marginTop="48dp"
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_marginBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -25,6 +25,7 @@
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read and verify card"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -63,5 +64,27 @@
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_create_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Create wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_purge_wallet"
android:enabled="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,6 +3,7 @@ package com.tangem.tangem_sdk.data
import android.os.Bundle
import com.tangem.tangem_card.data.Manufacturer
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.util.Log
import java.util.*
@ -61,6 +62,13 @@ fun TangemCard.loadFromBundle(B: Bundle) {
setIssuerData(B.getByteArray("IssuerData"), B.getByteArray("IssuerDataSignature"))
else setIssuerData(null, null)
if (B.containsKey("IssuerDataEx") && B.containsKey("IssuerDataExSignature") && B.containsKey("IssuerDataExCounter"))
setIssuerDataEx(B.getByteArray("IssuerDataEx"), B.getByteArray("IssuerDataExSignature"), B.getInt("IssuerDataExCounter"))
else setIssuerDataEx(null, null, 0)
if( B.containsKey("ProductMask") )
setProductMask(B.getInt("ProductMask"))
if (B.containsKey("NeedWriteIssuerData"))
needWriteIssuerData = B.getBoolean("NeedWriteIssuerData")
@ -86,6 +94,8 @@ fun TangemCard.loadFromBundle(B: Bundle) {
terminalIsLinked = B.getBoolean("terminalIsLinked")
if (B.containsKey("tagSignature")) tagSignature = B.getByteArray("tagSignature")
if (B.containsKey("idHash")) idHash = B.getByteArray("idHash")
if (B.containsKey("tlvIDCardData")) tlvIDCardData = B.getSerializable("tlvIDCardData") as TLVList
}
@ -149,6 +159,14 @@ fun TangemCard.saveToBundle(B: Bundle) {
B.putBoolean("NeedWriteIssuerData", needWriteIssuerData)
}
if (issuerDataEx != null && issuerDataExSignature != null) {
B.putByteArray("IssuerDataEx", issuerDataEx)
B.putByteArray("IssuerDataExSignature", issuerDataExSignature)
B.putInt("IssuerDataExCounter", issuerDataExCounter)
}
B.putInt("ProductMask",productMask)
if (isCodeConfirmed != null)
B.putBoolean("codeConfirmed", isCodeConfirmed)
@ -166,6 +184,9 @@ fun TangemCard.saveToBundle(B: Bundle) {
if (tagSignature != null) B.putByteArray("tagSignature", tagSignature)
if (idHash != null) B.putByteArray("idHash", idHash)
if (tlvIDCardData != null) B.putSerializable("tlvIDCardData", tlvIDCardData)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<vector android:height="36dp" android:tint="#505050"
android:viewportHeight="36.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>