From d5c411471b91d6e9a896183e971153cf88a0f321 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Jan 2020 23:14:32 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../main/java/com/tangem/data/Blockchain.java | 1 + .../tangem/data/network/ServerApiStellar.java | 7 +- .../com/tangem/ui/fragment/MainFragment.kt | 91 ++++ .../ui/fragment/additional/TagFragment.kt | 214 ++++++++ .../com/tangem/wallet/CoinEngineFactory.kt | 15 +- .../com/tangem/wallet/xlmtag/XlmTagData.kt | 133 +++++ .../com/tangem/wallet/xlmtag/XlmTagEngine.kt | 471 ++++++++++++++++++ app/src/main/res/layout/fragment_tag.xml | 9 + app/src/main/res/navigation/main_graph.xml | 8 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/nfc_tech_filter.xml | 1 + .../tangem/tangem_card/data/TangemCard.java | 9 + .../tangem_sdk/android/reader/NfcManager.kt | 3 +- .../tangem_sdk/data/TangemCardExtensions.kt | 13 +- 14 files changed, 966 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt create mode 100644 app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt create mode 100644 app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt create mode 100644 app/src/main/res/layout/fragment_tag.xml diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 92db5c5d43..234548d273 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -26,6 +26,7 @@ public enum Blockchain { Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"), StellarAsset("Asset", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"), + StellarTag("XLM-Tag", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"), Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java index f364820eb1..77427e3fc4 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java @@ -33,7 +33,7 @@ import io.reactivex.schedulers.Schedulers; public class ServerApiStellar { public ServerApiStellar(Blockchain blockchain) { - if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset) { + if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) { currentURL = ServerURL.API_STELLAR; } else { currentURL = ServerURL.API_STELLAR_TESTNET; @@ -173,10 +173,11 @@ public class ServerApiStellar { stellarRequest.setError(null); try { Server server; - if (ctx.getBlockchain() == Blockchain.Stellar || ctx.getBlockchain() == Blockchain.StellarAsset) { + Blockchain blockchain = ctx.getBlockchain(); + if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) { Network.usePublicNetwork(); server = new Server(currentURL); - } else if (ctx.getBlockchain() == Blockchain.StellarTestNet) { + } else if (blockchain == Blockchain.StellarTestNet) { Network.useTestNetwork(); server = new Server(currentURL); } else { diff --git a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index a8b9b4ac02..cecb0ddeee 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -7,10 +7,13 @@ 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 import android.text.SpannableString import android.text.style.ForegroundColorSpan +import android.util.Log import android.view.* import android.widget.PopupMenu import android.widget.TextView @@ -20,9 +23,13 @@ import androidx.core.os.bundleOf import androidx.lifecycle.ViewModelProviders import com.tangem.App import com.tangem.Constant +import com.tangem.data.Blockchain import com.tangem.data.Logger import com.tangem.tangem_card.data.TangemCard import com.tangem.tangem_card.reader.CardProtocol +import com.tangem.tangem_card.reader.TLV +import com.tangem.tangem_card.reader.TLVException +import com.tangem.tangem_card.reader.TLVList import com.tangem.tangem_card.tasks.CustomReadCardTask import com.tangem.tangem_card.tasks.ReadCardInfoTask import com.tangem.tangem_sdk.android.data.PINStorage @@ -45,10 +52,16 @@ 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 kotlinx.android.synthetic.main.fragment_main.* import kotlinx.android.synthetic.main.layout_touch_card.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import java.io.File import java.util.* +import kotlin.coroutines.CoroutineContext class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback, CardProtocol.Notifications, androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener, @@ -68,6 +81,11 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader private var zipFile: File? = null private var unknownBlockchain = false + private val parentJob = Job() + private val coroutineContext: CoroutineContext + get() = parentJob + Dispatchers.IO + private val scope = CoroutineScope(coroutineContext) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { setHasOptionsMenu(true) return super.onCreateView(inflater, container, savedInstanceState) @@ -152,6 +170,8 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader return } + parseNfcvTag(tag) + try { // get IsoDep handle and run cardReader thread val isoDep = IsoDep.get(tag) @@ -175,6 +195,77 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } } + private fun parseNfcvTag(tag: Tag) { + if (NfcV.get(tag) != null) { + if (Ndef.get(tag) != null) { + try { + onNdefDiscovered(Ndef.get(tag), tag.id) + } catch (e: Exception) { + e.printStackTrace() + (activity as MainActivity).nfcManager.notifyReadResult(false) + } + return + } else { + (activity as MainActivity).nfcManager.notifyReadResult(false) + return + } + } + } + + private fun onNdefDiscovered(ndef: Ndef, uid: ByteArray) { + scope.launch { + + + try { + ndef.connect() + val records = ndef.ndefMessage.records + for (record in records) { + if (record.toUri() != null) { + when (record.toUri().toString()) { + "vnd.android.nfc://ext/tangem.com:wallet" -> { + //mConsole.write(Util.bytesToHex(record.getPayload()), MessageAdapter.MSG_OKAY, "", null, false); + val payload = record.payload + Log.v(TAG, "tangem.com:wallet[${payload.size} bytes]:") + try { + val tlvNDEF: TLVList = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size)) + val cardDataTlv = TLVList.fromBytes((tlvNDEF.getTLV(TLV.Tag.TAG_CardData)).Value) + Log.v(TAG, "\n" + tlvNDEF.getParsedTLVs("")) + val card = TangemCard(uid.toString()) + card.batch = cardDataTlv.getTLV(TLV.Tag.TAG_Batch).asHexString + card.setIssuer(cardDataTlv.getTLV(TLV.Tag.TAG_Issuer_ID).Value.toString(), null) + card.blockchainID = Blockchain.StellarTag.id + card.walletPublicKey = tlvNDEF.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value + card.status = TangemCard.Status.Loaded + card.tagSignature = tlvNDEF.getTLV(TLV.Tag.TAG_Signature).Value + + val ctx = TangemContext(card) + val engineCoin = XlmTagEngine(ctx) + engineCoin.defineWallet() + launch(Dispatchers.Main) { + + 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_tagFragment, bundle) + } + } catch (e: TLVException) { + e.printStackTrace() + Log.v(TAG, e.message) + } + } + else -> Log.v(TAG, record.toUri().toString()) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + (activity as MainActivity).nfcManager.notifyReadResult(false) + } + + } + } + override fun onReadStart(cardProtocol: CardProtocol) { rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE } } diff --git a/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt new file mode 100644 index 0000000000..dd2d27594c --- /dev/null +++ b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt @@ -0,0 +1,214 @@ +package com.tangem.ui.fragment.additional + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.Html +import android.view.View +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.tangem.data.network.ServerApiCommon +import com.tangem.server_android.ServerApiTangem +import com.tangem.ui.fragment.BaseFragment +import com.tangem.ui.fragment.wallet.LoadedWalletFragment +import com.tangem.ui.fragment.wallet.LoadedWalletViewModel +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 kotlinx.android.synthetic.main.fr_loaded_wallet.* +import kotlinx.android.synthetic.main.layout_btn_details.* +import kotlinx.android.synthetic.main.layout_tangem_card.* + + +class TagFragment : BaseFragment(), NavigationResultListener { + override val layoutId = R.layout.fragment_tag + private lateinit var viewModel: LoadedWalletViewModel + private lateinit var ctx: TangemContext + private var serverApiCommon: ServerApiCommon = ServerApiCommon() + private var serverApiTangem: ServerApiTangem = ServerApiTangem() + + private var requestCounter: Int = 0 + set(value) { + field = value + LOG.i(LoadedWalletFragment.TAG, "requestCounter, set $field") + if (field <= 0) { + LOG.e(LoadedWalletFragment.TAG, "+++++++++++ FINISH REFRESH") + if (srl != null && srl.isRefreshing) + srl.isRefreshing = false + } else if (srl != null && !srl.isRefreshing) + srl.isRefreshing = true + } + + override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ctx = TangemContext.loadFromBundle(context, arguments) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val engine = XlmTagEngine(ctx) + + + btnLoad.visibility = View.GONE + btnDetails.visibility = View.GONE + btnExtract.text = getString(R.string.tag_claim) + btnExtract.isEnabled = false //TODO: enable when we implement extraction + btnExtract.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.btn_dark) + + ivTangemCard.setImageResource(R.drawable.card_default_nft) + + tvBalance.setSingleLine(!engine.needMultipleLinesForBalance()) + tvWallet.text = ctx.coinData.wallet + tvWallet.setOnClickListener { shareWallet() } + btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine.walletExplorerUri)) } + btnCopy.setOnClickListener { shareWallet() } + btnNewScan.setOnClickListener { navigateUp() } + srl?.setOnRefreshListener { refresh(true) } + + requestBalanceAndUnspentTransactions() +// update() + } + + + private fun shareWallet() { + val txtShare = ctx.coinData.wallet + val clipboard = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare) + Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show() + } + + + private fun update() { + ctx.coinData.setIsBalanceEqual(true) + + if (srl.isRefreshing) { + tvBalanceLine1.setTextColor(resources.getColor(R.color.primary)) + tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain) + tvBalanceLine2.text = "" + tvBalance.text = "" + tvBalanceEquivalent.text = "" + } else { + val validator = BalanceValidator() + validator.check(ctx, false) + context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) } + tvBalanceLine1?.text = getString(validator.firstLine) + tvBalanceLine2?.text = getString(validator.getSecondLine(false)) + } + + val engine = CoinEngineFactory.create(ctx) + when { + engine!!.hasBalanceInfo() -> { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(engine.balanceHTML, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(engine.balanceHTML) + tvBalance.text = html + tvBalanceEquivalent.text = engine.balanceEquivalent + } + + ctx.card?.offlineBalance != null -> { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(engine.offlineBalanceHTML, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(engine.offlineBalanceHTML) + tvBalance.text = html + } + + 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 + + } + + private fun requestBalanceAndUnspentTransactions() { + if (UtilHelper.isOnline(context as Activity)) { + val coinEngine = CoinEngineFactory.create(ctx) + 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) + } + update() + } + + override fun onProgress() { + if (activity == null) return + LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress") +// update() + } + + 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) + update() + } + } + + private fun refresh(clearData: Boolean = true) { + if (ctx.card == null) return + + // clear all card data and request again + ctx.coinData.clearInfo() + + if (clearData) { + ctx.error = null + ctx.message = null + } + + LOG.w(TAG, "============= START REFRESH") + requestCounter = 0 + srl?.isRefreshing = true + + update() + + ctx.coinData.setIsBalanceEqual(true) + + + requestBalanceAndUnspentTransactions() + + if (requestCounter == 0) { + // if no connection and no requests posted + srl?.isRefreshing = false + update() + } + } + + companion object { + val TAG: String = TagFragment::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 7a39a24de2..5df7370dce 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -1,24 +1,24 @@ package com.tangem.wallet import android.util.Log - -import com.tangem.wallet.btc.BtcEngine -import com.tangem.wallet.eth.EthEngine -import com.tangem.wallet.token.TokenEngine -import com.tangem.wallet.bch.BtcCashEngine import com.tangem.data.Blockchain -import com.tangem.wallet.eos.EosEngine +import com.tangem.wallet.bch.BtcCashEngine import com.tangem.wallet.binance.BinanceEngine +import com.tangem.wallet.btc.BtcEngine import com.tangem.wallet.cardano.CardanoData 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.ltc.LtcEngine import com.tangem.wallet.matic.MaticTokenEngine import com.tangem.wallet.nftToken.NftTokenEngine import com.tangem.wallet.rsk.RskEngine import com.tangem.wallet.rsk.RskTokenEngine +import com.tangem.wallet.token.TokenEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine +import com.tangem.wallet.xlmtag.XlmTagEngine import com.tangem.wallet.xrp.XrpEngine /** @@ -49,6 +49,7 @@ object CoinEngineFactory { Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine() Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine() Blockchain.StellarAsset -> XlmAssetEngine() + Blockchain.StellarTag -> XlmTagEngine() Blockchain.Eos -> EosEngine() Blockchain.Ducatus -> DucatusEngine() else -> null @@ -86,6 +87,8 @@ object CoinEngineFactory { XlmEngine(context) else if (Blockchain.StellarAsset == context.blockchain) XlmAssetEngine(context) + else if (Blockchain.StellarTag == context.blockchain) + XlmTagEngine(context) else if (Blockchain.Eos == context.blockchain) EosEngine(context) else if (Blockchain.Ducatus == context.blockchain) diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt new file mode 100644 index 0000000000..bdd8f4e3af --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt @@ -0,0 +1,133 @@ +package com.tangem.wallet.xlmtag + +import android.os.Bundle +import android.util.Log +import com.tangem.wallet.CoinData +import com.tangem.wallet.CoinEngine +import com.tangem.wallet.CoinEngine.InternalAmount +import org.stellar.sdk.KeyPair +import org.stellar.sdk.responses.AccountResponse +import org.stellar.sdk.responses.LedgerResponse +import java.math.BigDecimal + + +class XlmTagData : CoinData() { + class AccountResponseEx internal constructor(accountId: String?, sequenceNumber: Long?) : AccountResponse(KeyPair.fromAccountId(accountId), sequenceNumber) + + private var balance: CoinEngine.Amount? = null + private var sequenceNumber: Long? = 0L + private var baseReserve: CoinEngine.Amount? = CoinEngine.Amount("0.5", "XLM") + var baseFee: CoinEngine.Amount? = CoinEngine.Amount("0.00001", "XLM") + private set + var isError404 = false + var isTargetAccountCreated = false + var fundsFromTrustedSource = false + var fundsSentToTrustedSource = false + + + override fun clearInfo() { + super.clearInfo() + balance = null + isError404 = false + isTargetAccountCreated = false + fundsFromTrustedSource = false + fundsSentToTrustedSource = false + } + + fun getBalance(): CoinEngine.Amount? { + return if (balance != null) { + CoinEngine.Amount(balance!!.subtract(reserve), "XLM") + } else { + null + } + } + + val reserve: CoinEngine.Amount + get() = CoinEngine.Amount(baseReserve!!.multiply(BigDecimal.valueOf(2)), "XLM") + + var accountResponse: AccountResponse + get() = XlmTagData.AccountResponseEx(wallet, sequenceNumber) + set(accountResponse) { + if (accountResponse.balances.size > 0) { + val balanceResponse = accountResponse.balances[0] + balance = CoinEngine.Amount(balanceResponse.balance, "XLM") + } + sequenceNumber = accountResponse.sequenceNumber + isBalanceReceived = true + } + + fun setLedgerResponse(ledgerResponse: LedgerResponse) { + val xlmEngine = XlmTagEngine() + baseReserve = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseReserveInStroops, "stroops")) + baseFee = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseFeeInStroops, "stroops")) + } + + fun incSequenceNumber() { + sequenceNumber = sequenceNumber?.inc() + } + + override fun loadFromBundle(B: Bundle) { + super.loadFromBundle(B) + balance = if (B.containsKey("BalanceCurrency") && B.containsKey("BalanceDecimal")) { + CoinEngine.Amount(B.getString("BalanceDecimal"), B.getString("BalanceCurrency")) + } else { + null + } + sequenceNumber = if (B.containsKey("sequenceNumber")) { + B.getLong("sequenceNumber") + } else { + 0L + } + baseReserve = if (B.containsKey("BaseReserveCurrency") && B.containsKey("BaseReserveDecimal")) { + CoinEngine.Amount(B.getString("BaseReserveDecimal"), B.getString("BaseReserveCurrency")) + } else { + CoinEngine.Amount("0.5", "XLM") + } + baseFee = if (B.containsKey("BaseFeeCurrency") && B.containsKey("BaseFeeDecimal")) { + CoinEngine.Amount(B.getString("BaseFeeDecimal"), B.getString("BaseFeeCurrency")) + } else { + CoinEngine.Amount("0.00001", "XLM") + } + if (B.containsKey("Error404")) isError404 = B.getBoolean("Error404") else isError404 = false + if (B.containsKey("TargetAccountCreated")) isTargetAccountCreated = B.getBoolean("TargetAccountCreated") else isTargetAccountCreated = false + fundsFromTrustedSource = if (B.containsKey("FundsFromTrustedSource")) { + B.getBoolean("FundsFromTrustedSource") + } else { + false + } + fundsSentToTrustedSource = if (B.containsKey("FundsSentToTrustedSource")) { + B.getBoolean("FundsSentToTrustedSource") + } else { + false + } + + } + + override fun saveToBundle(B: Bundle) { + super.saveToBundle(B) + try { + if (balance != null) { + B.putString("BalanceCurrency", balance!!.currency) + B.putString("BalanceDecimal", balance!!.toValueString()) + } + if (sequenceNumber != null) { + B.putLong("sequenceNumber", sequenceNumber!!) + } + if (baseReserve != null) { + B.putString("BaseReserveCurrency", baseReserve!!.currency) + B.putString("BaseReserveDecimal", baseReserve!!.toValueString()) + } + if (baseFee != null) { + B.putString("BaseFeeCurrency", baseFee!!.currency) + B.putString("BaseFeeDecimal", baseFee!!.toValueString()) + } + if (isError404) B.putBoolean("Error404", true) + if (isTargetAccountCreated) B.putBoolean("TargetAccountCreated", true) + if (fundsFromTrustedSource) B.putBoolean("FundsFromTrustedSource", true) + if (fundsSentToTrustedSource) B.putBoolean("FundsSentToTrustedSource", true) + + } catch (e: Exception) { + Log.e("Can't save to bundle ", e.message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt new file mode 100644 index 0000000000..4fe5de60ca --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt @@ -0,0 +1,471 @@ +package com.tangem.wallet.xlmtag + +import android.net.Uri +import android.os.StrictMode +import android.text.InputFilter +import android.util.Log +import com.tangem.App +import com.tangem.data.Blockchain +import com.tangem.data.network.ServerApiStellar +import com.tangem.data.network.StellarRequest +import com.tangem.data.network.StellarRequest.Ledgers +import com.tangem.data.network.StellarRequest.SubmitTransaction +import com.tangem.tangem_card.data.TangemCard +import com.tangem.tangem_card.tasks.SignTask +import com.tangem.tangem_card.util.Util +import com.tangem.util.DecimalDigitsInputFilter +import com.tangem.wallet.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.stellar.sdk.* +import org.stellar.sdk.requests.RequestBuilder +import org.stellar.sdk.responses.operations.OperationResponse +import org.stellar.sdk.responses.operations.PaymentOperationResponse +import java.io.IOException +import java.math.BigDecimal + + +class XlmTagEngine : CoinEngine { + var coinData: XlmTagData? = null + val operations = mutableListOf() + + constructor(context: TangemContext) : super(context) { + if (context.coinData == null) { + coinData = XlmTagData() + context.coinData = coinData + } else if (context.coinData is XlmTagData) { + coinData = context.coinData as XlmTagData + } else { + throw Exception("Invalid type of Blockchain data for XlmEngine") + } + } + + constructor() : super() {} + + @Throws(Exception::class) + private fun checkBlockchainDataExists() { + if (coinData == null) throw Exception("No blockchain data") + } + + override fun awaitingConfirmation(): Boolean { + return App.pendingTransactionsStorage.hasTransactions(ctx.card) + } + + 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) + } else { + ctx.getString(R.string.tag_not_genuine) + } + } + + override fun getBalanceCurrency(): String { + return "XLM" + } + + override fun isBalanceNotZero(): Boolean { + if (coinData == null) return false + return if (coinData!!.getBalance() == null) false else coinData!!.getBalance()!!.notZero() + } + + override fun hasBalanceInfo(): Boolean { + return if (coinData == null) false else coinData!!.getBalance() != null || coinData!!.isError404 + } + + override fun isExtractPossible(): Boolean { + 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 fun getFeeCurrency(): String { + return "XLM" + } + + override fun validateAddress(address: String): Boolean { + try { + val kp = KeyPair.fromAccountId(address) + } catch (e: Exception) { + return false + } + return true + } + + override fun isNeedCheckNode(): Boolean { + return false + } + + override fun getWalletExplorerUri(): Uri { + return Uri.parse("https://stellar.expert/explorer/public/account/" + ctx.coinData.wallet) + } + + override fun getShareWalletUri(): Uri { + return if (ctx.card.denomination != null) { + Uri.parse(ctx.coinData.wallet + "?amount=" + convertToAmount(convertToInternalAmount(ctx.card.denomination)!!).toValueString()) + } else { + Uri.parse(ctx.coinData.wallet) + } + } + + override fun getAmountInputFilters(): Array { + return arrayOf(DecimalDigitsInputFilter(decimals)) + } + + override fun checkNewTransactionAmount(amount: Amount): Boolean { + return true + } + + override fun checkNewTransactionAmountAndFee(amountValue: Amount, feeValue: Amount, isIncludeFee: Boolean): Boolean { + return true + } + + 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 + } + } + + if (coinData!!.isBalanceReceived && coinData!!.isBalanceEqual) { + 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) + } + } + + 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 + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun getBalance(): Amount? { + return if (!hasBalanceInfo()) null else coinData!!.getBalance()!! + } + + override fun evaluateFeeEquivalent(fee: String): String { + return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try { + val feeAmount = Amount(fee, feeCurrency) + feeAmount.toEquivalentString(coinData!!.rate.toDouble()) + } catch (e: Exception) { + "" + } + } + + override fun getBalanceEquivalent(): String { + if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return "" + val balance = balance ?: return "" + return balance.toEquivalentString(coinData!!.rate.toDouble()) + } + + override fun calculateAddress(pkUncompressed: ByteArray): String { + val kp = KeyPair.fromPublicKey(pkUncompressed) + return kp.accountId + } + + override fun convertToAmount(internalAmount: InternalAmount): Amount { + val d = internalAmount.divide(multiplier) + return Amount(d, balanceCurrency) + } + + override fun convertToAmount(strAmount: String, currency: String): Amount { + return Amount(strAmount, currency) + } + + override fun convertToInternalAmount(amount: Amount): InternalAmount { + val d = amount.multiply(multiplier) + return InternalAmount(d, "stroops") + } + + override fun convertToInternalAmount(bytes: ByteArray): InternalAmount? { + if (bytes == null) return null + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return InternalAmount(Util.byteArrayToLong(reversed), "stroops") + } + + override fun convertToByteArray(internalAmount: InternalAmount): ByteArray { + val bytes = Util.longToByteArray(internalAmount.longValueExact()) + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return reversed + } + + override fun createCoinData(): CoinData { + return XlmTagData() + } + + override fun getUnspentInputsDescription(): String { + return "" + } + + @Throws(Exception::class) + override fun constructTransaction(amountValue: Amount, feeValue: Amount, IncFee: Boolean, targetAddress: String): SignTask.TransactionToSign { + var amountValue = amountValue + checkBlockchainDataExists() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + if (IncFee) { + amountValue = Amount(amountValue.subtract(feeValue), amountValue.currency) + } + val operation: Operation + operation = if (coinData!!.isTargetAccountCreated) PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), AssetTypeNative(), amountValue.toValueString()).build() else CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build() + val transaction = TransactionEx.buildEx(60, coinData!!.accountResponse, operation) + if (transaction.fee != convertToInternalAmount(feeValue).intValueExact()) { + throw Exception("Invalid fee!") + } + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw + } + + @Throws(Exception::class) + override fun getHashesToSign(): Array { + val dataForSign = arrayOf(transaction.hash()) + return dataForSign + } + + @Throws(Exception::class) + override fun getRawDataToSign(): ByteArray { + return transaction.signatureBase() + } + + override fun getHashAlgToSign(): String { + return "sha-256" + } + + @Throws(Exception::class) + override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray { + throw Exception("Issuer validation not supported!") + } + + @Throws(Exception::class) + override fun onSignCompleted(signFromCard: ByteArray): ByteArray { // Sign the transaction to prove you are actually the person sending it. + transaction.setSign(signFromCard) + val txForSend = transaction.toEnvelopeXdrBase64().toByteArray() + notifyOnNeedSendTransaction(txForSend) + return txForSend + } + } + } + + private fun checkTargetAccountCreated(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) { + val serverApi = ServerApiStellar(ctx.blockchain) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + coinData!!.isTargetAccountCreated = true + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onFail(request: StellarRequest.Base) { + Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error) + if (request.errorResponse != null && request.errorResponse.code == 404) { + coinData!!.isTargetAccountCreated = false + if (amount.compareTo(coinData!!.reserve) >= 0) { + blockchainRequestsCallbacks.onComplete(true) + } else { + ctx.setError(R.string.confirm_transaction_error_not_enough_xlm_for_create) + blockchainRequestsCallbacks.onComplete(false) + } + } else { // suppose account is created if anything goes wrong + coinData!!.isTargetAccountCreated = true + blockchainRequestsCallbacks.onComplete(true) + } + } + } + serverApi.setListener(listener) + serverApi.requestData(ctx, StellarRequest.Balance(targetAddress)) + } + + private fun requestPayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + val server = Server("https://horizon.stellar.org/") + val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet) + + try { + var operationsPage = server.payments().forAccount(accountKeyPair).order(RequestBuilder.Order.DESC).execute() + Log.e("Stellar", operationsPage.records.toString()) + + operations.addAll(operationsPage.records) + + while (operations.size < OPERATIONS_LIMIT) { + operationsPage = operationsPage.getNextPage(server.httpClient) + operations.addAll(operationsPage.records) + Log.e("Stellar", operationsPage.records.toString()) + Log.e("Stellar", "Operations downloaded: " + operationsPage.records.count()) + Log.e("Stellar", "Operations overall: " + operations.count()) + } + + parsePayments(blockchainRequestsCallbacks) + } catch (e: Exception) { + if (e.message != null) { + ctx.error = e.message + blockchainRequestsCallbacks.onComplete(false) + } else { + ctx.error = e.javaClass.name + blockchainRequestsCallbacks.onComplete(false) + } + } + } + + private fun parsePayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + coinData!!.fundsFromTrustedSource = operations.find { it is PaymentOperationResponse && it.from.accountId == TRUSTED_SOURCE } != null + coinData!!.fundsSentToTrustedSource = + (operations.find { it is PaymentOperationResponse && it.to.accountId == TRUSTED_DESTINATION } != null) && coinData!!.fundsFromTrustedSource + Log.e("Stellar", + "fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " + + "fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource") + blockchainRequestsCallbacks.onComplete(!ctx.hasError()) + } + + 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) + override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) { + coinData!!.maxFee = coinData!!.baseFee + coinData!!.normalFee = coinData!!.maxFee + coinData!!.minFee = coinData!!.normalFee + checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount) + } + + @Throws(IOException::class) + override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray) { + val serverApi = ServerApiStellar(ctx.blockchain) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + try { + if (!SubmitTransaction::class.java.isInstance(request)) throw Exception("Invalid request logic") + val submitTransactionRequest = request as SubmitTransaction + if (submitTransactionRequest.response.isSuccess) { + ctx.error = null + blockchainRequestsCallbacks.onComplete(true) + } else { + if (submitTransactionRequest.response.extras != null && submitTransactionRequest.response.extras.resultCodes != null) { + var trResult = submitTransactionRequest.response.extras.resultCodes.transactionResultCode + if (submitTransactionRequest.response.extras.resultCodes.operationsResultCodes != null && submitTransactionRequest.response.extras.resultCodes.operationsResultCodes.size > 0) { + trResult += "/" + submitTransactionRequest.response.extras.resultCodes.operationsResultCodes[0] + } + ctx.error = trResult + } else { + ctx.error = "transaction failed" + } + blockchainRequestsCallbacks.onComplete(false) + } + } catch (e: Exception) { + if (e.message != null) { + ctx.error = e.message + blockchainRequestsCallbacks.onComplete(false) + } else { + ctx.error = e.javaClass.name + blockchainRequestsCallbacks.onComplete(false) + } + } + } + + override fun onFail(request: StellarRequest.Base) { + ctx.error = request.error + blockchainRequestsCallbacks.onComplete(false) + } + } + serverApi.setListener(listener) + val transaction = TransactionEx.fromEnvelopeXdr(String(txForSend)) + coinData!!.incSequenceNumber() + serverApi.requestData(ctx, SubmitTransaction(transaction)) + } + + override fun needMultipleLinesForBalance(): Boolean { + return true + } + + override fun allowSelectFeeLevel(): Boolean { + return false + } + + override fun pendingTransactionTimeoutInSeconds(): Int { + return 10 + } + + companion object { + private val TAG = XlmTagEngine::class.java.simpleName + private val decimals: Int + private get() = 7 + + private val multiplier = BigDecimal("10000000") + + const val OPERATIONS_LIMIT = 50 + private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM" + private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2" + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tag.xml b/app/src/main/res/layout/fragment_tag.xml new file mode 100644 index 0000000000..87951c04e6 --- /dev/null +++ b/app/src/main/res/layout/fragment_tag.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 7409283746..a65f23db22 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -28,6 +28,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5cfe74312..645ff3bfac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -264,6 +264,12 @@ To this phone %1$d unspents (%2$d received) + + Claim + GENUINE + NOT GENUINE + ALREADY CLAIMED + Save Delete diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml index c10d3b1038..e2921c8701 100644 --- a/app/src/main/res/xml/nfc_tech_filter.xml +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -3,5 +3,6 @@ android.nfc.tech.IsoDep android.nfc.tech.Ndef + android.nfc.tech.NfcV \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java index 4fd4cc1b0f..8fe19575d6 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java @@ -29,6 +29,7 @@ public class TangemCard { private byte[] terminalPrivateKey; private byte[] terminalPublicKey; private boolean terminalIsLinked = false; + private byte[] tagSignature = null; public String getBlockchainID() { return blockchainID; @@ -629,6 +630,14 @@ public class TangemCard { DenominationText = null; } + public byte[] getTagSignature() { + return tagSignature; + } + + public void setTagSignature(byte[] tagSignature) { + this.tagSignature = tagSignature; + } + // public Bundle getAsBundle() { // Bundle B = new Bundle(); // saveToBundle(B); diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt index b59167f216..dffc588721 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt @@ -24,7 +24,8 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb val TAG: String = NfcManager::class.java.simpleName // reader mode flags: listen for type A (not B), skipping ndef check - private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS + private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_V or + NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS private const val DELAY_PRESENCE = 1500 private const val REQUEST_NFC_PERMISSIONS = 1 diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt index f6e825319a..9c6356dd4e 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt @@ -85,6 +85,8 @@ fun TangemCard.loadFromBundle(B: Bundle) { if (B.containsKey("terminalPublicKey")) terminalPublicKey = B.getByteArray("terminalPublicKey") terminalIsLinked = B.getBoolean("terminalIsLinked") + if (B.containsKey("tagSignature")) tagSignature = B.getByteArray("tagSignature") + } val TangemCard.asBundle: Bundle @@ -112,11 +114,10 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putInt("Health", health) if (settingsMask != null) B.putInt("settingsMask", settingsMask) B.putInt("pauseBeforePIN2", pauseBeforePIN2) - if( allowedSigningMethod!=null ){ - var iSigningMethod=0x80 - for(sM in allowedSigningMethod) - { - iSigningMethod=iSigningMethod.or(0x01.shl(sM.ID)) + if (allowedSigningMethod != null) { + var iSigningMethod = 0x80 + for (sM in allowedSigningMethod) { + iSigningMethod = iSigningMethod.or(0x01.shl(sM.ID)) } B.putInt("signingMethod", iSigningMethod) } @@ -163,6 +164,8 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putByteArray("terminalPublicKey", terminalPublicKey) B.putBoolean("terminalIsLinked", terminalIsLinked) + if (tagSignature != null) B.putByteArray("tagSignature", tagSignature) + } catch (e: Exception) { Log.e("Can't save to bundle ", e.message) }