Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-14 09:07:49 +00:00
commit 135a7f5127
28 changed files with 1197 additions and 213 deletions

View file

@ -26,7 +26,9 @@ 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"),
Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS");
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");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;

View file

@ -2,6 +2,7 @@ package com.tangem.data.network;
import com.tangem.data.network.model.InsightBody;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import java.util.List;
@ -18,7 +19,7 @@ public interface InsightApi {
Call<InsightResponse> insightAddress(@Path("address") String address);
@GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS)
Call<List<InsightResponse>> insightUnspent(@Path("address") String address);
Call<List<InsightUtxo>> insightUnspent(@Path("address") String address);
@GET(ServerApiInsight.INSIGHT_TRANSACTION)
Call<InsightResponse> insightTransaction(@Path("txId") String txId);

View file

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import com.tangem.data.network.model.InsightBody;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import java.util.List;
@ -18,11 +19,11 @@ import retrofit2.converter.gson.GsonConverterFactory;
public class ServerApiInsight {
private static String TAG = ServerApiInsight.class.getSimpleName();
public static final String INSIGHT_ADDRESS = "/addr/{address}";
public static final String INSIGHT_UNSPENT_OUTPUTS = "/addr/{address}/utxo";
public static final String INSIGHT_TRANSACTION = "/rawtx/{txId}";
public static final String INSIGHT_FEE = "/utils/estimatefee?nbBlocks=2,3,6";
public static final String INSIGHT_SEND = "/tx/send";
public static final String INSIGHT_ADDRESS = "addr/{address}";
public static final String INSIGHT_UNSPENT_OUTPUTS = "addr/{address}/utxo";
public static final String INSIGHT_TRANSACTION = "rawtx/{txId}";
public static final String INSIGHT_FEE = "utils/estimatefee?nbBlocks=2,3,6";
public static final String INSIGHT_SEND = "tx/send";
private int requestsCount = 0;
@ -38,7 +39,7 @@ public class ServerApiInsight {
public interface ResponseListener {
void onSuccess(String method, InsightResponse insightResponse);
void onSuccess(String method, List<InsightResponse> utxoList);
void onSuccess(String method, List<InsightUtxo> utxoList);
void onFail(String method, String message);
}
@ -49,7 +50,7 @@ public class ServerApiInsight {
public void requestData(String method, String wallet, String tx) {
requestsCount++;
String insightURL = "http://130.185.109.17:3001/insigth-api"; //TODO: make random selection
String insightURL = "https://insight.ducatus.io/insight-lite-api/"; //TODO: make random selection
this.lastNode = insightURL; //TODO: show node instead of URL
Retrofit retrofitInsight = new Retrofit.Builder()
@ -61,10 +62,10 @@ public class ServerApiInsight {
InsightApi insightApi = retrofitInsight.create(InsightApi.class);
if (method.equals(INSIGHT_UNSPENT_OUTPUTS)) {
Call<List<InsightResponse>> call = insightApi.insightUnspent(wallet);
call.enqueue(new Callback<List<InsightResponse>>() {
Call<List<InsightUtxo>> call = insightApi.insightUnspent(wallet);
call.enqueue(new Callback<List<InsightUtxo>>() {
@Override
public void onResponse(@NonNull Call<List<InsightResponse>> call, @NonNull Response<List<InsightResponse>> response) {
public void onResponse(@NonNull Call<List<InsightUtxo>> call, @NonNull Response<List<InsightUtxo>> response) {
requestsCount--;
if (response.code() == 200) {
@ -77,7 +78,7 @@ public class ServerApiInsight {
}
@Override
public void onFailure(@NonNull Call<List<InsightResponse>> call, @NonNull Throwable t) {
public void onFailure(@NonNull Call<List<InsightUtxo>> call, @NonNull Throwable t) {
requestsCount--;
responseListener.onFail(method, String.valueOf(t.getMessage()));
Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage());
@ -92,13 +93,9 @@ public class ServerApiInsight {
call = insightApi.insightAddress(wallet);
break;
case INSIGHT_TRANSACTION:
call = insightApi.insightTransaction(tx);
break;
case INSIGHT_FEE:
call = insightApi.insightFee();
break;
// case INSIGHT_FEE:
// call = insightApi.insightFee();
// break;
case INSIGHT_SEND:
call = insightApi.insightSend(new InsightBody(tx));

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

@ -12,27 +12,29 @@ data class InsightResponse(
@SerializedName("addrStr")
var addrStr: String = "",
// @SerializedName("2")
// var fee2: String = "",
//
// @SerializedName("3")
// var fee3: String = "",
//
// @SerializedName("6")
// var fee6: String = "",
@SerializedName("error")
var error: String = ""
)
data class InsightUtxo(
@SerializedName("txid")
var txid: String = "",
@SerializedName("satoshis")
var satoshis: Long? = null,
@SerializedName("height")
var height: Int? = null,
@SerializedName("vout")
var vout: Int? = null,
@SerializedName("2")
var fee2: String = "",
@SerializedName("3")
var fee3: String = "",
@SerializedName("6")
var fee6: String = "",
@SerializedName("rawtx")
var rawtx: String = "",
@SerializedName("error")
var error: String = ""
@SerializedName("scriptPubKey")
var scriptPubKey: String? = null
)

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,23 +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
/**
@ -48,7 +49,9 @@ 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
}
}
@ -84,8 +87,12 @@ 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)
DucatusEngine(context)
else
return null
} catch (e: Exception) {

View file

@ -566,7 +566,7 @@ public final class Transaction {
public static Script buildOutput(String address) throws BitcoinException {
//noinspection TryWithIdenticalCatches
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48) { //0 for BTC/BCH 1 address | 48 for LTC L address
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48 || addressWithCheckSumAndNetworkCode[0] == 49) { //0 for BTC/BCH 1 address | 48 for LTC L address | 49 for Ducatus
return buildOutputP2H(address);
}
@ -601,7 +601,7 @@ public final class Transaction {
//noinspection TryWithIdenticalCatches
try {
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48) {
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48 && addressWithCheckSumAndNetworkCode[0] != 49) {
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address);
}

View file

@ -24,6 +24,7 @@ import com.tangem.wallet.TangemContext;
import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONArray;
import org.json.JSONException;
@ -424,7 +425,15 @@ public class BtcCashEngine extends CoinEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -53,7 +53,7 @@ public class BinanceData extends CoinData {
}
public CoinEngine.Amount getBalance() {
return new CoinEngine.Amount(balance, "BNB");
return balance == null ? null : new CoinEngine.Amount(balance, "BNB");
}
public void setBalance(String balance) {

View file

@ -22,18 +22,19 @@ public class BtcData extends CoinData {
//for blockchain.info
private boolean hasUnconfirmed = false;
public String getUnspentInputsDescription() {
public Unspents getUnspentInputsDescription() {
try {
int gatheredUnspents = 0;
if (unspentTransactions == null) return "";
if (unspentTransactions == null) return null;
for (int i = 0; i < unspentTransactions.size(); i++) {
if (unspentTransactions.get(i).script != null && unspentTransactions.get(i).script.length() > 1)
gatheredUnspents++;
}
return unspentTransactions.size() + " unspents (" + gatheredUnspents + " received)";
return new Unspents(unspentTransactions.size(), gatheredUnspents);
} catch (Exception e) {
e.printStackTrace();
return "";
return null;
}
}

View file

@ -437,7 +437,15 @@ public class BtcEngine extends CoinEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -0,0 +1,3 @@
package com.tangem.wallet.btc
data class Unspents(val unspetns: Int, val gatheredUnspents: Int)

View file

@ -105,7 +105,7 @@ public class CardanoData extends CoinData {
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
return new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace");
return balance == null ? null : new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace");
}
public void setBalance(Long balance) {

View file

@ -7,6 +7,7 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiInsight;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
@ -24,11 +25,11 @@ import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
@ -48,7 +49,7 @@ public class DucatusEngine extends BtcEngine {
} else if (context.getCoinData() instanceof BtcData) {
coinData = (BtcData) context.getCoinData();
} else {
throw new Exception("Invalid type of Blockchain data for LtcEngine");
throw new Exception("Invalid type of Blockchain data for DucatusEngine");
}
}
@ -83,7 +84,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getBalanceCurrency() {
return "LTC";
return "DUC";
}
@Override
@ -118,7 +119,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getFeeCurrency() {
return "LTC";
return "DUC";
}
@Override
@ -168,16 +169,12 @@ public class DucatusEngine extends BtcEngine {
@Override
public Uri getWalletExplorerUri() {
return Uri.parse("https://live.blockcypher.com/ltc/address/" + ctx.getCoinData().getWallet());
return Uri.parse("https://insight.ducatus.io/insight/address/" + ctx.getCoinData().getWallet());
}
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
} else {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet());
}
return Uri.parse(ctx.getCoinData().getWallet());
}
@Override
@ -317,7 +314,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException {
byte netSelectionByte = (byte) 0x30;
byte netSelectionByte = (byte) 0x31;
byte hash1[] = Util.calculateSHA256(pkUncompressed);
byte hash2[] = Util.calculateRIPEMD160(hash1);
@ -381,23 +378,35 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
final ArrayList<UnspentOutputInfo> unspentOutputs;
ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
checkBlockchainDataExists();
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
// Build script for our address
List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
// // Build script for our address
// List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
// byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
//
// // Collect unspent
// unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
// Collect unspent
unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) {
unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null));
}
long fullAmount = 0;
for (int i = 0; i < unspentOutputs.size(); ++i) {
@ -413,8 +422,8 @@ public class DucatusEngine extends BtcEngine {
change = change - fees;
}
final long amountFinal=amount;
final long changeFinal=change;
final long amountFinal = amount;
final long changeFinal = change;
if (amount + fees > fullAmount) {
throw new CardProtocol.TangemException_WrongAmount(String.format("Balance (%d) < change (%d) + amount (%d)", fullAmount, change, amount));
@ -422,7 +431,7 @@ public class DucatusEngine extends BtcEngine {
final byte[][] txForSign = new byte[unspentOutputs.size()][];
final byte[][] bodyDoubleHash = new byte[unspentOutputs.size()][];
final byte[][] bodyHash= new byte[unspentOutputs.size()][];
final byte[][] bodyHash = new byte[unspentOutputs.size()][];
for (int i = 0; i < unspentOutputs.size(); ++i) {
txForSign[i] = BTCUtils.buildTXForSign(myAddress, targetAddress, myAddress, unspentOutputs, i, amount, change);
@ -434,13 +443,14 @@ public class DucatusEngine extends BtcEngine {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod==TangemCard.SigningMethod.Sign_Hash || signingMethod==TangemCard.SigningMethod.Sign_Raw;
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw;
}
@Override
public byte[][] getHashesToSign() throws Exception {
byte[][] dataForSign=new byte[unspentOutputs.size()][];
if (txForSign.length > 10) throw new Exception("To much hashes in one transaction!");
byte[][] dataForSign = new byte[unspentOutputs.size()][];
if (txForSign.length > 10)
throw new Exception("To much hashes in one transaction!");
for (int i = 0; i < unspentOutputs.size(); ++i) {
dataForSign[i] = bodyDoubleHash[i];
}
@ -479,7 +489,7 @@ public class DucatusEngine extends BtcEngine {
unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDer(r, s, pbKey);
}
byte[] txForSend=BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal);
byte[] txForSend = BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal);
notifyOnNeedSendTransaction(txForSend);
return txForSend;
}
@ -493,39 +503,20 @@ public class DucatusEngine extends BtcEngine {
ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
switch (method) {
case ServerApiInsight.INSIGHT_ADDRESS: {
try {
String walletAddress = insightResponse.getAddrStr();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(insightResponse.getBalanceSat());
coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat());
coinData.setValidationNodeDescription(ServerApiInsight.lastNode);
}
catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception");
}
}
break;
case ServerApiInsight.INSIGHT_TRANSACTION: {
try {
String raw = insightResponse.getRawtx();
String txHash = new String(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(raw)))); //TODO: check
for (BtcData.UnspentTransaction tx : coinData.getUnspentTransactions()) {
if (tx.txID.equals(txHash))
tx.script = raw;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
String walletAddress = insightResponse.getAddrStr();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
break;
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(insightResponse.getBalanceSat());
coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat());
coinData.setValidationNodeDescription(ServerApiInsight.lastNode);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception");
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
@ -535,30 +526,27 @@ public class DucatusEngine extends BtcEngine {
}
}
public void onSuccess(String method, List<InsightResponse> utxoList) {
// case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method
try {
coinData.getUnspentTransactions().clear();
for (InsightResponse utxo : utxoList) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTxid();
trUnspent.amount = utxo.getSatoshis();
trUnspent.outputN = utxo.getHeight();
coinData.getUnspentTransactions().add(trUnspent);
}
for (InsightResponse utxo : utxoList) {
//if (height != -1) { TODO: check
if (blockchainRequestsCallbacks.allowAdvance()) {
serverApiInsight.requestData(ServerApiInsight.INSIGHT_TRANSACTION, "", utxo.getTxid());
} else {
ctx.setError("Terminated by user");
}
}
} catch (Exception e) {
e.printStackTrace();
public void onSuccess(String method, List<InsightUtxo> utxoList) {
// case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method
try {
coinData.getUnspentTransactions().clear();
for (InsightUtxo utxo : utxoList) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTxid();
trUnspent.amount = utxo.getSatoshis();
trUnspent.outputN = utxo.getVout();
trUnspent.script = utxo.getScriptPubKey();
coinData.getUnspentTransactions().add(trUnspent);
}
} catch (Exception e) {
e.printStackTrace();
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
@ -582,65 +570,71 @@ public class DucatusEngine extends BtcEngine {
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
coinData.minFee=null;
coinData.maxFee=null;
coinData.normalFee=null;
final ServerApiInsight serverApiInsight = new ServerApiInsight();
final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
if ( method.equals(ServerApiInsight.INSIGHT_FEE)) {
try {
BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB
BigDecimal normalFee = new BigDecimal(insightResponse.getFee3());
BigDecimal maxFee = new BigDecimal(insightResponse.getFee6());
if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) {
serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "","");
}
minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size
normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network
// if (fee.compareTo(relayFee) < 0) {
// fee = relayFee;
// coinData.minFee=null;
// coinData.maxFee=null;
// coinData.normalFee=null;
//
// final ServerApiInsight serverApiInsight = new ServerApiInsight();
//
// final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
// @Override
// public void onSuccess(String method, InsightResponse insightResponse) {
// if ( method.equals(ServerApiInsight.INSIGHT_FEE)) {
// try {
// BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB
// BigDecimal normalFee = new BigDecimal(insightResponse.getFee3());
// BigDecimal maxFee = new BigDecimal(insightResponse.getFee6());
//
// if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) {
// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "","");
// }
minFee = minFee.setScale(8, RoundingMode.DOWN);
normalFee = normalFee.setScale(8, RoundingMode.DOWN);
maxFee = maxFee.setScale(8, RoundingMode.DOWN);
//
// minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size
// normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
// maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
//
//// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network
//// if (fee.compareTo(relayFee) < 0) {
//// fee = relayFee;
//// }
// minFee = minFee.setScale(8, RoundingMode.DOWN);
// normalFee = normalFee.setScale(8, RoundingMode.DOWN);
// maxFee = maxFee.setScale(8, RoundingMode.DOWN);
//
// coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency());
// coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency());
// coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency());
//
// blockchainRequestsCallbacks.onComplete(true);
//
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
// }
//
// @Override
// public void onSuccess (String method, List<InsightResponse> utxoList) {
// Log.e(TAG, "Wrong response body, InsightResponse expected");
// }
//
// @Override
// public void onFail(String method, String message) {
// if (!serverApiInsight.isRequestsSequenceCompleted()) {
// ctx.setError(message);
// blockchainRequestsCallbacks.onComplete(false);
// }
// }
// };
// serverApiInsight.setResponseListener(responseListener);
//
// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", ""); TODO: fee api returns -1 now
coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency());
coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency());
coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency());
coinData.minFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000089)), ctx.getBlockchain().getCurrency()); //fee for byte from Ducatus wallet for android
coinData.normalFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000144)), ctx.getBlockchain().getCurrency());
coinData.maxFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000350)), ctx.getBlockchain().getCurrency());
blockchainRequestsCallbacks.onComplete(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onSuccess (String method, List<InsightResponse> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) {
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", "");
blockchainRequestsCallbacks.onComplete(true);
}
@Override
@ -657,7 +651,7 @@ public class DucatusEngine extends BtcEngine {
if (resultString.isEmpty()) {
ctx.setError("No response from node");
blockchainRequestsCallbacks.onComplete(false);
}else { // TODO: Make check for a valid send response
} else { // TODO: Make check for a valid send response
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}
@ -676,7 +670,7 @@ public class DucatusEngine extends BtcEngine {
}
@Override
public void onSuccess (String method, List<InsightResponse> utxoList) {
public void onSuccess(String method, List<InsightUtxo> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}

View file

@ -31,6 +31,7 @@ import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONException;
@ -389,7 +390,15 @@ public class LtcEngine extends BtcEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

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

@ -198,7 +198,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:fontFamily="@font/maax"
android:text="The card is linked"
android:text="@string/details_linked_card_title"
android:textSize="@dimen/text_size_1_small" />
<TextView

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

@ -9,9 +9,9 @@
<string name="general_continue">Continuer</string>
<string name="error_empty_pin">Le code PIN est vide</string>
<string name="general_blockchain">Blockchain</string>
<string name="general_notification_scan_again">Réessayez de numériser</string>
<string name="general_notification_scan_again">Réessayez de scanner la carte</string>
<string name="general_error_cannot_erase_wallet_with_non_zero_balance">Impossible d\'effacer le porte-monnaie avec un solde différent de zéro</string>
<string name="general_send_transaction">Envoyer le paiement</string>
<string name="general_send_transaction">Transférer les fonds</string>
<string name="general_from_card">Depuis la carte</string>
<string name="general_on_card">sur la carte</string>
<string name="general_balance">avec solde</string>
@ -49,9 +49,9 @@
<string name="dialog_the_nfc_adapter_length_apdu">Oups .. Il semble que votre smartphone ne supporte pas de tels paquets NFC.</string>
<string name="dialog_the_nfc_adapter_length_apdu_advice">Essayez d\'envoyer une quantité inférieure ou utilisez un smartphone avec prise en charge complète de NFC.</string>
<string name="dialog_title_money_is_at_risk">Votre argent est en jeu!</string>
<string name="dialog_this_card_has_enforced_security_delay">Cette carte a imposé un délai de sécurité</string>
<string name="dialog_hold_card">Veuillez tenir la carte fermement \ n jusqu\'à ce que l\'opération soit terminée…</string>
<string name="dialog_you_may_be_required_to_repeat">Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone. \ n Ceci est fait pour sécuriser vos fonds.</string>
<string name="dialog_this_card_has_enforced_security_delay">Cette carte impose un délai de sécurité pour confirmer le transfert des fonds</string>
<string name="dialog_hold_card">Veuillez maintenir la carte dans cette position jusqu\'à ce que l\'opération soit terminée…</string>
<string name="dialog_you_may_be_required_to_repeat">Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone.\nCeci est fait pour sécuriser vos fonds.</string>
<!-- menu main -->
<string name="main_menu_manage_pin_1">Gérer le code PIN de l\'utilisateur…</string>
@ -74,12 +74,14 @@
<!-- Main -->
<string name="main_screen_tap_card">Mettez en contact la carte avec votre smartphone</string>
<string name="main_screen_scan_card">Scanner une carte avec votre \n %1$s \n comme indiqué ci-dessus</string>
<string name="main_screen_scan_card">Scanner votre carte avec votre %1$s comme indiqué ci-dessus</string>
<string name="main_screen_phone">phone</string>
<string name="main_screen_erased_wallet">Le porte-monnaie a été effacé</string>
<string name="main_screen_not_personalized">Non personnalisé</string>
<string name="main_screen_new_version_toast">Il existe une nouvelle version de l\'application: %1$s</string>
<string name="main_screen_btn_update">Mettre à jour</string>
<string name="main_screen_visit_store">Vous navez pas de carte?\nVisitez notre boutique sur %1$s</string>
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">Aucun porte-monnaie compatible installé</string>
@ -146,10 +148,10 @@
<string name="balance_validator_second_line_authenticity_not_verified">L\'authenticité ne peut pas être vérifiée. Balayez vers le bas pour actualiser.</string>
    
<!-- CreateNewWallet, Purge, SignTransaction -->
<string name="now_touch_the_card_with_id"> Maintenant, touchez la carte avec l\'ID </string>
<string name="now_touch_the_card_with_id">Maintenir la carte avec l\ID</string>
<string name="to_create_the_wallet">pour créer le porte-monnaie </string>
<string name="to_erase_the_wallet">pour effacer le porte-monnaie</string>
<string name="to_sign_the_transaction">pour signer le paiement</string>
<string name="to_sign_the_transaction">pour confirmer le transfert des fonds</string>
<string name="to_change_pin_codes">pour changer les codes PIN / PIN2</string>
<string name="nfc_purge_warning">En mettant la carte en contact avec votre smartphone, vous retirerez définitivement le porte-monnaie de la blockchain. Assurez-vous qu\'il n\'y aura pas d\'autres transactions entrantes</string>
<string name="nfc_error_cannot_erase_wallet">Impossible d\'effacer le porte-monnaie. Veillez à entrer le code PIN2 correct!</string>
@ -178,7 +180,7 @@
<!-- PrepareTransaction -->
<string name="prepare_transaction_hint_enter_address">entrez l\'adresse du porte-monnaie</string>
<string name="prepare_transaction_hint_enter_amount">entrer le montant</string>
<string name="prepare_transaction_btn_verify">Vérifiez</string>
<string name="prepare_transaction_btn_verify">Vérifier</string>
<string name="prepare_transaction_error_not_enough_funds">Pas assez de fonds</string>
<string name="prepare_transaction_error_unknown_amount_format">Format de montant inconnu</string>
<string name="prepare_transaction_error_incorrect_destination">Adresse du porte-monnaie de destination incorrecte</string>
@ -223,12 +225,12 @@
<string name="details_category_manufacturer">Fabricant</string>
<string name="details_category_wallet">Porte-monnaie</string>
<string name="details_card_identity">Identité de la carte</string>
<string name="details_attested">Attested</string>
<string name="details_attested">Attestée</string>
<string name="details_not_confirmed">Non confirmé</string>
<string name="details_reusable">Réutilisable</string>
<string name="details_none">None</string>
<string name="details_last_one">Le dernier!</string>
<string name="details_unlimited">Unlimited</string>
<string name="details_unlimited">illimitées</string>
<string name="details_one_off_card">Carte ponctuelle</string>
<string name="details_firmware">Micrologiciel</string>
<string name="details_registration_date">Date d\'enregistrement</string>
@ -238,7 +240,7 @@
<string name="details_category_issuer">Émetteur</string>
<string name="details_private_key">Clé privée</string>
<string name="details_validation_node">Noeud de validation</string>
<string name="details_unspents">Unspents</string>
<string name="details_unspents">Transferts</string>
<string name="details_protected_by_default_pin_1">Cette carte est protégée par le code PIN1 par défaut</string>
<string name="details_protected_by_user_pin_1">Cette carte est protégée par le code PIN1 de lutilisateur</string>
<string name="details_protected_by_default_pin_2">Cette carte est protégée par le code PIN2 par défaut</string>
@ -255,8 +257,9 @@
<string name="details_blockable">Bloquable\n</string>
<string name="details_atomic_commmands">Mode de commande atomique\n</string>
<string name="details_linking_card_supported">La liaison au terminal est prise en charge\n</string>
<string name="details_linked_card_title">La carte est liée</string>
<string name="details_linked_card_title">La carte est associée</string>
<string name="details_linked_card_to_phone">à ce téléphone</string>
<string name="details_unspents_number">%1$d envoyé(s), %2$d reçu(s)</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Enregistrer</string>

View file

@ -82,7 +82,7 @@
<string name="main_screen_new_version_toast">There is a new application version: %1$s</string>
<string name="main_screen_btn_update">Update</string>
<string name="main_screen_visit_store">Don\'t have a card?\n Visit our store at %1$s</string>
<string name="main_screen_store_address">tangemcards.com</string>
<string name="main_screen_store_address" translatable="false">tangemcards.com</string>
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">No compatible wallets installed</string>
@ -262,6 +262,13 @@
<string name="details_linking_card_supported">Linking to the terminal is supported\n</string>
<string name="details_linked_card_title">The card is linked</string>
<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>

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