Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-14 19:04:29 +03:00
commit c1a47fbb59
14 changed files with 966 additions and 15 deletions

View file

@ -26,6 +26,7 @@ public enum Blockchain {
Stellar("XLM", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"),
StellarTestNet("XLM/test", "XLM", 10000000.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"),
Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.tangem2, "Tezos");

View file

@ -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 {

View file

@ -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 }
}

View file

@ -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
}
}

View file

@ -1,25 +1,25 @@
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.tezos.TezosEngine
import com.tangem.wallet.xlm.XlmAssetEngine
import com.tangem.wallet.xlm.XlmEngine
import com.tangem.wallet.xlmtag.XlmTagEngine
import com.tangem.wallet.xrp.XrpEngine
/**
@ -50,6 +50,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()
Blockchain.Tezos -> TezosEngine()
@ -88,6 +89,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)

View file

@ -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)
}
}
}

View file

@ -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<OperationResponse>()
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<InputFilter> {
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<ByteArray> {
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"
}
}

View file

@ -0,0 +1,9 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tangem.ui.fragment.additional.TagFragment">
<include layout="@layout/fr_loaded_wallet"/>
</FrameLayout>

View file

@ -28,6 +28,9 @@
<action
android:id="@+id/action_main_to_settingsFragment"
app:destination="@id/settingsFragment" />
<action
android:id="@+id/action_main_to_tagFragment"
app:destination="@id/tagFragment" />
</fragment>
<fragment
@ -188,5 +191,10 @@
android:id="@+id/prepareKrakenWithdrawalFragment"
android:name="com.tangem.ui.fragment.additional.PrepareKrakenWithdrawalFragment"
android:label="PrepareKrakenWithdrawalFragment" />
<fragment
android:id="@+id/tagFragment"
android:name="com.tangem.ui.fragment.additional.TagFragment"
android:label="fragment_tag"
tools:layout="@layout/fragment_tag" />
</navigation>

View file

@ -264,6 +264,12 @@
<string name="details_linked_card_to_phone">To this phone</string>
<string name="details_unspents_number">%1$d unspents (%2$d received)</string>
<!-- TagFragment -->
<string name="tag_claim">Claim</string>
<string name="tag_genuine">GENUINE</string>
<string name="tag_not_genuine">NOT GENUINE</string>
<string name="tag_claimed">ALREADY CLAIMED</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Save</string>
<string name="pin_save_btn_delete">Delete</string>

View file

@ -3,5 +3,6 @@
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
</resources>

View file

@ -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);

View file

@ -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

View file

@ -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)
}