From 857913f36a955dfe34bd4a64234b0c92044fd870 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Nov 2019 18:06:23 +0300 Subject: [PATCH 01/26] Updated on 2026-08-14 --- .../main/java/com/tangem/data/Blockchain.java | 3 +- .../com/tangem/data/network/InsightApi.java | 3 +- .../tangem/data/network/ServerApiInsight.java | 33 ++- .../data/network/model/InsightResponse.kt | 34 +-- .../com/tangem/wallet/CoinEngineFactory.kt | 4 + .../java/com/tangem/wallet/Transaction.java | 4 +- .../tangem/wallet/ducatus/DucatusEngine.java | 255 +++++++++--------- 7 files changed, 163 insertions(+), 173 deletions(-) diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 782234d4ed..92db5c5d43 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -26,7 +26,8 @@ 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"); + 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; diff --git a/app/src/main/java/com/tangem/data/network/InsightApi.java b/app/src/main/java/com/tangem/data/network/InsightApi.java index 63eb570f1c..4095326dc5 100644 --- a/app/src/main/java/com/tangem/data/network/InsightApi.java +++ b/app/src/main/java/com/tangem/data/network/InsightApi.java @@ -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 insightAddress(@Path("address") String address); @GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS) - Call> insightUnspent(@Path("address") String address); + Call> insightUnspent(@Path("address") String address); @GET(ServerApiInsight.INSIGHT_TRANSACTION) Call insightTransaction(@Path("txId") String txId); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java index e3eae3384a..5a768471e6 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java @@ -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 utxoList); + void onSuccess(String method, List 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> call = insightApi.insightUnspent(wallet); - call.enqueue(new Callback>() { + Call> call = insightApi.insightUnspent(wallet); + call.enqueue(new Callback>() { @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { + public void onResponse(@NonNull Call> call, @NonNull Response> response) { requestsCount--; if (response.code() == 200) { @@ -77,7 +78,7 @@ public class ServerApiInsight { } @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + public void onFailure(@NonNull Call> 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)); diff --git a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt index 4910b36695..a16c535649 100644 --- a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt +++ b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 10aaf161f1..7a39a24de2 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -11,6 +11,7 @@ import com.tangem.wallet.eos.EosEngine import com.tangem.wallet.binance.BinanceEngine import com.tangem.wallet.cardano.CardanoData import com.tangem.wallet.cardano.CardanoEngine +import com.tangem.wallet.ducatus.DucatusEngine import com.tangem.wallet.ltc.LtcEngine import com.tangem.wallet.matic.MaticTokenEngine import com.tangem.wallet.nftToken.NftTokenEngine @@ -49,6 +50,7 @@ object CoinEngineFactory { Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine() Blockchain.StellarAsset -> XlmAssetEngine() Blockchain.Eos -> EosEngine() + Blockchain.Ducatus -> DucatusEngine() else -> null } } @@ -86,6 +88,8 @@ object CoinEngineFactory { XlmAssetEngine(context) else if (Blockchain.Eos == context.blockchain) EosEngine(context) + else if (Blockchain.Ducatus == context.blockchain) + DucatusEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/Transaction.java b/app/src/main/java/com/tangem/wallet/Transaction.java index 0d4e54fd51..07dc9b42e5 100644 --- a/app/src/main/java/com/tangem/wallet/Transaction.java +++ b/app/src/main/java/com/tangem/wallet/Transaction.java @@ -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); } diff --git a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java index 4a29f13377..697f671354 100644 --- a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java +++ b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java @@ -6,6 +6,7 @@ import android.util.Log; 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; @@ -27,7 +28,6 @@ import com.tangem.wallet.btc.BtcEngine; 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; @@ -47,7 +47,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"); } } @@ -82,7 +82,7 @@ public class DucatusEngine extends BtcEngine { @Override public String getBalanceCurrency() { - return "LTC"; + return "DUC"; } @Override @@ -117,7 +117,7 @@ public class DucatusEngine extends BtcEngine { @Override public String getFeeCurrency() { - return "LTC"; + return "DUC"; } @Override @@ -167,16 +167,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 @@ -316,7 +312,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); @@ -385,18 +381,22 @@ public class DucatusEngine extends BtcEngine { @Override public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { - final ArrayList unspentOutputs; + ArrayList unspentOutputs = new ArrayList<>(); checkBlockchainDataExists(); String myAddress = ctx.getCoinData().getWallet(); byte[] pbKey = ctx.getCard().getWalletPublicKey(); - // Build script for our address - List rawTxList = coinData.getUnspentTransactions(); - byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes; +// // Build script for our address +// List 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) { @@ -412,8 +412,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)); @@ -421,7 +421,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); @@ -433,13 +433,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]; } @@ -478,7 +479,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; } @@ -492,39 +493,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()) { @@ -534,30 +516,27 @@ public class DucatusEngine extends BtcEngine { } } - public void onSuccess(String method, List 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 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 @@ -581,65 +560,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 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 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 @@ -656,7 +641,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); } @@ -675,7 +660,7 @@ public class DucatusEngine extends BtcEngine { } @Override - public void onSuccess (String method, List utxoList) { + public void onSuccess(String method, List utxoList) { Log.e(TAG, "Wrong response body, InsightResponse expected"); } From 2903152947f1cf8bcb8088415aad3883e252a7bb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Dec 2019 14:02:29 +0300 Subject: [PATCH 02/26] Updated on 2026-08-14 --- app/src/main/java/com/tangem/wallet/binance/BinanceData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java index de90f5c65a..bfe3b630cf 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java @@ -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) { From 04ca7466c1558ecef83107b577e15dad59836c9a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Dec 2019 16:19:15 +0300 Subject: [PATCH 03/26] Updated on 2026-08-14 --- app/src/main/java/com/tangem/wallet/cardano/CardanoData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java b/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java index e3ba66dc73..44e69eed30 100644 --- a/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java +++ b/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java @@ -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) { From 9e067e324d6ff8cdc09b418d6b5200e2e2b07911 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Dec 2019 12:39:02 +0300 Subject: [PATCH 04/26] Updated on 2026-08-14 --- .../com/tangem/wallet/bch/BtcCashEngine.java | 11 ++++++- .../java/com/tangem/wallet/btc/BtcData.java | 9 +++--- .../java/com/tangem/wallet/btc/BtcEngine.java | 10 ++++++- .../java/com/tangem/wallet/btc/Unspents.kt | 3 ++ .../tangem/wallet/ducatus/DucatusEngine.java | 11 ++++++- .../java/com/tangem/wallet/ltc/LtcEngine.java | 11 ++++++- app/src/main/res/layout/fr_verify_card.xml | 2 +- app/src/main/res/values-fr/strings.xml | 29 ++++++++++--------- app/src/main/res/values/strings.xml | 3 +- 9 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/tangem/wallet/btc/Unspents.kt diff --git a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java index 03a2d3c2ce..2acdefb4bc 100644 --- a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcData.java b/app/src/main/java/com/tangem/wallet/btc/BtcData.java index 71c1d7c8f1..0d0eca1d85 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcData.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcData.java @@ -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; } } diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java index 021ce73e38..fd49ed7ec4 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/btc/Unspents.kt b/app/src/main/java/com/tangem/wallet/btc/Unspents.kt new file mode 100644 index 0000000000..12029683e3 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/btc/Unspents.kt @@ -0,0 +1,3 @@ +package com.tangem.wallet.btc + +data class Unspents(val unspetns: Int, val gatheredUnspents: Int) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java index c8c3ed8122..17e8b9789c 100644 --- a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java +++ b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java @@ -25,6 +25,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 java.io.ByteArrayOutputStream; import java.math.BigDecimal; @@ -377,7 +378,15 @@ 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 diff --git a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java index a2a6abc339..7402e658d9 100644 --- a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java @@ -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 diff --git a/app/src/main/res/layout/fr_verify_card.xml b/app/src/main/res/layout/fr_verify_card.xml index 09cbf321ca..189186e5f5 100644 --- a/app/src/main/res/layout/fr_verify_card.xml +++ b/app/src/main/res/layout/fr_verify_card.xml @@ -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" /> Continuer Le code PIN est vide Blockchain - Réessayez de numériser + Réessayez de scanner la carte Impossible d\'effacer le porte-monnaie avec un solde différent de zéro - Envoyer le paiement + Transférer les fonds Depuis la carte sur la carte avec solde @@ -49,9 +49,9 @@ Oups .. Il semble que votre smartphone ne supporte pas de tels paquets NFC. Essayez d\'envoyer une quantité inférieure ou utilisez un smartphone avec prise en charge complète de NFC. Votre argent est en jeu! - Cette carte a imposé un délai de sécurité - Veuillez tenir la carte fermement \ n jusqu\'à ce que l\'opération soit terminée… - 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. + Cette carte impose un délai de sécurité pour confirmer le transfert des fonds + Veuillez maintenir la carte dans cette position jusqu\'à ce que l\'opération soit terminée… + 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. Gérer le code PIN de l\'utilisateur… @@ -74,12 +74,14 @@ Mettez en contact la carte avec votre smartphone - Scanner une carte avec votre \n %1$s \n comme indiqué ci-dessus + Scanner votre carte avec votre %1$s comme indiqué ci-dessus phone Le porte-monnaie a été effacé Non personnalisé Il existe une nouvelle version de l\'application: %1$s Mettre à jour + Vous n’avez pas de carte?\nVisitez notre boutique sur %1$s + Aucun porte-monnaie compatible installé @@ -146,10 +148,10 @@ L\'authenticité ne peut pas être vérifiée. Balayez vers le bas pour actualiser.      - Maintenant, touchez la carte avec l\'ID + Maintenir la carte avec l\’ID pour créer le porte-monnaie pour effacer le porte-monnaie - pour signer le paiement + pour confirmer le transfert des fonds pour changer les codes PIN / PIN2 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 Impossible d\'effacer le porte-monnaie. Veillez à entrer le code PIN2 correct! @@ -178,7 +180,7 @@ entrez l\'adresse du porte-monnaie entrer le montant - Vérifiez + Vérifier Pas assez de fonds Format de montant inconnu Adresse du porte-monnaie de destination incorrecte @@ -223,12 +225,12 @@ Fabricant Porte-monnaie Identité de la carte - Attested + Attestée Non confirmé Réutilisable None Le dernier! - Unlimited + illimitées Carte ponctuelle Micrologiciel Date d\'enregistrement @@ -238,7 +240,7 @@ Émetteur Clé privée Noeud de validation - Unspents + Transferts Cette carte est protégée par le code PIN1 par défaut Cette carte est protégée par le code PIN1 de l’utilisateur Cette carte est protégée par le code PIN2 par défaut @@ -255,8 +257,9 @@ Bloquable\n Mode de commande atomique\n La liaison au terminal est prise en charge\n - La carte est liée + La carte est associée à ce téléphone + %1$d envoyé(s), %2$d reçu(s) Enregistrer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebf79111a2..e5cfe74312 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ There is a new application version: %1$s Update Don\'t have a card?\n Visit our store at %1$s - tangemcards.com + tangemcards.com No compatible wallets installed @@ -262,6 +262,7 @@ Linking to the terminal is supported\n The card is linked To this phone + %1$d unspents (%2$d received) Save From d5c411471b91d6e9a896183e971153cf88a0f321 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Jan 2020 23:14:32 +0300 Subject: [PATCH 05/26] Updated on 2026-08-14 --- .../main/java/com/tangem/data/Blockchain.java | 1 + .../tangem/data/network/ServerApiStellar.java | 7 +- .../com/tangem/ui/fragment/MainFragment.kt | 91 ++++ .../ui/fragment/additional/TagFragment.kt | 214 ++++++++ .../com/tangem/wallet/CoinEngineFactory.kt | 15 +- .../com/tangem/wallet/xlmtag/XlmTagData.kt | 133 +++++ .../com/tangem/wallet/xlmtag/XlmTagEngine.kt | 471 ++++++++++++++++++ app/src/main/res/layout/fragment_tag.xml | 9 + app/src/main/res/navigation/main_graph.xml | 8 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/nfc_tech_filter.xml | 1 + .../tangem/tangem_card/data/TangemCard.java | 9 + .../tangem_sdk/android/reader/NfcManager.kt | 3 +- .../tangem_sdk/data/TangemCardExtensions.kt | 13 +- 14 files changed, 966 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt create mode 100644 app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt create mode 100644 app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt create mode 100644 app/src/main/res/layout/fragment_tag.xml diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 92db5c5d43..234548d273 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -26,6 +26,7 @@ public enum Blockchain { Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"), StellarAsset("Asset", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"), + StellarTag("XLM-Tag", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"), Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java index f364820eb1..77427e3fc4 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java @@ -33,7 +33,7 @@ import io.reactivex.schedulers.Schedulers; public class ServerApiStellar { public ServerApiStellar(Blockchain blockchain) { - if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset) { + if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) { currentURL = ServerURL.API_STELLAR; } else { currentURL = ServerURL.API_STELLAR_TESTNET; @@ -173,10 +173,11 @@ public class ServerApiStellar { stellarRequest.setError(null); try { Server server; - if (ctx.getBlockchain() == Blockchain.Stellar || ctx.getBlockchain() == Blockchain.StellarAsset) { + Blockchain blockchain = ctx.getBlockchain(); + if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) { Network.usePublicNetwork(); server = new Server(currentURL); - } else if (ctx.getBlockchain() == Blockchain.StellarTestNet) { + } else if (blockchain == Blockchain.StellarTestNet) { Network.useTestNetwork(); server = new Server(currentURL); } else { diff --git a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index a8b9b4ac02..cecb0ddeee 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -7,10 +7,13 @@ import android.net.Uri import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.IsoDep +import android.nfc.tech.Ndef +import android.nfc.tech.NfcV import android.os.Bundle import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan +import android.util.Log import android.view.* import android.widget.PopupMenu import android.widget.TextView @@ -20,9 +23,13 @@ import androidx.core.os.bundleOf import androidx.lifecycle.ViewModelProviders import com.tangem.App import com.tangem.Constant +import com.tangem.data.Blockchain import com.tangem.data.Logger import com.tangem.tangem_card.data.TangemCard import com.tangem.tangem_card.reader.CardProtocol +import com.tangem.tangem_card.reader.TLV +import com.tangem.tangem_card.reader.TLVException +import com.tangem.tangem_card.reader.TLVList import com.tangem.tangem_card.tasks.CustomReadCardTask import com.tangem.tangem_card.tasks.ReadCardInfoTask import com.tangem.tangem_sdk.android.data.PINStorage @@ -45,10 +52,16 @@ import com.tangem.wallet.BuildConfig import com.tangem.wallet.CoinEngineFactory import com.tangem.wallet.R import com.tangem.wallet.TangemContext +import com.tangem.wallet.xlmtag.XlmTagEngine import kotlinx.android.synthetic.main.fragment_main.* import kotlinx.android.synthetic.main.layout_touch_card.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import java.io.File import java.util.* +import kotlin.coroutines.CoroutineContext class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback, CardProtocol.Notifications, androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener, @@ -68,6 +81,11 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader private var zipFile: File? = null private var unknownBlockchain = false + private val parentJob = Job() + private val coroutineContext: CoroutineContext + get() = parentJob + Dispatchers.IO + private val scope = CoroutineScope(coroutineContext) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { setHasOptionsMenu(true) return super.onCreateView(inflater, container, savedInstanceState) @@ -152,6 +170,8 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader return } + parseNfcvTag(tag) + try { // get IsoDep handle and run cardReader thread val isoDep = IsoDep.get(tag) @@ -175,6 +195,77 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } } + private fun parseNfcvTag(tag: Tag) { + if (NfcV.get(tag) != null) { + if (Ndef.get(tag) != null) { + try { + onNdefDiscovered(Ndef.get(tag), tag.id) + } catch (e: Exception) { + e.printStackTrace() + (activity as MainActivity).nfcManager.notifyReadResult(false) + } + return + } else { + (activity as MainActivity).nfcManager.notifyReadResult(false) + return + } + } + } + + private fun onNdefDiscovered(ndef: Ndef, uid: ByteArray) { + scope.launch { + + + try { + ndef.connect() + val records = ndef.ndefMessage.records + for (record in records) { + if (record.toUri() != null) { + when (record.toUri().toString()) { + "vnd.android.nfc://ext/tangem.com:wallet" -> { + //mConsole.write(Util.bytesToHex(record.getPayload()), MessageAdapter.MSG_OKAY, "", null, false); + val payload = record.payload + Log.v(TAG, "tangem.com:wallet[${payload.size} bytes]:") + try { + val tlvNDEF: TLVList = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size)) + val cardDataTlv = TLVList.fromBytes((tlvNDEF.getTLV(TLV.Tag.TAG_CardData)).Value) + Log.v(TAG, "\n" + tlvNDEF.getParsedTLVs("")) + val card = TangemCard(uid.toString()) + card.batch = cardDataTlv.getTLV(TLV.Tag.TAG_Batch).asHexString + card.setIssuer(cardDataTlv.getTLV(TLV.Tag.TAG_Issuer_ID).Value.toString(), null) + card.blockchainID = Blockchain.StellarTag.id + card.walletPublicKey = tlvNDEF.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value + card.status = TangemCard.Status.Loaded + card.tagSignature = tlvNDEF.getTLV(TLV.Tag.TAG_Signature).Value + + val ctx = TangemContext(card) + val engineCoin = XlmTagEngine(ctx) + engineCoin.defineWallet() + launch(Dispatchers.Main) { + + val bundle = Bundle() + bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag) + ctx.saveToBundle(bundle) + navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY, + R.id.action_main_to_tagFragment, bundle) + } + } catch (e: TLVException) { + e.printStackTrace() + Log.v(TAG, e.message) + } + } + else -> Log.v(TAG, record.toUri().toString()) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + (activity as MainActivity).nfcManager.notifyReadResult(false) + } + + } + } + override fun onReadStart(cardProtocol: CardProtocol) { rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE } } diff --git a/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt new file mode 100644 index 0000000000..dd2d27594c --- /dev/null +++ b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt @@ -0,0 +1,214 @@ +package com.tangem.ui.fragment.additional + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.Html +import android.view.View +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.tangem.data.network.ServerApiCommon +import com.tangem.server_android.ServerApiTangem +import com.tangem.ui.fragment.BaseFragment +import com.tangem.ui.fragment.wallet.LoadedWalletFragment +import com.tangem.ui.fragment.wallet.LoadedWalletViewModel +import com.tangem.ui.navigation.NavigationResultListener +import com.tangem.util.LOG +import com.tangem.util.UtilHelper +import com.tangem.wallet.* +import com.tangem.wallet.xlmtag.XlmTagEngine +import kotlinx.android.synthetic.main.fr_loaded_wallet.* +import kotlinx.android.synthetic.main.layout_btn_details.* +import kotlinx.android.synthetic.main.layout_tangem_card.* + + +class TagFragment : BaseFragment(), NavigationResultListener { + override val layoutId = R.layout.fragment_tag + private lateinit var viewModel: LoadedWalletViewModel + private lateinit var ctx: TangemContext + private var serverApiCommon: ServerApiCommon = ServerApiCommon() + private var serverApiTangem: ServerApiTangem = ServerApiTangem() + + private var requestCounter: Int = 0 + set(value) { + field = value + LOG.i(LoadedWalletFragment.TAG, "requestCounter, set $field") + if (field <= 0) { + LOG.e(LoadedWalletFragment.TAG, "+++++++++++ FINISH REFRESH") + if (srl != null && srl.isRefreshing) + srl.isRefreshing = false + } else if (srl != null && !srl.isRefreshing) + srl.isRefreshing = true + } + + override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ctx = TangemContext.loadFromBundle(context, arguments) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val engine = XlmTagEngine(ctx) + + + btnLoad.visibility = View.GONE + btnDetails.visibility = View.GONE + btnExtract.text = getString(R.string.tag_claim) + btnExtract.isEnabled = false //TODO: enable when we implement extraction + btnExtract.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.btn_dark) + + ivTangemCard.setImageResource(R.drawable.card_default_nft) + + tvBalance.setSingleLine(!engine.needMultipleLinesForBalance()) + tvWallet.text = ctx.coinData.wallet + tvWallet.setOnClickListener { shareWallet() } + btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine.walletExplorerUri)) } + btnCopy.setOnClickListener { shareWallet() } + btnNewScan.setOnClickListener { navigateUp() } + srl?.setOnRefreshListener { refresh(true) } + + requestBalanceAndUnspentTransactions() +// update() + } + + + private fun shareWallet() { + val txtShare = ctx.coinData.wallet + val clipboard = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare) + Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show() + } + + + private fun update() { + ctx.coinData.setIsBalanceEqual(true) + + if (srl.isRefreshing) { + tvBalanceLine1.setTextColor(resources.getColor(R.color.primary)) + tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain) + tvBalanceLine2.text = "" + tvBalance.text = "" + tvBalanceEquivalent.text = "" + } else { + val validator = BalanceValidator() + validator.check(ctx, false) + context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) } + tvBalanceLine1?.text = getString(validator.firstLine) + tvBalanceLine2?.text = getString(validator.getSecondLine(false)) + } + + val engine = CoinEngineFactory.create(ctx) + when { + engine!!.hasBalanceInfo() -> { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(engine.balanceHTML, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(engine.balanceHTML) + tvBalance.text = html + tvBalanceEquivalent.text = engine.balanceEquivalent + } + + ctx.card?.offlineBalance != null -> { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(engine.offlineBalanceHTML, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(engine.offlineBalanceHTML) + tvBalance.text = html + } + + else -> tvBalance.text = "" + } + + if (ctx.card!!.tokenSymbol.length > 1) { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(ctx.blockchainName) + tvBlockchain.text = html + } else + tvBlockchain.text = ctx.blockchainName + + } + + private fun requestBalanceAndUnspentTransactions() { + if (UtilHelper.isOnline(context as Activity)) { + val coinEngine = CoinEngineFactory.create(ctx) + requestCounter++ + coinEngine!!.requestBalanceAndUnspentTransactions( + object : CoinEngine.BlockchainRequestsCallbacks { + + override fun onComplete(success: Boolean) { + LOG.i(TAG, "requestBalanceAndUnspentTransactions onComplete: $success, request counter $requestCounter") + if (activity == null) return + requestCounter-- + if (!success) { + LOG.e(TAG, "requestBalanceAndUnspentTransactions ctx.error: " + ctx.error) + } + update() + } + + override fun onProgress() { + if (activity == null) return + LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress") +// update() + } + + override fun allowAdvance(): Boolean { + return try { + context?.let { UtilHelper.isOnline(it) }!! + } catch (e: KotlinNullPointerException) { + e.printStackTrace() + false + } + } + } + ) + } else { + ctx.error = getString(R.string.general_error_no_connection) + update() + } + } + + private fun refresh(clearData: Boolean = true) { + if (ctx.card == null) return + + // clear all card data and request again + ctx.coinData.clearInfo() + + if (clearData) { + ctx.error = null + ctx.message = null + } + + LOG.w(TAG, "============= START REFRESH") + requestCounter = 0 + srl?.isRefreshing = true + + update() + + ctx.coinData.setIsBalanceEqual(true) + + + requestBalanceAndUnspentTransactions() + + if (requestCounter == 0) { + // if no connection and no requests posted + srl?.isRefreshing = false + update() + } + } + + companion object { + val TAG: String = TagFragment::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 7a39a24de2..5df7370dce 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -1,24 +1,24 @@ package com.tangem.wallet import android.util.Log - -import com.tangem.wallet.btc.BtcEngine -import com.tangem.wallet.eth.EthEngine -import com.tangem.wallet.token.TokenEngine -import com.tangem.wallet.bch.BtcCashEngine import com.tangem.data.Blockchain -import com.tangem.wallet.eos.EosEngine +import com.tangem.wallet.bch.BtcCashEngine import com.tangem.wallet.binance.BinanceEngine +import com.tangem.wallet.btc.BtcEngine import com.tangem.wallet.cardano.CardanoData import com.tangem.wallet.cardano.CardanoEngine import com.tangem.wallet.ducatus.DucatusEngine +import com.tangem.wallet.eos.EosEngine +import com.tangem.wallet.eth.EthEngine import com.tangem.wallet.ltc.LtcEngine import com.tangem.wallet.matic.MaticTokenEngine import com.tangem.wallet.nftToken.NftTokenEngine import com.tangem.wallet.rsk.RskEngine import com.tangem.wallet.rsk.RskTokenEngine +import com.tangem.wallet.token.TokenEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine +import com.tangem.wallet.xlmtag.XlmTagEngine import com.tangem.wallet.xrp.XrpEngine /** @@ -49,6 +49,7 @@ object CoinEngineFactory { Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine() Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine() Blockchain.StellarAsset -> XlmAssetEngine() + Blockchain.StellarTag -> XlmTagEngine() Blockchain.Eos -> EosEngine() Blockchain.Ducatus -> DucatusEngine() else -> null @@ -86,6 +87,8 @@ object CoinEngineFactory { XlmEngine(context) else if (Blockchain.StellarAsset == context.blockchain) XlmAssetEngine(context) + else if (Blockchain.StellarTag == context.blockchain) + XlmTagEngine(context) else if (Blockchain.Eos == context.blockchain) EosEngine(context) else if (Blockchain.Ducatus == context.blockchain) diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt new file mode 100644 index 0000000000..bdd8f4e3af --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt @@ -0,0 +1,133 @@ +package com.tangem.wallet.xlmtag + +import android.os.Bundle +import android.util.Log +import com.tangem.wallet.CoinData +import com.tangem.wallet.CoinEngine +import com.tangem.wallet.CoinEngine.InternalAmount +import org.stellar.sdk.KeyPair +import org.stellar.sdk.responses.AccountResponse +import org.stellar.sdk.responses.LedgerResponse +import java.math.BigDecimal + + +class XlmTagData : CoinData() { + class AccountResponseEx internal constructor(accountId: String?, sequenceNumber: Long?) : AccountResponse(KeyPair.fromAccountId(accountId), sequenceNumber) + + private var balance: CoinEngine.Amount? = null + private var sequenceNumber: Long? = 0L + private var baseReserve: CoinEngine.Amount? = CoinEngine.Amount("0.5", "XLM") + var baseFee: CoinEngine.Amount? = CoinEngine.Amount("0.00001", "XLM") + private set + var isError404 = false + var isTargetAccountCreated = false + var fundsFromTrustedSource = false + var fundsSentToTrustedSource = false + + + override fun clearInfo() { + super.clearInfo() + balance = null + isError404 = false + isTargetAccountCreated = false + fundsFromTrustedSource = false + fundsSentToTrustedSource = false + } + + fun getBalance(): CoinEngine.Amount? { + return if (balance != null) { + CoinEngine.Amount(balance!!.subtract(reserve), "XLM") + } else { + null + } + } + + val reserve: CoinEngine.Amount + get() = CoinEngine.Amount(baseReserve!!.multiply(BigDecimal.valueOf(2)), "XLM") + + var accountResponse: AccountResponse + get() = XlmTagData.AccountResponseEx(wallet, sequenceNumber) + set(accountResponse) { + if (accountResponse.balances.size > 0) { + val balanceResponse = accountResponse.balances[0] + balance = CoinEngine.Amount(balanceResponse.balance, "XLM") + } + sequenceNumber = accountResponse.sequenceNumber + isBalanceReceived = true + } + + fun setLedgerResponse(ledgerResponse: LedgerResponse) { + val xlmEngine = XlmTagEngine() + baseReserve = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseReserveInStroops, "stroops")) + baseFee = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseFeeInStroops, "stroops")) + } + + fun incSequenceNumber() { + sequenceNumber = sequenceNumber?.inc() + } + + override fun loadFromBundle(B: Bundle) { + super.loadFromBundle(B) + balance = if (B.containsKey("BalanceCurrency") && B.containsKey("BalanceDecimal")) { + CoinEngine.Amount(B.getString("BalanceDecimal"), B.getString("BalanceCurrency")) + } else { + null + } + sequenceNumber = if (B.containsKey("sequenceNumber")) { + B.getLong("sequenceNumber") + } else { + 0L + } + baseReserve = if (B.containsKey("BaseReserveCurrency") && B.containsKey("BaseReserveDecimal")) { + CoinEngine.Amount(B.getString("BaseReserveDecimal"), B.getString("BaseReserveCurrency")) + } else { + CoinEngine.Amount("0.5", "XLM") + } + baseFee = if (B.containsKey("BaseFeeCurrency") && B.containsKey("BaseFeeDecimal")) { + CoinEngine.Amount(B.getString("BaseFeeDecimal"), B.getString("BaseFeeCurrency")) + } else { + CoinEngine.Amount("0.00001", "XLM") + } + if (B.containsKey("Error404")) isError404 = B.getBoolean("Error404") else isError404 = false + if (B.containsKey("TargetAccountCreated")) isTargetAccountCreated = B.getBoolean("TargetAccountCreated") else isTargetAccountCreated = false + fundsFromTrustedSource = if (B.containsKey("FundsFromTrustedSource")) { + B.getBoolean("FundsFromTrustedSource") + } else { + false + } + fundsSentToTrustedSource = if (B.containsKey("FundsSentToTrustedSource")) { + B.getBoolean("FundsSentToTrustedSource") + } else { + false + } + + } + + override fun saveToBundle(B: Bundle) { + super.saveToBundle(B) + try { + if (balance != null) { + B.putString("BalanceCurrency", balance!!.currency) + B.putString("BalanceDecimal", balance!!.toValueString()) + } + if (sequenceNumber != null) { + B.putLong("sequenceNumber", sequenceNumber!!) + } + if (baseReserve != null) { + B.putString("BaseReserveCurrency", baseReserve!!.currency) + B.putString("BaseReserveDecimal", baseReserve!!.toValueString()) + } + if (baseFee != null) { + B.putString("BaseFeeCurrency", baseFee!!.currency) + B.putString("BaseFeeDecimal", baseFee!!.toValueString()) + } + if (isError404) B.putBoolean("Error404", true) + if (isTargetAccountCreated) B.putBoolean("TargetAccountCreated", true) + if (fundsFromTrustedSource) B.putBoolean("FundsFromTrustedSource", true) + if (fundsSentToTrustedSource) B.putBoolean("FundsSentToTrustedSource", true) + + } catch (e: Exception) { + Log.e("Can't save to bundle ", e.message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt new file mode 100644 index 0000000000..4fe5de60ca --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt @@ -0,0 +1,471 @@ +package com.tangem.wallet.xlmtag + +import android.net.Uri +import android.os.StrictMode +import android.text.InputFilter +import android.util.Log +import com.tangem.App +import com.tangem.data.Blockchain +import com.tangem.data.network.ServerApiStellar +import com.tangem.data.network.StellarRequest +import com.tangem.data.network.StellarRequest.Ledgers +import com.tangem.data.network.StellarRequest.SubmitTransaction +import com.tangem.tangem_card.data.TangemCard +import com.tangem.tangem_card.tasks.SignTask +import com.tangem.tangem_card.util.Util +import com.tangem.util.DecimalDigitsInputFilter +import com.tangem.wallet.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.stellar.sdk.* +import org.stellar.sdk.requests.RequestBuilder +import org.stellar.sdk.responses.operations.OperationResponse +import org.stellar.sdk.responses.operations.PaymentOperationResponse +import java.io.IOException +import java.math.BigDecimal + + +class XlmTagEngine : CoinEngine { + var coinData: XlmTagData? = null + val operations = mutableListOf() + + constructor(context: TangemContext) : super(context) { + if (context.coinData == null) { + coinData = XlmTagData() + context.coinData = coinData + } else if (context.coinData is XlmTagData) { + coinData = context.coinData as XlmTagData + } else { + throw Exception("Invalid type of Blockchain data for XlmEngine") + } + } + + constructor() : super() {} + + @Throws(Exception::class) + private fun checkBlockchainDataExists() { + if (coinData == null) throw Exception("No blockchain data") + } + + override fun awaitingConfirmation(): Boolean { + return App.pendingTransactionsStorage.hasTransactions(ctx.card) + } + + override fun getBalanceHTML(): String { + return if (hasBalanceInfo() && isBalanceNotZero && coinData!!.fundsFromTrustedSource) { + ctx.getString(R.string.tag_genuine) + } else if (coinData!!.fundsSentToTrustedSource) { + ctx.getString(R.string.tag_claimed) + } else { + ctx.getString(R.string.tag_not_genuine) + } + } + + override fun getBalanceCurrency(): String { + return "XLM" + } + + override fun isBalanceNotZero(): Boolean { + if (coinData == null) return false + return if (coinData!!.getBalance() == null) false else coinData!!.getBalance()!!.notZero() + } + + override fun hasBalanceInfo(): Boolean { + return if (coinData == null) false else coinData!!.getBalance() != null || coinData!!.isError404 + } + + override fun isExtractPossible(): Boolean { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data) + } else if (!isBalanceNotZero) { + ctx.setMessage(R.string.general_wallet_empty) + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.loaded_wallet_message_wait) + } else { + return true + } + return false + } + + override fun getFeeCurrency(): String { + return "XLM" + } + + override fun validateAddress(address: String): Boolean { + try { + val kp = KeyPair.fromAccountId(address) + } catch (e: Exception) { + return false + } + return true + } + + override fun isNeedCheckNode(): Boolean { + return false + } + + override fun getWalletExplorerUri(): Uri { + return Uri.parse("https://stellar.expert/explorer/public/account/" + ctx.coinData.wallet) + } + + override fun getShareWalletUri(): Uri { + return if (ctx.card.denomination != null) { + Uri.parse(ctx.coinData.wallet + "?amount=" + convertToAmount(convertToInternalAmount(ctx.card.denomination)!!).toValueString()) + } else { + Uri.parse(ctx.coinData.wallet) + } + } + + override fun getAmountInputFilters(): Array { + return arrayOf(DecimalDigitsInputFilter(decimals)) + } + + override fun checkNewTransactionAmount(amount: Amount): Boolean { + return true + } + + override fun checkNewTransactionAmountAndFee(amountValue: Amount, feeValue: Amount, isIncludeFee: Boolean): Boolean { + return true + } + + override fun validateBalance(balanceValidator: BalanceValidator): Boolean { + return try { + if (ctx.card.offlineBalance == null && !ctx.coinData.isBalanceReceived || !ctx.coinData.isBalanceReceived && ctx.card.remainingSignatures != ctx.card.maxSignatures) { + if (coinData!!.isError404) { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_no_account + balanceValidator.setSecondLine(R.string.balance_validator_second_line_create_account_instruction) + } else { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance + balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance) + return false + } + } + + if (coinData!!.isBalanceReceived && coinData!!.isBalanceEqual) { + balanceValidator.setScore(100) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance + balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain) + if (coinData!!.getBalance()!!.isZero) { + balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet + balanceValidator.setSecondLine(R.string.empty_string) + } + } + + if (ctx.card.offlineBalance != null && !coinData!!.isBalanceReceived && ctx.card.remainingSignatures == ctx.card.maxSignatures && coinData!!.getBalance()!!.notZero()) { + balanceValidator.setScore(80) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline + balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance) + } + + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun getBalance(): Amount? { + return if (!hasBalanceInfo()) null else coinData!!.getBalance()!! + } + + override fun evaluateFeeEquivalent(fee: String): String { + return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try { + val feeAmount = Amount(fee, feeCurrency) + feeAmount.toEquivalentString(coinData!!.rate.toDouble()) + } catch (e: Exception) { + "" + } + } + + override fun getBalanceEquivalent(): String { + if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return "" + val balance = balance ?: return "" + return balance.toEquivalentString(coinData!!.rate.toDouble()) + } + + override fun calculateAddress(pkUncompressed: ByteArray): String { + val kp = KeyPair.fromPublicKey(pkUncompressed) + return kp.accountId + } + + override fun convertToAmount(internalAmount: InternalAmount): Amount { + val d = internalAmount.divide(multiplier) + return Amount(d, balanceCurrency) + } + + override fun convertToAmount(strAmount: String, currency: String): Amount { + return Amount(strAmount, currency) + } + + override fun convertToInternalAmount(amount: Amount): InternalAmount { + val d = amount.multiply(multiplier) + return InternalAmount(d, "stroops") + } + + override fun convertToInternalAmount(bytes: ByteArray): InternalAmount? { + if (bytes == null) return null + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return InternalAmount(Util.byteArrayToLong(reversed), "stroops") + } + + override fun convertToByteArray(internalAmount: InternalAmount): ByteArray { + val bytes = Util.longToByteArray(internalAmount.longValueExact()) + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return reversed + } + + override fun createCoinData(): CoinData { + return XlmTagData() + } + + override fun getUnspentInputsDescription(): String { + return "" + } + + @Throws(Exception::class) + override fun constructTransaction(amountValue: Amount, feeValue: Amount, IncFee: Boolean, targetAddress: String): SignTask.TransactionToSign { + var amountValue = amountValue + checkBlockchainDataExists() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + if (IncFee) { + amountValue = Amount(amountValue.subtract(feeValue), amountValue.currency) + } + val operation: Operation + operation = if (coinData!!.isTargetAccountCreated) PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), AssetTypeNative(), amountValue.toValueString()).build() else CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build() + val transaction = TransactionEx.buildEx(60, coinData!!.accountResponse, operation) + if (transaction.fee != convertToInternalAmount(feeValue).intValueExact()) { + throw Exception("Invalid fee!") + } + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw + } + + @Throws(Exception::class) + override fun getHashesToSign(): Array { + val dataForSign = arrayOf(transaction.hash()) + return dataForSign + } + + @Throws(Exception::class) + override fun getRawDataToSign(): ByteArray { + return transaction.signatureBase() + } + + override fun getHashAlgToSign(): String { + return "sha-256" + } + + @Throws(Exception::class) + override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray { + throw Exception("Issuer validation not supported!") + } + + @Throws(Exception::class) + override fun onSignCompleted(signFromCard: ByteArray): ByteArray { // Sign the transaction to prove you are actually the person sending it. + transaction.setSign(signFromCard) + val txForSend = transaction.toEnvelopeXdrBase64().toByteArray() + notifyOnNeedSendTransaction(txForSend) + return txForSend + } + } + } + + private fun checkTargetAccountCreated(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) { + val serverApi = ServerApiStellar(ctx.blockchain) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + coinData!!.isTargetAccountCreated = true + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onFail(request: StellarRequest.Base) { + Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error) + if (request.errorResponse != null && request.errorResponse.code == 404) { + coinData!!.isTargetAccountCreated = false + if (amount.compareTo(coinData!!.reserve) >= 0) { + blockchainRequestsCallbacks.onComplete(true) + } else { + ctx.setError(R.string.confirm_transaction_error_not_enough_xlm_for_create) + blockchainRequestsCallbacks.onComplete(false) + } + } else { // suppose account is created if anything goes wrong + coinData!!.isTargetAccountCreated = true + blockchainRequestsCallbacks.onComplete(true) + } + } + } + serverApi.setListener(listener) + serverApi.requestData(ctx, StellarRequest.Balance(targetAddress)) + } + + private fun requestPayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + val server = Server("https://horizon.stellar.org/") + val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet) + + try { + var operationsPage = server.payments().forAccount(accountKeyPair).order(RequestBuilder.Order.DESC).execute() + Log.e("Stellar", operationsPage.records.toString()) + + operations.addAll(operationsPage.records) + + while (operations.size < OPERATIONS_LIMIT) { + operationsPage = operationsPage.getNextPage(server.httpClient) + operations.addAll(operationsPage.records) + Log.e("Stellar", operationsPage.records.toString()) + Log.e("Stellar", "Operations downloaded: " + operationsPage.records.count()) + Log.e("Stellar", "Operations overall: " + operations.count()) + } + + parsePayments(blockchainRequestsCallbacks) + } catch (e: Exception) { + if (e.message != null) { + ctx.error = e.message + blockchainRequestsCallbacks.onComplete(false) + } else { + ctx.error = e.javaClass.name + blockchainRequestsCallbacks.onComplete(false) + } + } + } + + private fun parsePayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + coinData!!.fundsFromTrustedSource = operations.find { it is PaymentOperationResponse && it.from.accountId == TRUSTED_SOURCE } != null + coinData!!.fundsSentToTrustedSource = + (operations.find { it is PaymentOperationResponse && it.to.accountId == TRUSTED_DESTINATION } != null) && coinData!!.fundsFromTrustedSource + Log.e("Stellar", + "fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " + + "fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource") + blockchainRequestsCallbacks.onComplete(!ctx.hasError()) + } + + override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + CoroutineScope(Dispatchers.IO).launch { requestPayments(blockchainRequestsCallbacks) } + val serverApi = ServerApiStellar(Blockchain.Stellar) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + Log.i(TAG, "onSuccess: " + request.javaClass.simpleName) + if (request is StellarRequest.Balance) { + coinData!!.accountResponse = request.accountResponse + coinData!!.validationNodeDescription = serverApi.currentURL + + blockchainRequestsCallbacks.onProgress() + } else if (request is Ledgers) { + coinData!!.setLedgerResponse(request.ledgerResponse) + if (serverApi.isRequestsSequenceCompleted) { + } else { + blockchainRequestsCallbacks.onProgress() + } + } else { + ctx.error = "Invalid request logic" + blockchainRequestsCallbacks.onComplete(false) + } + } + + override fun onFail(request: StellarRequest.Base) { + Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error) + if (request.errorResponse != null && request.errorResponse.code == 404) { + coinData!!.isError404 = true + } else { + ctx.error = request.error + } + if (serverApi.isRequestsSequenceCompleted) { + if (ctx.hasError()) { + blockchainRequestsCallbacks.onComplete(false) + } else { + blockchainRequestsCallbacks.onComplete(true) + } + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + serverApi.setListener(listener) + serverApi.requestData(ctx, StellarRequest.Balance(coinData!!.wallet)) + serverApi.requestData(ctx, Ledgers()) + } + + @Throws(Exception::class) + override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) { + coinData!!.maxFee = coinData!!.baseFee + coinData!!.normalFee = coinData!!.maxFee + coinData!!.minFee = coinData!!.normalFee + checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount) + } + + @Throws(IOException::class) + override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray) { + val serverApi = ServerApiStellar(ctx.blockchain) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + try { + if (!SubmitTransaction::class.java.isInstance(request)) throw Exception("Invalid request logic") + val submitTransactionRequest = request as SubmitTransaction + if (submitTransactionRequest.response.isSuccess) { + ctx.error = null + blockchainRequestsCallbacks.onComplete(true) + } else { + if (submitTransactionRequest.response.extras != null && submitTransactionRequest.response.extras.resultCodes != null) { + var trResult = submitTransactionRequest.response.extras.resultCodes.transactionResultCode + if (submitTransactionRequest.response.extras.resultCodes.operationsResultCodes != null && submitTransactionRequest.response.extras.resultCodes.operationsResultCodes.size > 0) { + trResult += "/" + submitTransactionRequest.response.extras.resultCodes.operationsResultCodes[0] + } + ctx.error = trResult + } else { + ctx.error = "transaction failed" + } + blockchainRequestsCallbacks.onComplete(false) + } + } catch (e: Exception) { + if (e.message != null) { + ctx.error = e.message + blockchainRequestsCallbacks.onComplete(false) + } else { + ctx.error = e.javaClass.name + blockchainRequestsCallbacks.onComplete(false) + } + } + } + + override fun onFail(request: StellarRequest.Base) { + ctx.error = request.error + blockchainRequestsCallbacks.onComplete(false) + } + } + serverApi.setListener(listener) + val transaction = TransactionEx.fromEnvelopeXdr(String(txForSend)) + coinData!!.incSequenceNumber() + serverApi.requestData(ctx, SubmitTransaction(transaction)) + } + + override fun needMultipleLinesForBalance(): Boolean { + return true + } + + override fun allowSelectFeeLevel(): Boolean { + return false + } + + override fun pendingTransactionTimeoutInSeconds(): Int { + return 10 + } + + companion object { + private val TAG = XlmTagEngine::class.java.simpleName + private val decimals: Int + private get() = 7 + + private val multiplier = BigDecimal("10000000") + + const val OPERATIONS_LIMIT = 50 + private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM" + private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2" + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tag.xml b/app/src/main/res/layout/fragment_tag.xml new file mode 100644 index 0000000000..87951c04e6 --- /dev/null +++ b/app/src/main/res/layout/fragment_tag.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 7409283746..a65f23db22 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -28,6 +28,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5cfe74312..645ff3bfac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -264,6 +264,12 @@ To this phone %1$d unspents (%2$d received) + + Claim + GENUINE + NOT GENUINE + ALREADY CLAIMED + Save Delete diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml index c10d3b1038..e2921c8701 100644 --- a/app/src/main/res/xml/nfc_tech_filter.xml +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -3,5 +3,6 @@ android.nfc.tech.IsoDep android.nfc.tech.Ndef + android.nfc.tech.NfcV \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java index 4fd4cc1b0f..8fe19575d6 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java @@ -29,6 +29,7 @@ public class TangemCard { private byte[] terminalPrivateKey; private byte[] terminalPublicKey; private boolean terminalIsLinked = false; + private byte[] tagSignature = null; public String getBlockchainID() { return blockchainID; @@ -629,6 +630,14 @@ public class TangemCard { DenominationText = null; } + public byte[] getTagSignature() { + return tagSignature; + } + + public void setTagSignature(byte[] tagSignature) { + this.tagSignature = tagSignature; + } + // public Bundle getAsBundle() { // Bundle B = new Bundle(); // saveToBundle(B); diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt index b59167f216..dffc588721 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt @@ -24,7 +24,8 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb val TAG: String = NfcManager::class.java.simpleName // reader mode flags: listen for type A (not B), skipping ndef check - private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS + private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_V or + NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS private const val DELAY_PRESENCE = 1500 private const val REQUEST_NFC_PERMISSIONS = 1 diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt index f6e825319a..9c6356dd4e 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt @@ -85,6 +85,8 @@ fun TangemCard.loadFromBundle(B: Bundle) { if (B.containsKey("terminalPublicKey")) terminalPublicKey = B.getByteArray("terminalPublicKey") terminalIsLinked = B.getBoolean("terminalIsLinked") + if (B.containsKey("tagSignature")) tagSignature = B.getByteArray("tagSignature") + } val TangemCard.asBundle: Bundle @@ -112,11 +114,10 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putInt("Health", health) if (settingsMask != null) B.putInt("settingsMask", settingsMask) B.putInt("pauseBeforePIN2", pauseBeforePIN2) - if( allowedSigningMethod!=null ){ - var iSigningMethod=0x80 - for(sM in allowedSigningMethod) - { - iSigningMethod=iSigningMethod.or(0x01.shl(sM.ID)) + if (allowedSigningMethod != null) { + var iSigningMethod = 0x80 + for (sM in allowedSigningMethod) { + iSigningMethod = iSigningMethod.or(0x01.shl(sM.ID)) } B.putInt("signingMethod", iSigningMethod) } @@ -163,6 +164,8 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putByteArray("terminalPublicKey", terminalPublicKey) B.putBoolean("terminalIsLinked", terminalIsLinked) + if (tagSignature != null) B.putByteArray("tagSignature", tagSignature) + } catch (e: Exception) { Log.e("Can't save to bundle ", e.message) } From d35dc8a87b2a2e906ea3f7adbaf82e533da8b589 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Jan 2020 18:57:45 +0300 Subject: [PATCH 06/26] Updated on 2026-08-14 --- .../main/java/com/tangem/data/Blockchain.java | 7 +- .../tangem/data/network/ServerApiTezos.java | 139 +++++ .../com/tangem/data/network/TezosApi.java | 35 ++ .../tangem/data/network/model/TezosBody.kt | 25 + .../data/network/model/TezosResponse.kt | 19 + .../com/tangem/wallet/CoinEngineFactory.kt | 4 + .../tangem/wallet/cardano/CardanoEngine.java | 2 +- .../java/com/tangem/wallet/tezos/TezosData.kt | 49 ++ .../com/tangem/wallet/tezos/TezosEngine.kt | 507 ++++++++++++++++++ build.gradle | 2 +- 10 files changed, 784 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/tangem/data/network/ServerApiTezos.java create mode 100644 app/src/main/java/com/tangem/data/network/TezosApi.java create mode 100644 app/src/main/java/com/tangem/data/network/model/TezosBody.kt create mode 100644 app/src/main/java/com/tangem/data/network/model/TezosResponse.kt create mode 100644 app/src/main/java/com/tangem/wallet/tezos/TezosData.kt create mode 100644 app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 92db5c5d43..f51ab87524 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -23,11 +23,12 @@ public enum Blockchain { BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance Testnet"), Matic("MATIC", "MTX", 1.0, R.drawable.tangem2, "Matic"), MaticTestNet("MATIC/test", "MTX", 1.0, R.drawable.tangem2, "Matic Testnet"), - Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), - StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"), + 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"), Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"), - Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"); + Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"), + Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.tangem2, "Tezos"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/ServerApiTezos.java b/app/src/main/java/com/tangem/data/network/ServerApiTezos.java new file mode 100644 index 0000000000..b5300912ca --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiTezos.java @@ -0,0 +1,139 @@ +package com.tangem.data.network; + +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import com.tangem.data.network.model.TezosAccountResponse; +import com.tangem.data.network.model.TezosForgeBody; +import com.tangem.data.network.model.TezosHeaderResponse; +import com.tangem.data.network.model.TezosPreapplyBody; +import com.tangem.tangem_card.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; + +public class ServerApiTezos { + private static String TAG = ServerApiTezos.class.getSimpleName(); + + private final String letzbakeURI = "https://teznode.letzbake.com"; + private final String tezrpcURI = "https://mainnet.tezrpc.me"; + + static final String TEZOS_ADDRESS = "chains/main/blocks/head/context/contracts/{address}"; + static final String TEZOS_HEADER = "chains/main/blocks/head/header"; + static final String TEZOS_MANAGER_KEY = "chains/main/blocks/head/context/contracts/{address}/manager_key"; + static final String TEZOS_FORGE_OPERATIONS = "chains/main/blocks/head/helpers/forge/operations"; + static final String TEZOS_PREAPPLY_OPERATIONS = "chains/main/blocks/head/helpers/preapply/operations"; + static final String TEZOS_RUN_OPERATION = "chains/main/blocks/head/helpers/scripts/run_operation"; + static final String TEZOS_INJECT_OPERATIONS = "injection/operation"; + + private Retrofit retrofitTezos = new Retrofit.Builder() + .baseUrl(letzbakeURI) + .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(ScalarsConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + //logging for testing + .client(new OkHttpClient.Builder().addInterceptor( + new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + ).build()) + + .build(); + + private TezosApi tezosApi = retrofitTezos.create(TezosApi.class); + + private int requestsCount = 0; + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + public void getAddress(String wallet, SingleObserver accountObserver) { + requestsCount++; + Log.i(TAG, "new getAddress request"); + + Single accountSingle = tezosApi.getAccount(wallet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + accountSingle.subscribe(accountObserver); + } + + public void getMangerKey(String wallet, SingleObserver accountObserver) { + requestsCount++; + Log.i(TAG, "new getManagerKey request"); + + Single managerKeySingle = tezosApi.getManagerKey(wallet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + managerKeySingle.subscribe(accountObserver); + } + + public TezosHeaderResponse getHeader() throws Exception { // TODO? not async + requestsCount++; + Log.i(TAG, "new getHeader request"); + + Response headerResponse = tezosApi.getHeader().execute(); + + requestsCount--; + if (headerResponse.code() == 200) { + return headerResponse.body(); + } else { + throw new Exception("Wrong header response, code: " + headerResponse.code()); + } + } + + public String forgeOperations(TezosForgeBody tezosForgeBody) throws Exception { // TODO? not async + requestsCount++; + Log.i(TAG, "new forgeOperations request"); + + Response forgeResponse = tezosApi.forgeOperations(tezosForgeBody).execute(); + + requestsCount--; + if (forgeResponse.code() == 200) { + return forgeResponse.body(); + } else { + throw new Exception("Wrong forge response, code: " + forgeResponse.code()); + } + } + + public void peapplyOperations(TezosPreapplyBody tezosPreapplyBody) throws Exception { + Log.i(TAG, "new peapplyOperations request"); + + List tezosPreapplyBodyList = new ArrayList<>(); + tezosPreapplyBodyList.add(tezosPreapplyBody); + Response preapplyResponse = tezosApi.preapplyOperations(tezosPreapplyBodyList).execute(); + + if (preapplyResponse.code() != 200) { + String error = "Preapply error: unknown error"; + if (preapplyResponse.errorBody() != null) { + error = "Preapply error: " + preapplyResponse.errorBody().string(); + } + Log.e(TAG, error); + throw new Exception(error); + } + } + + public void injectOperations(String txForSend, SingleObserver injectObserver) { + requestsCount++; + Log.i(TAG, "new injectOperations request"); + + Single injectSingle = tezosApi.injectOperations(txForSend) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + injectSingle.subscribe(injectObserver); + } +} diff --git a/app/src/main/java/com/tangem/data/network/TezosApi.java b/app/src/main/java/com/tangem/data/network/TezosApi.java new file mode 100644 index 0000000000..e61809b3d7 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/TezosApi.java @@ -0,0 +1,35 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.TezosAccountResponse; +import com.tangem.data.network.model.TezosForgeBody; +import com.tangem.data.network.model.TezosHeaderResponse; +import com.tangem.data.network.model.TezosPreapplyBody; + +import java.util.List; + +import io.reactivex.Single; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface TezosApi { + @GET(ServerApiTezos.TEZOS_ADDRESS) + Single getAccount(@Path("address") String address); + + @GET(ServerApiTezos.TEZOS_HEADER) + Call getHeader(); + + @GET(ServerApiTezos.TEZOS_MANAGER_KEY) + Single getManagerKey(@Path("address") String address); + + @POST(ServerApiTezos.TEZOS_FORGE_OPERATIONS) + Call forgeOperations(@Body TezosForgeBody tezosForgeBody); + + @POST(ServerApiTezos.TEZOS_PREAPPLY_OPERATIONS) + Call preapplyOperations(@Body List tezosPreapplyBodyList); + + @POST(ServerApiTezos.TEZOS_INJECT_OPERATIONS) + Single injectOperations(@Body String txForSend); +} diff --git a/app/src/main/java/com/tangem/data/network/model/TezosBody.kt b/app/src/main/java/com/tangem/data/network/model/TezosBody.kt new file mode 100644 index 0000000000..16bf34b145 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TezosBody.kt @@ -0,0 +1,25 @@ +package com.tangem.data.network.model + +data class TezosForgeBody( + val branch: String, + val contents: List +) + +data class TezosOperationContent( + val kind: String, + val source: String, + val fee: String, + val counter: String, + val gas_limit: String, + val storage_limit: String, + val public_key: String? = null, + val destination: String? = null, + val amount: String? = null +) + +data class TezosPreapplyBody( + val protocol: String, + val branch: String, + val contents: List, + val signature: String +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt b/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt new file mode 100644 index 0000000000..db3bece398 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt @@ -0,0 +1,19 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class TezosAccountResponse( + @SerializedName("balance") + var balance: Long? = null, + + @SerializedName("counter") + var counter: Long? = null +) + +data class TezosHeaderResponse( + @SerializedName("protocol") + var protocol: String? = null, + + @SerializedName("hash") + var hash: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 7a39a24de2..fb372a74b7 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -17,6 +17,7 @@ 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.tezos.TezosEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine import com.tangem.wallet.xrp.XrpEngine @@ -51,6 +52,7 @@ object CoinEngineFactory { Blockchain.StellarAsset -> XlmAssetEngine() Blockchain.Eos -> EosEngine() Blockchain.Ducatus -> DucatusEngine() + Blockchain.Tezos -> TezosEngine() else -> null } } @@ -90,6 +92,8 @@ object CoinEngineFactory { EosEngine(context) else if (Blockchain.Ducatus == context.blockchain) DucatusEngine(context) + else if (Blockchain.Tezos == context.blockchain) + TezosEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java index c9a700254f..3b7792df59 100644 --- a/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java @@ -503,7 +503,7 @@ public class CardanoEngine extends CoinEngine { @Override public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { - return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw; + return signingMethod == TangemCard.SigningMethod.Sign_Hash; } @Override diff --git a/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt b/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt new file mode 100644 index 0000000000..0fe80b1c4a --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt @@ -0,0 +1,49 @@ +package com.tangem.wallet.tezos + +import android.os.Bundle +import android.util.Log +import com.tangem.wallet.CoinData +import java.lang.Exception + +class TezosData : CoinData() { + var balance: Long? = null + var counter: Long? = null + var publicKeyReavealed: Boolean? = null + var tezosPublicKey: String? = null + + override fun clearInfo() { + super.clearInfo() + balance = null + counter = null + publicKeyReavealed = null + tezosPublicKey = null + } + + override fun loadFromBundle(B: Bundle) { + super.loadFromBundle(B) + + if (B.containsKey("Balance")) balance = B.getLong("Balance") + + if (B.containsKey("Counter")) counter = B.getLong("Counter") + + if (B.containsKey("PublicKeyReavealed")) publicKeyReavealed = B.getBoolean("PublicKeyReavealed") + + if (B.containsKey("TezosPublicKey")) tezosPublicKey = B.getString("TezosPublicKey") + } + + override fun saveToBundle(B: Bundle) { + super.saveToBundle(B) + try { + if (balance != null) B.putLong("Balance", balance!!) + + if (counter != null) B.putLong("Counter", counter!!) + + if (publicKeyReavealed != null) B.putBoolean("PublicKeyReavealed", publicKeyReavealed!!) + + if (tezosPublicKey != null) B.putString("TezosPublicKey", tezosPublicKey!!) + + } catch (e: Exception) { + Log.e("Can't save to bundle ", e.message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt b/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt new file mode 100644 index 0000000000..5ad3c85cf2 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt @@ -0,0 +1,507 @@ +package com.tangem.wallet.tezos + +import android.net.Uri +import android.os.StrictMode +import android.text.InputFilter +import android.util.Log +import com.tangem.App +import com.tangem.data.network.ServerApiTezos +import com.tangem.data.network.model.TezosAccountResponse +import com.tangem.data.network.model.TezosForgeBody +import com.tangem.data.network.model.TezosOperationContent +import com.tangem.data.network.model.TezosPreapplyBody +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.CryptoUtil +import com.tangem.util.DecimalDigitsInputFilter +import com.tangem.wallet.* +import io.reactivex.observers.DisposableSingleObserver +import org.spongycastle.jcajce.provider.digest.Blake2b +import java.math.BigDecimal + +class TezosEngine : CoinEngine { + constructor() + + constructor(context: TangemContext) : super(context) { + if (context.coinData == null) { + coinData = TezosData() + context.coinData = coinData + } else if (context.coinData is TezosData) { + coinData = context.coinData as TezosData + } else { + throw Exception("Invalid type of Blockchain data for XlmEngine") + } + } + + private val TAG = TezosEngine::class.java.simpleName + + var coinData: TezosData? = null + + private fun getDecimals() = 6 + + @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? { + val balance = balance + return if (balance != null) { + balance.toDescriptionString(getDecimals()) + } else { + "" + } + } + + override fun getBalanceCurrency(): String? { + return "XTZ" + } + + override fun isBalanceNotZero(): Boolean { + if (coinData == null) return false + return if (balance == null) false else balance!!.notZero() + } + + override fun hasBalanceInfo(): Boolean { + return if (coinData == null) false else coinData!!.balance != null + } + + 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 "XTZ" + } + + override fun validateAddress(address: String?): Boolean { + val prefixedHashWithChecksum = Base58.decodeBase58(address) + + if (prefixedHashWithChecksum == null || prefixedHashWithChecksum.size != 27) return false + + val prefixedHash = prefixedHashWithChecksum.copyOf(23) + val checksum = prefixedHashWithChecksum.copyOfRange(23, 27) + + val calcChecksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4) + + return calcChecksum.contentEquals(checksum) + } + + override fun isNeedCheckNode(): Boolean { + return false + } + + override fun getWalletExplorerUri(): Uri? { + return Uri.parse("https://tezblock.io/account/" + ctx.coinData.wallet) + } + + override fun getShareWalletUri(): Uri? { + return Uri.parse(ctx.coinData.wallet) + } + + override fun getAmountInputFilters(): Array? { + return arrayOf(DecimalDigitsInputFilter(getDecimals())) + } + + override fun checkNewTransactionAmount(amount: Amount): Boolean { + if (coinData == null) return false + return amount <= balance + } + + override fun checkNewTransactionAmountAndFee(amountValue: Amount?, + feeValue: Amount?, + isIncludeFee: Boolean + ): Boolean { + try { + checkBlockchainDataExists() + } catch (e: java.lang.Exception) { + e.printStackTrace() + return false + } + if (feeValue == null || amountValue == null) return false + if (feeValue.isZero || amountValue.isZero) return false + if (isIncludeFee && (amountValue > balance || amountValue < feeValue)) return false + if (!isIncludeFee && amountValue.add(feeValue) > balance) return false + + return true + } + + override fun validateBalance(balanceValidator: BalanceValidator): Boolean { + try { + if (ctx.card.offlineBalance == null && + !ctx.coinData.isBalanceReceived || + !ctx.coinData.isBalanceReceived && + ctx.card.remainingSignatures != ctx.card.maxSignatures + ) { + 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) { + 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 (balance!!.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 && + balance!!.notZero() + ) { + balanceValidator.setScore(80) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline + balanceValidator.setSecondLine( + R.string.balance_validator_second_line_internet_to_get_balance + ) + } + return true + } catch (e: java.lang.Exception) { + e.printStackTrace() + return false + } + } + + override fun getBalance(): Amount? { + var balanceAmount: Amount? = null + + if (hasBalanceInfo()) { + val xtzBalance = BigDecimal + .valueOf(coinData!!.balance!!).movePointLeft(getDecimals()) + balanceAmount = Amount(xtzBalance, balanceCurrency) + } + + return balanceAmount + } + + override fun evaluateFeeEquivalent(fee: String?): String? { + return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try { + val feeAmount = Amount(fee, feeCurrency) + feeAmount.toEquivalentString(coinData!!.rate.toDouble()) + } catch (e: java.lang.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 publicKeyHash = Blake2b.Blake2b160().digest(pkUncompressed) + + val tz1Prefix = Util.hexToBytes("06A19F") + val prefixedHash = tz1Prefix + publicKeyHash + + val checksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4) + val prefixedHashWithChecksum = prefixedHash + checksum + + return Base58.encodeBase58(prefixedHashWithChecksum) + } + + fun calculateTezosPublicKey(pkUncompressed: ByteArray): String { + val edpkPrefix = Util.hexToBytes("0D0F25D9") + val prefixedPubKey = edpkPrefix + pkUncompressed + + val checksum = CryptoUtil.doubleSha256(prefixedPubKey).copyOfRange(0, 4) + val prefixedHashWithChecksum = prefixedPubKey + checksum + + return Base58.encodeBase58(prefixedHashWithChecksum) + } + + override fun convertToAmount(internalAmount: InternalAmount): Amount { + return Amount(internalAmount.movePointLeft(getDecimals()), balanceCurrency) + } + + override fun convertToAmount(strAmount: String, currency: String): Amount { + return Amount(strAmount, currency) + } + + override fun convertToInternalAmount(amount: Amount): InternalAmount { + return InternalAmount(amount.movePointRight(getDecimals()), "mutez") + } + + 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), "mutez") + } + + 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 TezosData() + } + + override fun getUnspentInputsDescription() = "" + + override fun constructTransaction( + amountValue: Amount, + feeValue: Amount, + IncFee: Boolean, + targetAddress: String + ): SignTask.TransactionToSign { + + checkBlockchainDataExists() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + + val finalAmount = if (IncFee) { + amountValue.minus(feeValue) + } else { + amountValue + } + + val serverApiTezos = ServerApiTezos() + val headerResponse = serverApiTezos.header + + val contents = arrayListOf() + + var counter = coinData!!.counter!! + + if (!coinData!!.publicKeyReavealed!!) { + counter++ + val revealOp = TezosOperationContent( + kind = "reveal", + source = coinData!!.wallet, + fee = "1300", + counter = counter.toString(), + gas_limit = "10000", + storage_limit = "0", + public_key = coinData!!.tezosPublicKey!! + ) + + contents.add(revealOp) + } + + counter++ + val transactionOp = TezosOperationContent( + kind = "transaction", + source = coinData!!.wallet, + fee = "1350", + counter = counter.toString(), + gas_limit = "10600", + storage_limit = "277", + destination = targetAddress, + amount = finalAmount.movePointRight(getDecimals()).toBigInteger().toString() + ) + + contents.add(transactionOp) + + val tezosForgeBody = TezosForgeBody(headerResponse.hash!!, contents) + val forgeResponse = serverApiTezos.forgeOperations(tezosForgeBody) + val watermark = "03" + val forgedBytes = Util.hexToBytes(watermark + forgeResponse) + + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash + } + + @Throws(java.lang.Exception::class) + override fun getHashesToSign(): Array { + val dataForSign = arrayOfNulls(1) + dataForSign[0] = Blake2b.Blake2b256().digest(forgedBytes) + return dataForSign + } + + @Throws(java.lang.Exception::class) + override fun getRawDataToSign(): ByteArray? { + throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName) + } + + @Throws(java.lang.Exception::class) + override fun getHashAlgToSign(): String? { + throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName) + } + + @Throws(java.lang.Exception::class) + override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray?): ByteArray? { + throw java.lang.Exception("Transaction validation by issuer not supported in this version") + } + + @Throws(java.lang.Exception::class) + override fun onSignCompleted(signFromCard: ByteArray): ByteArray? { + val edsigPrefix = Util.hexToBytes("09F5CD8612") + val prefixedSignature = edsigPrefix + signFromCard + val checksum = CryptoUtil.doubleSha256(prefixedSignature).copyOfRange(0, 4) + val prefixedSignatureWithChecksum = prefixedSignature + checksum + + val preapplyBody = TezosPreapplyBody( + protocol = headerResponse.protocol!!, + branch = headerResponse.hash!!, + contents = contents, + signature = Base58.encodeBase58(prefixedSignatureWithChecksum) + ) + + try { + serverApiTezos.peapplyOperations(preapplyBody) + } catch (e: java.lang.Exception) { + ctx.error = e.message + return null + } + + val txForSend = Util.hexToBytes(forgeResponse) + signFromCard + notifyOnNeedSendTransaction(txForSend) + return txForSend + } + } + } + + override fun requestBalanceAndUnspentTransactions( + blockchainRequestsCallbacks: BlockchainRequestsCallbacks + ) { + coinData!!.tezosPublicKey = calculateTezosPublicKey(ctx.card.walletPublicKey) + + val serverApiTezos = ServerApiTezos() + + val accountObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: TezosAccountResponse) { + coinData!!.balance = response.balance + coinData!!.isBalanceReceived = true + coinData!!.counter = response.counter + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "requestBalanceAndUnspentTransactions error " + e.message) + ctx.error = e.message + e.printStackTrace() + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(false) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + + val managerKeyObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: String) { + coinData!!.publicKeyReavealed = true + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "requestFee error " + e.message) + e.printStackTrace() + + coinData!!.publicKeyReavealed = false // error expected when key is not revealed + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + + serverApiTezos.getAddress(coinData!!.wallet, accountObserver) + serverApiTezos.getMangerKey(coinData!!.wallet, managerKeyObserver) + + } + + override fun requestFee( + blockchainRequestsCallbacks: BlockchainRequestsCallbacks, + targetAddress: String?, + amount: Amount? + ) { + var fee: BigDecimal = BigDecimal.valueOf(0.00135) + + if (!coinData!!.publicKeyReavealed!!) { + fee += BigDecimal.valueOf(0.0013) + } + + val serverApiTezos = ServerApiTezos() + + val accountObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: TezosAccountResponse) { + if (response.balance == 0L) { + fee += BigDecimal.valueOf(0.257) + } + val feeAmount = Amount(fee, feeCurrency) + coinData!!.minFee = feeAmount + coinData!!.normalFee = feeAmount + coinData!!.maxFee = feeAmount + + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "requestFee error " + e.message) + ctx.error = e.message + e.printStackTrace() + + blockchainRequestsCallbacks.onComplete(false) + } + } + + serverApiTezos.getAddress(targetAddress, accountObserver) + } + + override fun requestSendTransaction( + blockchainRequestsCallbacks: BlockchainRequestsCallbacks, + txForSend: ByteArray? + ) { + + if (txForSend == null) { + blockchainRequestsCallbacks.onComplete(false) + } else { + val injectObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: Any) { + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onError(e: Throwable) { + blockchainRequestsCallbacks.onComplete(false) + } + } + + ServerApiTezos().injectOperations(Util.bytesToHex(txForSend), injectObserver) + } + } + + override fun allowSelectFeeLevel(): Boolean { + return false + } + + override fun pendingTransactionTimeoutInSeconds(): Int { + return 60 + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index e01893d33f..c4edca4c38 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'io.fabric.tools:gradle:1.31.0' From e68e08f04525a7f94e6babc4894cfc041e18bfe8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Jan 2020 18:30:09 +0300 Subject: [PATCH 07/26] Updated on 2026-08-14 --- .../java/com/tangem/ui/fragment/MainFragment.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index cecb0ddeee..98776f218e 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -127,7 +127,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader tvBuyCards?.setText(spannable, TextView.BufferType.SPANNABLE) llShoppingView?.setOnClickListener { - val uri = Uri.parse ("https://www.tangemcards.com") + val uri = Uri.parse("https://www.tangemcards.com") val intent = Intent(Intent.ACTION_VIEW, uri) startActivity(intent) } @@ -233,7 +233,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader 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.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 @@ -316,8 +316,13 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } } card.status == TangemCard.Status.Empty -> { - val bundle = Bundle().apply { ctx.saveToBundle(this) } - navigateToDestination(R.id.action_main_to_emptyWalletFragment, bundle) + val engineCoin = CoinEngineFactory.create(ctx) + if (engineCoin != null) { + val bundle = Bundle().apply { ctx.saveToBundle(this) } + navigateToDestination(R.id.action_main_to_emptyWalletFragment, bundle) + } else { + showUnkownBlockchainWarning() + } } card.status == TangemCard.Status.Purged -> Toast.makeText(context, R.string.main_screen_erased_wallet, Toast.LENGTH_SHORT).show() card.status == TangemCard.Status.NotPersonalized -> Toast.makeText(context, R.string.main_screen_not_personalized, Toast.LENGTH_SHORT).show() From f84e0d0a7ce897bb24a16bd232dc79cdda5f1927 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Jan 2020 13:59:00 +0300 Subject: [PATCH 08/26] Updated on 2026-08-14 --- .../com/tangem/ui/fragment/MainFragment.kt | 94 +++++--------- .../tangem/tangem_card/reader/NfcReader.java | 1 + .../tangem/tangem_card/reader/TLVList.java | 16 +++ .../tangem_sdk/android/reader/NfcManager.kt | 2 +- .../tangem_sdk/android/reader/NfcReader.kt | 4 + .../tangem_sdk/android/reader/NfcVReader.kt | 39 ++++++ .../android/reader/ReadSlixTagTask.kt | 120 ++++++++++++++++++ 7 files changed, 213 insertions(+), 63 deletions(-) create mode 100644 tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcVReader.kt create mode 100644 tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/ReadSlixTagTask.kt diff --git a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index cecb0ddeee..b4f7e31965 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -35,6 +35,9 @@ import com.tangem.tangem_card.tasks.ReadCardInfoTask import com.tangem.tangem_sdk.android.data.PINStorage import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation import com.tangem.tangem_sdk.android.reader.NfcReader +import com.tangem.tangem_sdk.android.reader.NfcVReader +import com.tangem.tangem_sdk.android.reader.ReadResult +import com.tangem.tangem_sdk.android.reader.ReadSlixTagTask import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID import com.tangem.tangem_sdk.data.loadFromBundle @@ -127,7 +130,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader tvBuyCards?.setText(spannable, TextView.BufferType.SPANNABLE) llShoppingView?.setOnClickListener { - val uri = Uri.parse ("https://www.tangemcards.com") + val uri = Uri.parse("https://www.tangemcards.com") val intent = Intent(Intent.ACTION_VIEW, uri) startActivity(intent) } @@ -170,7 +173,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader return } - parseNfcvTag(tag) + NfcV.get(tag)?.let { onNfcVDiscovered(it, tag.id) } try { // get IsoDep handle and run cardReader thread @@ -195,74 +198,41 @@ 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) { + private fun onNfcVDiscovered(nfcV: NfcV, uid: ByteArray) { scope.launch { + when (val readResult = ReadSlixTagTask(NfcVReader(nfcV)).read()) { + is ReadResult.Failure -> (activity as MainActivity).nfcManager.notifyReadResult(false) + is ReadResult.Success -> { + try { + val tlvs = readResult.tlvs + val cardDataTlv = TLVList.fromBytes((tlvs.getTLV(TLV.Tag.TAG_CardData)).Value) + Log.v(TAG, "\n" + tlvs.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 = tlvs.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value + card.status = TangemCard.Status.Loaded + card.tagSignature = tlvs.getTLV(TLV.Tag.TAG_Signature).Value + val ctx = TangemContext(card) + val engineCoin = XlmTagEngine(ctx) + engineCoin.defineWallet() + launch(Dispatchers.Main) { - 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()) + 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) } } - } catch (e: Exception) { - e.printStackTrace() - (activity as MainActivity).nfcManager.notifyReadResult(false) } - } } diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java index d93099f6ac..d0ad24b4b5 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java @@ -17,4 +17,5 @@ public interface NfcReader { void connect(); + boolean isConnected(); } \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java index d071484cbd..40c36a777b 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java @@ -69,4 +69,20 @@ public class TLVList extends ArrayList { while (tlv != null); return tlvList; } + + public static TLVList tryFromBytes(byte[] mData) { + TLVList tlvList = new TLVList(); + ByteArrayInputStream stream = new ByteArrayInputStream(mData); + TLV tlv = null; + do { + try { + tlv = TLV.ReadFromStream(stream); + if (tlv != null) tlvList.add(tlv); + } catch (IOException e) { + break; + } + } + while (tlv != null); + return tlvList; + } } diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt index dffc588721..a1c6e5f49d 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt @@ -25,7 +25,7 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb // 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_NFC_V or - NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS + NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS private const val DELAY_PRESENCE = 1500 private const val REQUEST_NFC_PERMISSIONS = 1 diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcReader.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcReader.kt index c7a0409b99..a628e05071 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcReader.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcReader.kt @@ -38,4 +38,8 @@ data class NfcReader( isoDep.timeout = timeout } + override fun isConnected(): Boolean { + return isoDep.isConnected + } + } \ No newline at end of file diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcVReader.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcVReader.kt new file mode 100644 index 0000000000..47c9239cc7 --- /dev/null +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcVReader.kt @@ -0,0 +1,39 @@ +package com.tangem.tangem_sdk.android.reader + +import android.nfc.tech.NfcV + +data class NfcVReader( + var nfcV: NfcV? +) : com.tangem.tangem_card.reader.NfcReader { + + override fun getId(): ByteArray? { + return nfcV?.tag?.id + } + + override fun setTimeout(timeout: Int) { + } + + override fun getTimeout(): Int { + return 0 + } + + override fun transceive(data: ByteArray?): ByteArray? { + return nfcV?.transceive(data) + } + + override fun ignoreTag() { + nfcV?.close() + nfcV = null + } + + override fun notifyReadResult(success: Boolean) { + } + + override fun connect() { + nfcV?.connect() + } + + override fun isConnected(): Boolean { + return nfcV?.isConnected == true + } +} \ No newline at end of file diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/ReadSlixTagTask.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/ReadSlixTagTask.kt new file mode 100644 index 0000000000..a061348bd4 --- /dev/null +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/ReadSlixTagTask.kt @@ -0,0 +1,120 @@ +package com.tangem.tangem_sdk.android.reader + +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import com.tangem.tangem_card.reader.NfcReader +import com.tangem.tangem_card.reader.TLV +import com.tangem.tangem_card.reader.TLVList +import com.tangem.tangem_card.util.Util +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.* + +class ReadSlixTagTask(val nfcReader: NfcReader) { + + private var lastErrorCode: Int = 0 + + fun read(): ReadResult { + if (!nfcReader.isConnected) { + try { + nfcReader.connect() + } catch (e: Exception) { + return ReadResult.Failure(e) + } + } + return try { + runRead() + } catch (e: Exception) { + nfcReader.ignoreTag() + ReadResult.Failure(e) + } + } + + private fun runRead(): ReadResult { + val ndefMessage = runReadNDEF() + val records: Array = ndefMessage.records + for (record in records) { + if (record.toUri() != null && record.toUri().toString() == "vnd.android.nfc://ext/tangem.com:wallet") { + val payload = record.payload + val tlvNDEF = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size)) + nfcReader.ignoreTag() + return ReadResult.Success(tlvNDEF) + } + } + return ReadResult.Failure(Exception("No parcelable Tlv has been found.")) + } + + private fun runReadNDEF(): NdefMessage { + val answerCC = readSingleBlock(0x00) + + if (answerCC.size != 4 || answerCC[0].toInt() != 0xE1 || ((answerCC[1].toInt() and 0xF0) != 0x40)) { + + } else { + throw Exception("Failed! Invalid CC read " + Util.bytesToHex(answerCC)) + } + if ((answerCC[3].toInt() and 0x01) != 0x01) { + throw Exception("Multiple block read unsupported!") + } + val areaSize = 8 * answerCC[2] + val blocksCount = areaSize / 4 + + val areaBuf = readMultipleBlocks(1, blocksCount) + + val tlvNDEF = TLVList.tryFromBytes(areaBuf) + + return NdefMessage(tlvNDEF.getTLV(TLV.Tag.TAG_CardPublicKey).Value) + } + + private fun readSingleBlock(blockNo: Int): ByteArray { + return doTransceive(0x20, blockNo)!! + } + + private fun readMultipleBlocks(startBlock: Int, blocksCount: Int): ByteArray { + val resultBuf = ByteArrayOutputStream() + val maxBlocksAtOnce = 32 + var blocksRemaining = blocksCount + var firstBlockToRead = startBlock + while (blocksRemaining > 0) { + val blocksToRead = if (blocksRemaining > maxBlocksAtOnce) { + maxBlocksAtOnce + } else { + blocksRemaining + } + val blocks = doTransceive(0x23.toByte(), firstBlockToRead, blocksToRead - 1) + blocksRemaining -= blocksToRead + firstBlockToRead += blocksToRead + resultBuf.write(blocks!!) + } + return resultBuf.toByteArray() + } + + private fun doTransceive(cmd: Byte, p1: Int?, p2: Int? = null, params: ByteArray? = null): ByteArray? { + val command: ByteArray + val res: ByteArray? + lastErrorCode = -1 + val os = ByteArrayOutputStream() + os.write(REQ_FLAG.toInt()) + os.write(cmd.toInt()) + p1?.let { os.write(p1) } + p2?.let { os.write(it) } + params?.let { os.write(params, 0, params.size) } + command = os.toByteArray() + if (!nfcReader.isConnected) throw IOException("Connection lost") + res = nfcReader.transceive(command) + lastErrorCode = res[0].toInt() + if (lastErrorCode != 0) { + throw IOException("Error! Code: " + String.format("0x%02x", lastErrorCode)) + } + return res.copyOfRange(1, res.size) + } + + companion object{ + // iso15693 flags + private const val REQ_FLAG: Byte = 0x02 + } +} + +sealed class ReadResult { + data class Success(val tlvs: TLVList) : ReadResult() + data class Failure(val exception: Exception) : ReadResult() +} From 76655ac88c9e1cbcadc13d31a63db519fa0ddb09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Jan 2020 18:10:39 +0300 Subject: [PATCH 09/26] Updated on 2026-08-14 --- .../tangem/server_android/data/LocalStorage.kt | 6 +++++- .../src/main/res/drawable/card_bc00.png | Bin 0 -> 10429 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 server-android/src/main/res/drawable/card_bc00.png diff --git a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt index 5521ab0217..a1cf90eab6 100644 --- a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt +++ b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt @@ -47,7 +47,7 @@ class LocalStorage artworks = HashMap() } - if (artworks.count() < 29) { + if (artworks.count() < 30) { // forceSave=true only on the last one putResourceArtworkToCatalog(R.drawable.card_default, false) putResourceArtworkToCatalog(R.drawable.card_default_nft, false) @@ -77,6 +77,7 @@ class LocalStorage putResourceArtworkToCatalog(R.drawable.card_ru043, false) putResourceArtworkToCatalog(R.drawable.card_tg044, false) putResourceArtworkToCatalog(R.drawable.card_tg046, false) + putResourceArtworkToCatalog(R.drawable.card_bc00, false) putResourceArtworkToCatalog(R.drawable.card_ff32, true) } if (batchesFile.exists()) { @@ -237,6 +238,9 @@ class LocalStorage hexCID in "CB25000000000000".."CB25000000099999" -> R.drawable.card_ru043 hexCID in "CB26000000000000".."CB26000000099999" -> R.drawable.card_tg044 + //Sergio's business card + hexCID in "BC00000000000000".."BC99999999999999" -> R.drawable.card_bc00 + card.batch == "0004" -> R.drawable.card_ru006 card.batch == "0006" -> R.drawable.card_ru006 card.batch == "0010" -> R.drawable.card_ru006 diff --git a/server-android/src/main/res/drawable/card_bc00.png b/server-android/src/main/res/drawable/card_bc00.png new file mode 100644 index 0000000000000000000000000000000000000000..fdf643b4e8d05f405741740b5c3e6c3c9f647447 GIT binary patch literal 10429 zcmb7qXIv9c)NYUj0tkd&q=a4tL5g%jN2E%x3P|rFO{9btdMJW`6e$YQq)JDr5d@?v zAU*V6rSmTT`~H6S)BSMwL$W(FXJ&Wj%$etT&PMBKsS*>?6M{e>Vs$koJrD>N4g%q{ zL2-c+a|^UQ@CUX>Xd*zMszjp8=Mdl;=cS&i0;pn;VI4Sstn)-)`R3-PuCDIq&!7AI z`>U&~6%`fj?d?FztE;QCv$N^x>64R_`T034c4>JTySKO3-`_tqHFa>Xe{^*8@88Md z;^N`q!P3&=!NI}F$?^5|&H4HHzkmPc=H?C$4<{xj2nh+7mzUVs*fuveKYsj(LZNVR zajUDVcXxNuX!PsXuOB^nba{F8>({T5k&&#dtnKYBK|w)UT3Q(y86_p9KY#u}AP@rs z13f*x+1Xind3j-BVH+D8Pft%vOUt;pxUsP@CMKqml9JxuUQ<(3Cnu-2wl)$H5-u(- z6%`eAb#+luQ9nPwoSdAUot=n?2ybuiva+(Oswx-^*3i(9oSfX!(n3i|xv;QMP*AYG zzJ76W(bUvLPfs5j8fs@}*U{0Tp`pRc%NraV{PykJ?(XjR_;@!rx7ym;m6eru@7`%^ zYfny2{`m3ZMTA@`2m}YID{a^K}e=Z*)1EZAIPm zcHNQOYuEc1xtgNlv0QO>CzX=|mZi%?)xDXz-C<720mr8}AGf-ePU zB;L48P}lV{R-4rET0cJ)ZB&&kHL(a2>@pfA(QlJES;4aW8OJ|1&a5-3i;OmXWwQUt zq-?mXRdZiu*)0F?Y;nL##+9zq6N7x-YcySUzaT9sko4SoBdctjD9_}+p{MOY0LxyY zHXo&@@l@fnGGaZ#Fz0e)(!wP(uIyddB&+W%9wkM;;mRO> zlY8x1bJP7&$nmL_5dRB}ch2(JmqQIr`v(6?mE*N%&gpL^lfPbC9VGnS2)oXGo_piy z-gUL1Hq#T|aGcG-zvR%}XA%`A^kvnkVQ0jl-kH4CqPG^CLBW;+q>q;{8MYJg6G z3@PY_nlZK6c@~Maycz4!4Vf=hHCZX(loRdBYG&ET$F9zA+HYt%KaFo3Tp6glUpi}& zID%`HE2(NVs@o#`d%{xtAvsKk+7UJBPVWdO95J+DUp=Vp5yI1(;eF=RQPmz7(!4CyHx7Xz>O$?vW6!si_ z#HmDCqH~tUREy<b@i~QH!DrqeM)49PC|9|9gJ%ahrX5R5n33 zCNFTW>q5)@!bu@Mjo*fUNRja|aVqJ;!j(qK`1|8{8tFP(yE}a-NoPoWm6Mc<2`3Aa z6d31)(u>-w`QLGDTZ6&`CH2!f4taqd51-F0mdNzWx!k> z23W%JappAeygss_AOy!pJai{yu%K`aM~9xZL)@LjtLy8HYSaFJkcLgZZ2tBwk|I>4RW*F4o`t0dFtAi{dOOid4>~%wv z=aqO{N0W?JUm&?NQc!Ef4;)@k$lsLxQDJ ztz*A1fMf(xZ6eUt6L8uAGg?yCUt|P&wMvf<0BcOLs9{^hku z@J3ghC-i{qcDADb(n9uK6ML*}d=H8iCdTWccdFvi@5S7+-CZqqs~cRrJhoArqP1c| zZQ~ypyj_QBOaudWkEt!zn$4GO-GV%dE^-am4K{k=XACLAaUPV&3U5=1uDw_$7vVLH;70S;! zD%d|lqR?)VCg@)+k~Vu{1@Hv9DI{lLUPPoZ20`A);W&~Yq&T-AnmgK`JGblMsgiSS z$K@s3#~eUa$7S(;>3*apc-s-;xOCvnML=#PRyLEv(nZhoE8SVDo6GbWD#C8I(EXjr zQf@s(z;9n~1ooQ>FUC=irUe0$9V_Hn=q{~AqI#yo)@fqb(_CM}BhH`Iy#L&x)ybzDHl~Q4b!;6BL4g(WT&}7`x=_8$y9*A5 z|F|^JE>|nH%54^CM~Arvu)4yqE@Hn5<310M>ODp{jirZqU6g=LW>eVym_!WMi{%rr zvQ<266t4P2VJqe-$i@;_2m#h6dxobBn(iv=(V=$vL4nzY^#nL0aDD%gMfFaGD9_5c zJ`z>c7?nQdQRzX55NLSeYdvjLZi=EJx2`|oPRE_9V=hTPdY)e|yoHzC^8d3?4tz5P3NB5mp%MJa)fAJwx&rSZLp# zFQ&eGTLh`U7CLS?g(neiyoS$Hc$bSfGEV=C38VegS&4bceiCJ&h7ck7{BcJhAwnre zxWHlc(2)?av-wBSEcLgFV7mx{a$bA3TM6IQCc`&dG>wgD&HSlQN7f9TyAkA-+? zqA8Z9Ny`DrFhvfQINadT;6>zbEf0og_Htud)Wq;is6_H>osmD+klYM(R%I&~eH5Saf4`Ch#B!0gk3%I-6 zY?Y8<#WK~ajiZw;LK1nj^`fky;JcX5Na-SS_O!?7Z5ObF77M1>dK^6Y;7E99O;n8% znW_gw%5_JAV*4x@JIpq&r4k-9rSgD0T`~3*;^fb@z4}KO5mFvKGW=PLc6@;I7^f{A zZez;!;NDI$KMH3I4#UnJmhtKcBUFwNoZ>L74EX!ZOe`u`>El8rSmL!DLP<)P&eN9C zmWhQr`6w6wDhPC^9PiS<42X%K656?ZpWg&MpArBH6Nj-2dbO1jIH8p!duhEW)1Z(} z#;;c4YCK**0RC2dEmO%oNJh6~FNon6{8!-;Fk&_`1`l+le%?xsy>vzVO1V}6wgE!L zzl59Lioez8yO69QpCnXPBnTE#*hb$JW8Rn|&M7%Yjrj#o>WoVjL|&hxj71gsN=w7C zom^>9&d4vG$-PbRSRioSMWDmj^XxG{A;&H?{LWufvdGxJh#*$#44RLHn)-RblOw8! zvPFyod?W8GE-w7?;gN4NowXa`Y%bn~Yf{g}9UsK4$n-i`LiN0Zeh}^Fk2BS@{7ieX zwHSc@@#O%HmnU5WQ!*E#jSg5WD5=0Fo7;|x9haS?qJ7MViV;AyXuE$JI`k?YU>LV9 zp0nOdGum!qxw3SJRN(WdkRY?f*jXyC+=vAFr&|uEt<{av#xfFl6w2N)uvWi6k5On3 zP+rKG{r#>Ez5z;ocsxNYcNDhKmUQ-rU{?II5#`{Z6Rx2=0-1-WjumUvNSHZ&+^VD9 zOoNn)fSb9Sr?h2ui>^CDCe_R{tTOeAuEV3P-9{_o}qz8<-B(ZU%GJ24U#$rNYWg*Y6FF0GZzGX< zLTkp|U68ixvOCaUHi;@%QR1k7yBYAP*t`t{GjDT5(vLA_<%+is_$pWxu z4aL$@$|aJuLTnj|pGpA6p--<)`e&^t0`)CWYYTw`VUNiunsJ?0^#aE+9DbAFP7nkh z{uM(HW`D-eBPtwraEe0CXC4KsucA(foE5+NNg~dP3f7X+c(_Y5spR%%0gL#B! z9o(4(^v)&KCBZl__AsDh9Ly~+gMk73gM!Ehi9*3R%c%gE0>yAcB?)oBh~(?tb!H+J zfi6vz>-xuCGE}D4Mn(nqxId6)7Am6kba>qTI@~#A3KLW_3Mpj$Nt3?^q>?8eRli6@ zAI>a^dd{8FMnCCUSzT3~J_9$-WtgGGHNY4|UlQm3YrQmh`zz}wv;GM}Z0>h`RiSRy z$7GQ21Em_@YcH2e49T~iBiH1p>NtY*5xU4NU$t*h{fGx+g_KYFvUYa%n#+6d%2A0{ z&i?fs@k>Ldn^b>JQM`pI~EtYiGV}7?SOj%11t3h9BNwim<>fcaBtSqLmsXDHVg>nP*#yI zD+M`_%1cn>z_}?mN>PQqNn8lE1jc85$p24+4JC&wfVVlfZ&4yhnxflAN zyXgzVBZjTjA#rP(_q4g*TKDnODk{WvRhac)cSU*Vp8GZ#Nz&^|!oVTg?*NYqp=WgWfGQ_41nt(wGkXY z4H)}6h*C(s_e~YZ3H?OkKTZKKhBJ7lAvx%@IVVt+I0sg)wu+`BE*aQwg~8J3=Z_= z1p_^5U;=|~=?*}LPFbj(U?AYZZ+pm(0-hcOGy_%~@GwRH5ue-PfUdge!M_V}v9~@b zPjW|o_$92?8raS0z~5biL0-V?Q2rz)=B~;k4?{;W(U5%aoO(f$q8K-&bD+TvZ?hM|oYcjwHwnYpRUB6PAVdu2I-B zP_69xONMv{>21H%0ZrW?}K)NApY7RjmG7Yd6n59xf^Rs>xaE zuhlPu_toTvM~{u2pLVSJ1~N}K8O(iO>cG}INUjx2>M)twSO^}i@5V+D2ey!l9Ijl= z$n}#)B{9kl_tl{I_(};>P(NCpkgT8m9>o862^SIWuS!0t zk6AxZ#lFgryBM%MqVRe!_|Yx`?2GtNZIhunYPH< zz6;1!$>nLAC(BdK&AZOfy*5TXr1wH2zuZMnt~+Kv>RRuzQn>cc!S~ykpJdBE5-HTv zk;|D-U7r-I*b73-R@wfWILoLls)&a^wY~49MRS}Gud4RUBk+rMNgHeE_U4PKFM{%E zn6+~Xmav#Vvm+;xQgiOcR|wxfQ;*ZVN20le6?~=DFuWNS?RhHM#rWGr_oUkkDz`?W z?Rj_4CcrFG=CIGb=RP6k%4=E^g{knwDr0EXqh5A-o2&uF5p^zB+nj6Bx+;pHmnv(H zKK6QU1i|IGL5zH+stdw&`N2!LN1NFPYIgqY^}-kWht)x!A3+I5*J(1s-Y3^@f1pibqA4t(bpjDa%yfVqg)8%wI`3 zybsq9_Bbt@2 zdHa5VB7ENRx2gd?+mC(PTQmORu(zb$ac1wYG~1reN=J zgu(rvkC9@cFXi@?OKxg zR1x=N!2elWM|V4?{mt7n_P4C=1vgRgBv-R~o%leJhGiWMoVaCO^tn>T z^1>{K$p`_|=|^U+@=MQtIEExWGtJw*>3JCSy!qrj)pylLDl(WciCe%dR5^^)B zMQ!UT@-4FgZ;qP%8_Bcq+6X|IIwI|8nj(Hv;1jfa^+O$PFtPfKX`x(Q+}6OK%G#m^ z(e}N_1)PKrQzAyvV>|cma|e*RsTEqz_v@nXO+1t8Xlifv3kXVqW2_Ve@*~-0Qs8eE zL4R{t36Kn63AF_mt-a@P@s=OWQl)|q^^M9;jn~zh#tg*g1E;w1Dt_Ib{(^Y zz?Ffm2|^}mYjq~x6Ve4oJ-TQ1@Hbp)(fqQL-t4Wbz+1{L9x2?1q24{5B)3R=e}zE^=@9{;_?JVHrPU|0E+& z1#zw$mWRh9yzqznW>A*%wOBdV%vhr{%vR_9Cjv#y73{Amx>*hJzVoR{^|0J3`WW|=S^csk$qc7o%h#YUFIP^H7NP>)K!lAlXAUcR7*$y&)k)L2k@Mz<3o zA`q9=bYl@Ig4ZQ`Hf#&CwY0sW+7d2LARGR07S<0zj6@hWl&V`Ey^lVB9(Spm+Q;E( z3NeW(Cm2yMKGC2c4uc=#cpk{^cP+i+sny1u{gkAGD-IL;ulLAK+K@8hZ ze4eFPAv;tZhz~3&>1*PP$ZX${s`);gU=$*HtoK6X7^{#L{z0LZ4u#19qZXd{0B8avaUSY|1b8 zhwZbFkY|VMqR}QVna==C=@s4{*(#0o^M+>jAR#Go;~Z)V(G+{vP&eI|PX>b1V;Np) zX$|lsO1}?y7gyDvp4!LLWQx`0e;}Cu9)GrfRcd5sZr+a`YCfFN$`{4vJ1gH5cQ>2u%)yi&2{T7}JQqa5m)M7M@{$XXSq$pxHuS zEIoG17@i7o_gcrte39i4dAM$P@hxjn!llKio?mKeWE?R+$G(n1RKGA_r;TZNs6(GX zKsP!2Rb1A{0!8$q1{>vo|Mat37M=1{j=E%pda$6I5{+_tz_Fjxl)p1=JO742dbw2+ zR8#wB|A@k#B{GleY9{6+4qa7&sC&uAINPh+^z50(bn57YFLct(7>bhm4q*0NG;Fak$t3bTknhL0;2Qw)?YH0^$N*@6!O_u20EU2? z20`#q1E3lK!f5}8=>hnC(0`kU&;erH?TEKPo-qQ1v@Hgxnmj53Mg)Qa7#sqyWPte7 zfE6`CfZz(kazVXdAq?zKiGjE+f7=z7%KG^iR{7iOkV`Ods{qVrp$4510C%XPC6b>( z6LJ@sR{#S6ZGoZYb5{ZOoreeXbxXQ)3l(7?#0U{`xFy26`O3!;n@9TMAQbZ5Rls8R= zutdU2v(W04WMCfqb50uacUxYYf(aDWA*|=M=ye9!D%bEWGm|J7*^=;MB!T zx`KP~D#onwooiLL?95%)n4zEkP3?l2F|Rv5kSL!W`{oL`H<$gHQ%Xvc#z*i?NC{)u zD(dU)mlx4;#MgE;Ur$a}KBcW;O;a_mvx~GVD=JCw6V_61NMA=hF&|m&jEbUY=Q8g6 zGsrMthgp#AtHr7}5l5W3-Nh8zst|lWQzy!nDSE-D=G zoFZCK$wflG{fdo( z=oh6$Zz1+WX>&&(n3YXw;{ox|DX6>*__BcEaP{BO+2b-H1e)lLQ$o(7od5=^+4gUJ zOkCNYJZYj1W@}D6-fb_8*rDWj%v|qqq+D8{mFgP}K_pvp=QCqx(cXpI_|H4*bIvA% zRX9uwpI(@9yyORe;PI!)-e4~@)6G=MUbwr5FE=n-sXJRV2&fdHsGOsh*MavNxhyCO zouquKlok08Vu`A^3@B4DJXqG3nQ-Uw)pEY@yMzcIN{+T(2mx_%GY-d3z*ZOBILMDd zPQZ>kAOs_0yBr8KH47%Qem)acXrcSFcEeAfQUivhWU^l1`XieUZ*3`Zf}>;M!aSqT zNkJfd$nCcPT~p)WssRNw;GM4z(-hEi;kDcD6IEh=cYh$uqNQ^pjDwi02h1dVk_ztu zk_D0h8%tQNygCwfqK}NDAb;xTiz-7j)z-0K+J^w*|CUwVox@gMpS=>2!Nty6O=%c* zcb^131^^UZh1=XoqH&lGVv4E!Qw@-^uy0*y9tEUkc9zig{Q;80JzOtz;w|6RgN|s8 ze6`MY5)?wLoT5uRJ2@sqQS?|`KvyV~KjqQnQd%;K&HuCBS)1QbHxn{KfMjpgv1?+Zph!}^mKH-q`ybPMGBi3{ zLub4V?OcwS9v&aJSDrMs$S&-cuB@oYd?#a}nDl+~rBH}b$?S$~fs!E!yQ@* zu3F($!zBb#(~M7GPX$;E8E?-&&~c*1%`?j3D=^@aIH!R}716QcM0t;!rll_3fGWKa zdO#r)kr!C(yJNI%b)vE)ppk|=eJ`i$l4lN8_4O%%U;EkABaMH00yY!@KR;%lj83;@ z>a>bCTV?I}Nx^JFy*><^RE-AhIrotHxg^h0#c1+d8oe?s%=` z!bm#!C@73Ja39=Nm8A+{EroreaBZD;zIZ;DYty>+@i|wiyjL+ynyXFnFo`XJn<#MR zv~R00NZ)|}dgHM7@ z&OtUdQ`7HrYduL#uwBn?+;28294i!)(cjrx?W;fKj~YZp$GiAm5`R%bzwmsh_~eal zb7SWlq!+~M!_MA>duDd$obmB+TRY~exWAWl9G%HID)=;I&W$93?_SWg_f_GI`B5-U zwu6I*=ofs+MA@53`07>R%SjD^>(74CR||Zx%V#5jp7o+9dbl_JhW0g+A{I@dtvxqY zMW_7r#MkNff229gt1da|7_Giu+&t8bafO;&zmyB^vq{rFOKSe}*+u3RQDCz*--g#B z^iRTB#j@eJDE1<0ceto+81EHNV{`6Qj>SfY%dX3i7m488NIYFPFP8L2N|tws53c%f zojjs7qu*l9=6i2y5Ys7=s?%W_QViFQ(O-YgMV$wAmErB{qP{LVot6lcP(7S`r(vOY zpZ{#KdF+a=KlTlYoHE@(Mx@tS$y?gsuZvgogM`PUZ||k-YVc2g(|+tfM&Lf!9UES5 zMw1}-r~Koa3f1oQ<;vMrwhh$^0u*I9ogR(v<=X4U))vtL>5D`sy@g8kFuDEVUG2um zn{wB}H{~kR6?0#HWyRY2?nUo@yE(}^;S${XozRzSr+zGYW7b>d-j#a|yL3#M+_O)Bb$=x^QSR_xM8d)!iV)^^NY+^IEih`gxyjZOK2MV0-&d zPU#!_+Y@8{j%D4oUxg}q$<&fpoo?`Tde+S7QqL{BNf2#{ACCJNILUR@}TBd2vq18tCuMh~+L9Is`Z*y!UIR zjwKDzj_+PPKB4?gGEU}10Lo?Z;k5Zh4Elc`AwzvPS%3FGvc2&WxoxGctff?;@C^As D=%N(V literal 0 HcmV?d00001 From f63c90a59100a5da7669cd0ae37d37978a851e57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Jan 2020 18:56:17 +0300 Subject: [PATCH 10/26] Updated on 2026-08-14 --- .../main/java/com/tangem/server_android/data/LocalStorage.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt index a1cf90eab6..81dc1f368a 100644 --- a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt +++ b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt @@ -47,7 +47,7 @@ class LocalStorage artworks = HashMap() } - if (artworks.count() < 30) { + if (artworks.count() < 31) { // forceSave=true only on the last one putResourceArtworkToCatalog(R.drawable.card_default, false) putResourceArtworkToCatalog(R.drawable.card_default_nft, false) @@ -70,6 +70,7 @@ class LocalStorage putResourceArtworkToCatalog(R.drawable.card_ru031, false) putResourceArtworkToCatalog(R.drawable.card_ru032, false) putResourceArtworkToCatalog(R.drawable.card_ru037, false) + putResourceArtworkToCatalog(R.drawable.card_ru038, false) putResourceArtworkToCatalog(R.drawable.card_ru039, false) putResourceArtworkToCatalog(R.drawable.card_ru040, false) putResourceArtworkToCatalog(R.drawable.card_ru041, false) From 37c68616bad4a09514fe8385a809d914cb17d46a Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 19 Jan 2020 14:37:37 +0300 Subject: [PATCH 11/26] Updated on 2026-08-14 --- .../fragment/wallet/LoadedWalletFragment.kt | 75 +++++ build.gradle | 2 +- .../tangem/tangem_card/data/TangemCard.java | 258 +++++++----------- .../tangem_card/reader/CardProtocol.java | 192 +++++++++++++ .../tangem_card/reader/ProductMask.java | 25 ++ .../tangem_card/reader/SettingsMask.java | 26 ++ .../com/tangem/tangem_card/reader/TLV.java | 75 ++++- .../tangem/tangem_card/reader/TLVList.java | 4 + .../tangem_card/tasks/CustomReadCardTask.java | 7 + .../tangem_card/tasks/VerifyCardTask.java | 4 + .../tangem_card/tasks/WriteIDCardTask.java | 36 +++ .../tangem_sdk/data/TangemCardExtensions.kt | 15 + 12 files changed, 553 insertions(+), 166 deletions(-) create mode 100644 tangem-card-old/src/main/java/com/tangem/tangem_card/reader/ProductMask.java create mode 100644 tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/WriteIDCardTask.java diff --git a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt index 31ca17f0dd..db7e155e92 100644 --- a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt @@ -30,6 +30,7 @@ import com.tangem.server_android.model.CardVerifyAndGetInfo import com.tangem.tangem_card.data.TangemCard import com.tangem.tangem_card.reader.CardProtocol import com.tangem.tangem_card.tasks.VerifyCardTask +import com.tangem.tangem_card.tasks.WriteIDCardTask import com.tangem.tangem_card.util.Util import com.tangem.tangem_sdk.android.reader.NfcReader import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD @@ -55,6 +56,8 @@ import kotlinx.android.synthetic.main.layout_tangem_card.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat import java.util.* import kotlin.concurrent.timerTask @@ -525,6 +528,19 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte else updateViews() + // TODO - remove, only for test&demo reason! + // TODO - begin + if( cardProtocol.card.isIDCard) + { + if( cardProtocol.card.hasIDCardData() ) + { + Toast.makeText(activity, "ID card: ${cardProtocol.card.idCardData.fullName}", Toast.LENGTH_SHORT).show() + }else{ + Toast.makeText(activity, "Empty ID card", Toast.LENGTH_SHORT).show() + } + } + // TODO - end + mpSecondScanSound.start() } } else { @@ -789,6 +805,65 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte val uid = tag?.id val sUID = Util.byteArrayToHexString(uid) if (ctx.card.uid != sUID || cardProtocol != null) { + + //TODO - remove, only for test + //TODO - begin + if( ctx.card.isIDCard && !ctx.card.hasIDCardData() ) + { + val idCardData=TangemCard.IDCardData() + idCardData.fullName="John Doe" + idCardData.birthday="1980-1-1" + idCardData.gender="M" + val dt= Date() + idCardData.issueDate= SimpleDateFormat ("yyyy-MM-dd", Locale.US).format(dt) + dt.year = dt.year + 10 + idCardData.expireDate=SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt) + idCardData.trustedAddress="GCOQLQT32RVH2MTWQGK5K3GIGT3STYG5323KMNTTFHIU2T5TFOGR4KDT" // ask D.Baturin + idCardData.photo=Util.hexToBytes("FFD8FFE000104A46494600010101012C012C0000FFDB0043000A07070807060A0808080B0A0A0B0E18100E0D0D0E1D15161118231F2524221F2221262B372F26293429212230413134393B3E3E3E252E4449433C48373D3E3BFFDB0043010A0B0B0E0D0E1C10101C3B2822283B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3BFFC000110800C8007903012200021101031101FFC4001C0000020203010100000000000000000000000706080103050402FFC4003F100001030301050505060406020300000001020304000511060712213141135161718114223291A115235262B1C1428292D10816333472A2175393D2E1FFC4001801000301010000000000000000000000000002030104FFC4002111000202030100020301000000000000000001021103213112132232415142FFDA000C03010002110311003F007351451400562B35A83ED29F53097505D424294807DE4839C123B8E0FCA8016BB4CD712AC97FB642B72FDF8AA12A4273C179C84A0F811BC7D45306CF768B7BB547B9425EFB0FA3793DE3BC1F10720D579D75297335C5E1D5924A64A9B19EE4FBA3F4AECECDB5B7F96AE2604E708B64B57BC4F2617C82FC8F23E87A54164FB6C771D0FBA2BE50B4AD0149214923208E44566AE219A28A2800A28A2800A28A2800A28A2800A28A2803E5440049E02ABEFF009F26C5DA0C9D44CA94B69C74B6A673C16C03809F900478D580791DAB2B6F38DF494E7CEAA9BAD165E5B4AF89B5149F307151CADAAA1A28ECEB35B0FEAB9D322381C8D31424B4B1D52B483F30720F8835C4A3248009381CBC28AE76ED9444C7496D2AEDA65B44375227C04F04B2E2B0A6C7E45741E07879533AD7B55D2B7040ED662A0B879A2520A47F50C8FAD2028A78E468571459E6F53D85E46FB77AB7A93DE24A3FBD706EBB54D2D6DCA5B98A9CE0FE18A8DE1FD4709FAD57EC03D07CAB34CF3333C967B4E5F19D47638D7561B53687C1FBB5104A48241071E55EE932E3C3614FCA7DB61A4FC4E38A0948F5355F6C1B43BBE9BB02ED36F6D8CA9E538979D05451903202797319E3DFCAB8574BD5CEF6FF006D739CF4B5E78768AC84F90E43D053FCAA8CF2588B36AFB46A0B9C8856A7952BD99016E3C9410D8C9C0009E679F2E1C2BBB4BED8EDA7D8B4A393D69C393DE2A07F227DD4FD778FAD306A916DAB6630A28A298C31466B54A694FC575943CB654B414A5C47C4824731E2290978D5FAE2C976936C977B901E8EB2927711850E8A1EEF22307D69252F26A564CEFF00B49BCE92D48FDB6E36D8F2E383DA30EB6A2D296D9E5DE091C41F2A52DD65353AEF32630D29A6A43EB752DA8E4A428938FAD6FBBDFEEB7E5B4BBACD5CA532084296948290798E0077573AB9A53B2895051582A006490078D7C7B4320E3B44FCE9526CD3651520B268ABA6A0483024DB5448CEE19C82BF54A72457A6E9B37D51698AECA760B6FB2CA0AD6A61E4A8800649C7034530B22D45789570E3EEB7F335F22E0BEADA4F91A7F8A467A47BE83E15E76A634E1C1F70F8D7A291C5AE9A9D8D7B0ED76D36BB544B6AACB2D0DC6692D02D3885E70319C1C73A6258AFE9BEB1DBB76DB8446F190A96C86F7BCB8E4D56C813E55B26B53613CA62432ADE42D38C83EB4E2D15B556EF1218B5DE582CCD748436F3292A43A7C40E293F4F2ABC277A62490C8A2B159AB8862951B68B75B0B712E224B2D5C53F765927DF79BEFC7E53D4F426999738D2E5C25330E7182EAB876C9682D491E00F0CF8F1A40EBEB55AECB75EC5ABFBD75B82944CB5BC527B33D0139E2AF0E953C9C1A3D22F5A24C8EC123032A3CBBAB7020F2391E15BA2C11739B1E0938F68790D820711BCA033F5AE68D5EC77C3970E15C2F32D31E1467A5BEAE4DB482A3F21D2A52C6C8F5A3CD05FD9496F3C92E486D27E59A7BE9ED3569D236AF6580D25A42465E7DC237DC3F894AFDB90AC2F58E986DEEC57A86D895E71BA6523FBD59E47FE5095FD10917671AA23DFE0439B6A931DB7E4210A7D1EF21033C4EF24903866AC9169B2D762520B653BBBA78E538C63E55861F664B21E8EF21D695C96DA82927D452FB5D6D43EC49E6C7608E275D490859DD2A4B4A3FC21238A95E1C878F2A46DCDD1BA479E06C32C2D2D6B9B3A649CA894A1B21B4A46780EA4FCC56ABEEC36D521ADFB24C7A23C07FA7215DA215EB8C8FAD781AD21B56D449126E17E55B82F88695254D91FC8D8C0A1CD27B58D3A3B7817D55C528E3D9A649709FE57471F4AAF99FF004CB42E351E8FBDE977B72E509686C9C21E4FBCDABC94387EF5E78A9712C80E1C9E80F414D381B4C45E62CBD2FACA10B7497DB5305F282942564606FA4F141CE0E797952CD68536B5217F12490AF31CEA7372AA66C523E69F7B33B169F896166E76B57B5497D387A4B806FA55D518FE1C1E9D79E4D2523E9BD473612AE10EC52DF86919ED529F8C0E6523991E40D31F64F11C5155CAC3770EC65283770B74B6F716D9E8A041209EE381919071D371C5A7612636EB358159AE810876D3F53BDA6748BAEC3514CD96B11E3A8734920E543C40071E24579F44681B6E9EB434E4B88D49BA3C90B92FBC80B2147894A73C80E5E278D7236C984CCD2AE3BFEDD370FBDCF2E68E7E80D320F33E751CADF068A10FB53B4C5B56AF262212DA2530878B6848012AC949C01DFBB9F9D462D17362CD798572928538DC5792E96D3CD653C401EB8A97ED817BDACDB4FE086D8FAA8D426CD6A3A8355DBACFBC52890F250B23984F3511E80D4A11F5219BA44D615A358ED764AA6CE99EC1670B3B8307B3183C908CFBE47551F9F4AF5DE767DB39D33B912F5A9673531C00E13BAA207794A50703CE9CD162B16F84DC68ACA5A61840436DA0704A40E005548BD5CA45DEF32EE129654F48754B513D38F01E4070F4AEBAA264F275B6FDB305C7BEE9DBBA6E165998DD71232DAB23805A738F250EA3A72AEF6C36CAD4D72E5A9660EDE507BB1696BE25248DE5ABCCEF019F3EFAD5616C9FF0F574F6FE2CEF3AA8FBDD3DF4EEE3F9F35AF613A8988F26769F7D610B92A1223E7F89406149F3C007D0D0074F52EDBBEC8D46F5BADF6B6E5468AE169D756E9495A81C2B7703000391939CD31B4E5FE16A7B247BAC1512D3C38A55F121438149F106AB56BBB1CAB06AFB846908210E3CA759591C16DA89208EFE783E20D3736131E533A3E53AF052587A6294CE7AE1290A23C3231E8680241AFB41C1D636A5E1B437736924C6918C1CFE051EA93F4E755CD80F34F3B0DE4143CD28A0A55CC107047A1AB7279555FD6FD89DA4DE7D9F1B9ED4BCE3F163DEFF00B66926AE2CD5D2C6DAA208369870C0C7B3B086FE490297D15B1A676EC23C61D9C5BEC52B5B63E10BC28938FF009209FE6358D9AEBF125B458AF2F80F21388D21C57FA807F0289EA0723D478F3F88529BD5FB6F6675BD41E83648A52A7D1C50A5614381EBEF2CE3BF74D47174690D8159AC0ACD748847B5B694635869D76D8EB9D93A141C61DC6771C1C8F910483E06A0D17556BDD22C2605F74BBD7669801089914925491C8920107D403DF4DAAC1AC693E8595A3575FD7A9750BD725C572265296C30E1CA91BA3183E39CD78ECD25FD35A82D1A95D654A862491BC3AEEE02C79EEAABEF51C77226A3B8B0F496E53A990BED1D6F3BAA51393CFC4E3D29A7A2B4BDBB516CB99B7DC992B69F7DD752A4F0536ADE202927A1E15CD19799146AD0C5833A2DCA135321BE87E3BC90A6DC41C850A5DDD7629619B7B76E7EDF222C57165D7632027033C4E147E11E87151FFF00C6DAEF4BBEBFF2B5F7B48CA39DD4BDD913FF00242BDD27C73515D6E35D4211A2EA6B9BCEFB58514474C90A07040C94A78733C2BA1493E13A247AF351C7BF983A0B46321D8ADAD285167E070A79241EA91CCABA919E993D0D47B227A1DAA04ED32E94DD203490E84AB74BEB1C77D27A2B3D3A8C75E732D13A12D9A420A16D35DA5C1D6C07E439C559C64A53DC9CFCFAD4AAA32C8EF43A889DB6ED3AD5706D169DA1D8D0B9118E0BEE460BC1EF5208CA4F7E39F753021EBED12DC34262DF6DECB084E10D85767BA3B827031F2AF6DE74C593502026ED6D625103016A4E163C943047CE92BB50D176BD2371B649B7B0E88127783AD29D2AC292412013C46527E9548E452D18D5135D5BB67B6468AB89A68AA7CD706EA1EECC86DB27A8CF151EE0063F4A80CDD0D3ECFA3D3A8AEFBE89F2E5A53D8B87DE4A141472AFCC4E0E3A0F3A72E9DD17A5ECA96A65A6D8D071690B4487097178232082ACE3D315AF5E69995AAACCCC18D25A8FD9BE1E5A9C04E40491818F3A9CF25E91A908BB346B5CC9E98F769CEC261CE01F4341C4A4FE6191C3C45582D17A6E1699B0B70E1A98782CEFAA4328DDEDF3C94789CF0F1C776290B61F6190B7ED53FB3684C012CCB50FF6EF0CEE927F01CEEABCC1E94E6D9DE96BE69A82A6AE972438CAC6510D037C327A90B3FA018ADC46489AD158ACD5C50A8D6BBD4A9D33A69F94850F6B7BEE62A7BD67AFA0C9F4A92D41B6A3A818B1D89296D0DAAE52829A8CB29054D24FC6B07A70C0F323BA964E91A8439254A2A512493924F326AC2ECF59EC7425A138C6F31BFF00D4A27F7AAF237411BD9DDEB8EEAB31A762A6169CB6C54AD2B0D456D2149E4AF747115C654E8D26F5AA7EDADB7D9ADABE2DB1D82549F0C9715F4A7252727FDCFF0088C8CB74E12B537BB9F16303EB54C7D62C871F3E3DF59AC0E559A98C14BADB6C44BFA25A7F1EF4798820F8282927F6A62D2EB6D935B8FA25B8C4FBF265A02478241513FA7CE9E1F9215F092E8394A99A12CAF2CE55EC88493DFBBEEFED5DD75412DA94790493F4AE0E828AB85A12CCCB80857B225641E9BDEF7EF5EBD513D36BD2F729AA38ECA32F77FE44607D48A57D3570AD8A3BCA51E849A766CAB5A7DAF6FF00B127BB99B111F74A51E2F343F529E47C307BE92206001DD5E9B7CF936AB8313E1BA5A911D616DA8743FD8F235B097960D596A6B35C4D29A9236A8B1B370630959F75E6B3C5A58E63F71E04576EBAD3B247CA88032780155B3596A273536A491394486527B28E83FC2D83C3D4F127CE9F7AC2E3F65692BA4D070A6E32820FE623753F522AB3018E1DD51CCFF43C4CD3E76617B4DDB4830C2D797E01F677013C7747141FE9E1E8690D5DDD21AAA4E93BB898D23B561C1B9219CE37D3E1DC474FFF006B9C72C6528B6C76D936BBCDA757C2202D85A1A59CF25A49520F8E4647A577646D92C0DC7DE8F1273CEE3820A12819F1564FE94B2D57AB2E1ABA6076684B6C3790CC647C280799F127BE9E32A7663563CB4BEAEB5EACB737260BE80F1482EC62A1DA34AEA08EEEE3C8D77307B8FCAAA608AA69C0E47794D287220E08F222BD2A9B7B5A77177896A477190B23E59A6A8BE3336593BDEA8B2E9D8EA76E97165820706F7B79C578048E26945DA4FDB16B76886571EC9031BC09E286C9C9C9FC6BC63872C7852FC420A5153AE2964F3F1F5A9468BD52EE8FBA990D33DAC6753B8FB20E37873041EF1FDE8B8C79D0A6CB148425B42508484A523000E400E94ABDAF6A76D486F4EC570294141D9641F871F0A3CFA9F4AF8BFED8FB786A62C509D61D58C191237728FF008A46727C4FCA960E38E3CEADD756A71C5A8A94B51C9513CC935318F9A28A2B00636CD6DFAAECD3A3DCE2DB1C91699C901EDD751EF273C16015734FE9914EAE3E1503D8F5C7DAF46988A56550A42DBC7E557BC3F53F2A9ED76415224FA2F76CD70F66D28C4249C2A6494823BD281BC7EBBB490A636DA6E3DBEA285012ACA62C6DF50EE52CFF00648A5CD73E4772291E05145153342B5B921B68E16AC1EEC56CAF9536851CA90924778AD55FB035A65B0A380B03CC62B766B52E3B2B182D8F4E15AA3EF32FA982494E32934F49AD196CF551451533428A28A0028A283C050032B62971EC6F770B7295812180EA47E641C1FA2BE94E7CD577D1E6569BD79685CB41692FA90013F0ADB75380A07A8E23E5561BDEEEAEAC4FEB44E5D14ACE8E7B5DEB6B85F26A94DD9848DC694382A4A5184809EE4F0E2AF9782BA404894E840C2438A091DC327156A5C09663AB7404A508380060000555351DE515779CD4F2248D8B31451454470A28A2800A31451400514514005145140051451400E6D2F6585ADB6690187CF6732DEA5B6C4948CA995A5594F98C6EE454DF7F50FF00E9B77FF22FFF00AD2F762170CB374B6A95F0A90FA0798293FA0A6BD76436AC93E9E4B9AFB3B5CB5FE16167FEA6AAEC3892273A962334A75D282ADD4F3C049513E8013566F512FB3D37735E71BB0DD3FF0043506D9368F4C1B3AAF539A1ED13DBDD692A1F0327F7573F2C52CE3E9A46A7484C03919A2BD57582AB65DA64058C18CFADAF92881F4C5796B95940A28A2800A28A2800A28A2800A28A2803D56CB6C8BBDC1B811021521EC86D2B504EF9033BA09E1938E15F1360CBB74A5459B19D8CFA3E26DD4949AF88D25E8729A951D5B8F32B0E36A1D140E47E95635B8D65D75A6A24B9B09A92CC9682D3BC3DE6D5D40573041C8E1DD548414918DD0A2D935C3D8B5CB0D156133195B27CF1BC3EA9FAD3F37877D2AA4ECAE6586FB0EF1A7A47B5371A421D319E2038003C40572570CF3C7AD357753DD57C69A54C9B354B8ACCE88EC5909DF65E414389CFC493C08AD8942508094A425206001C80AFBAC550C2BFED56DDEC1AEA4B813BA898DA1F1E646E9FAA6A1D4DADB75BB2D5AEE694FC2A5C759F31BC9FD15503D21A4A66ADBA88AC65B8CDE15224638369F0EF51E83F6AE49C7ED48A27A38C224930CCC0C39ECC970345EDD3BA16464273DF8AD5565D5A4ED074C2B4EA6304C12DEE6E8F8B3F8F3F8B3C73DF55EF50D865E9BBD3F6C9832A6CE50B0301C41E4A1E7F439144F1F904ECE6515BE14291719CCC288D175F7D610DA075269FB6ED9D5958D22DD866C74C827EF1D7C7059748E2A49E631C8780AC841C8D6E8AF4738E0327A0A91EAAD1772D2A63BB2125D8B21092979238256464A15DC471C778AEF3BB31B95A758DB23A92A976C7E52712529F8520EF10B1D0E0791FA53A2E16F897582EC29CC25F8EF2775685F223F63E34F1C769D8AE4557A9AECE34546D5AEDC173CBA88CC361085B6AC10E2B883E3800F0F115E4D73A1A56919BDA2379FB6BCAFB97C8E293F817DC7C7AD37B67164FB1346C36DC4EEBF247B43BDF957103D13814421F6A66B7A14FAAB66B7AD39BF21A419F053C7B7653EF207E74F31E6323CAA5BB16BF769165D89D5E4B47DA18C9FE13C163D0E0FF0035353151D73455ADBD431EFB6F47B0CD6944B8591843E9230A0A4F2E3DE3AF7D51429DA16F448AB358159AA8A1451450047B5BE9B56A9D3ABB6B4E21A77B66DC42D7C9385713FD2555EDD3F6083A6ED4D5BA037BADA38A967E2715D54A3D49A28ACA5760752A21B43D1C9D5566DF8E948B8C50551D5CB7FBD04F71E9DC71E34514356A80E1ECA345AADB14DFAE2C944B7D2531DB58C169BEA48E855FA79D32A8A2B22A91AC3145145318689B062DC62391263087D874616DAC642856F4A424000600E94514019A28A2800A28A2803FFFD9") + + val writeTask=WriteIDCardTask(ctx.card, cardProtocol!!.reader,App.localStorage, App.pinStorage, idCardData, ctx.card.issuer.privateDataKey, + object: CardProtocol.Notifications{ + override fun onReadProgress(cardProtocol: CardProtocol?, progress: Int) { + LOG.i("WriteIDCardTask","onReadProgress: ${progress}%") + } + override fun onReadWait(msec: Int) { + } + override fun onReadStart(cardProtocol: CardProtocol?) { + LOG.i("WriteIDCardTask","onReadStart") + rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE } + + } + override fun onReadCancel() { + LOG.i("WriteIDCardTask","onReadCancel") + rlProgressBar?.postDelayed({ + try { + rlProgressBar?.visibility = View.GONE + } catch (e: Exception) { + e.printStackTrace() + } + }, 500) + + } + override fun onReadFinish(cardProtocol: CardProtocol?) { + LOG.i("WriteIDCardTask","onReadFinish") + rlProgressBar?.postDelayed({ + try { + rlProgressBar?.visibility = View.GONE + } catch (e: Exception) { + e.printStackTrace() + } + }, 500) + + } + override fun onReadAfterRequest() { + } + override fun onReadBeforeRequest(timeout: Int) { + } + }) + writeTask.start() + return + } + //TODO end (activity as MainActivity).nfcManager.ignoreTag(isoDep.tag) return } diff --git a/build.gradle b/build.gradle index c4edca4c38..dde59cbded 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.60' + ext.kotlin_version = '1.3.61' repositories { google() jcenter() diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java index 8fe19575d6..0fad9c1874 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java @@ -1,9 +1,14 @@ package com.tangem.tangem_card.data; import com.tangem.tangem_card.reader.CardProtocol; +import com.tangem.tangem_card.reader.ProductMask; import com.tangem.tangem_card.reader.SettingsMask; +import com.tangem.tangem_card.reader.TLV; +import com.tangem.tangem_card.reader.TLVList; import com.tangem.tangem_card.util.Util; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; @@ -288,6 +293,28 @@ public class TangemCard { return ""; } + private byte[] issuerDataEx; + private byte[] issuerDataExSignature; + private int issuerDataExCounter; + + public byte[] getIssuerDataEx() { + return issuerDataEx; + } + + public byte[] getIssuerDataExSignature() { + return issuerDataExSignature; + } + + public int getIssuerDataExCounter() { + return issuerDataExCounter; + } + + public void setIssuerDataEx(byte[] value, byte[] signature, int counter) { + issuerDataEx = value; + issuerDataExSignature = signature; + issuerDataExCounter = counter; + } + private int pauseBeforePIN2 = 0; public void setPauseBeforePIN2(int value) { @@ -579,6 +606,79 @@ public class TangemCard { this.UID = UID; } + public int productMask = ProductMask.Note; + + public void setProductMask(int productMask) { + this.productMask = productMask; + } + + public boolean isIDCard() { + try { + return (productMask & ProductMask.IdCard) != 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + TLVList tlvIDCardData = null; + + public boolean hasIDCardData() { + if (tlvIDCardData == null) { + if (issuerDataEx == null) return false; + try { + tlvIDCardData = TLVList.fromBytes(issuerDataEx); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + return tlvIDCardData.hasTag(TLV.Tag.TAG_FullName) && tlvIDCardData.hasTag(TLV.Tag.TAG_Birthday) && tlvIDCardData.hasTag(TLV.Tag.TAG_Gender) && tlvIDCardData.hasTag(TLV.Tag.TAG_Photo) + && tlvIDCardData.hasTag(TLV.Tag.TAG_IssueDate) && tlvIDCardData.hasTag(TLV.Tag.TAG_ExpireDate) && tlvIDCardData.hasTag(TLV.Tag.TAG_TrustedAddress); + } + + public static class IDCardData + { + public String fullName; + public String birthday; + public String gender; + public byte[] photo; + public String issueDate; + public String expireDate; + public String trustedAddress; + + public TLVList toTLVList() { + TLVList tlvIDCardData=new TLVList(); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_FullName,fullName.getBytes(StandardCharsets.UTF_8))); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_Birthday,birthday.getBytes(StandardCharsets.UTF_8))); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_Gender, gender.getBytes(StandardCharsets.UTF_8))); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_Photo, photo)); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_IssueDate,issueDate.getBytes(StandardCharsets.UTF_8))); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_ExpireDate,expireDate.getBytes(StandardCharsets.UTF_8))); + tlvIDCardData.add(new TLV(TLV.Tag.TAG_TrustedAddress,trustedAddress.getBytes(StandardCharsets.UTF_8))); + return tlvIDCardData; + } + + public static IDCardData fromTLVList(TLVList tlvIDCardData) + { + IDCardData result=new IDCardData(); + result.fullName=tlvIDCardData.getTLV(TLV.Tag.TAG_FullName).getAsString(); + result.birthday=tlvIDCardData.getTLV(TLV.Tag.TAG_Birthday).getAsString(); + result.gender=tlvIDCardData.getTLV(TLV.Tag.TAG_Gender).getAsString(); + result.photo=tlvIDCardData.getTLV(TLV.Tag.TAG_Photo).Value; + result.issueDate=tlvIDCardData.getTLV(TLV.Tag.TAG_IssueDate).getAsString(); + result.expireDate=tlvIDCardData.getTLV(TLV.Tag.TAG_ExpireDate).getAsString(); + result.trustedAddress=tlvIDCardData.getTLV(TLV.Tag.TAG_TrustedAddress).getAsString(); + return result; + } + } + + public IDCardData getIDCardData() + { + if( !hasIDCardData() ) return null; + return IDCardData.fromTLVList(tlvIDCardData); + } private byte[] offlineBalance; @@ -638,164 +738,6 @@ public class TangemCard { this.tagSignature = tagSignature; } -// public Bundle getAsBundle() { -// Bundle B = new Bundle(); -// saveToBundle(B); -// return B; -// } -// -// public void saveToBundle(Bundle B) { -// try { -// B.putString("UID", UID); -// B.putByteArray("CID", CID); -// B.putString("PIN", PIN); -// B.putString("PIN2", PIN2.name()); -// B.putString("Status", status.name()); -// B.putString("Blockchain", blockchainID); -// B.putString("BlockchainName", blockchainName); -// B.putInt("TokensDecimal", tokensDecimal); -// B.putString("TokenSymbol", tokenSymbol); -// B.putString("ContractAddress", contractAddress); -// if (dtPersonalization != null) B.putLong("dtPersonalization", dtPersonalization.getTime()); -// B.putInt("RemainingSignatures", remainingSignatures); -// B.putInt("MaxSignatures", maxSignatures); -// B.putInt("Health", health); -// if (settingsMask != null) B.putInt("settingsMask", settingsMask); -// B.putInt("pauseBeforePIN2", pauseBeforePIN2); -// if (signingMethod != null) B.putString("signingMethod", signingMethod.name()); -// if (manufacturer != null) B.putString("Manufacturer", manufacturer.name()); -// if (encryptionMode != null) B.putString("EncryptionMode", encryptionMode.name()); -// if (issuer != null) B.putString("Issuer", issuer.getID()); -// if (issuerPublicDataKey != null) B.putByteArray("IssuerPublicDataKey", issuerPublicDataKey); -// if (firmwareVersion != null) B.putString("FirmwareVersion", firmwareVersion); -// if (batch != null) B.putString("Batch", batch); -// B.putBoolean("ManufacturerConfirmed", manufacturerConfirmed); -// B.putBoolean("CardPublicKeyValid", isCardPublicKeyValid()); -// B.putByteArray("CardPublicKey", getCardPublicKey()); -// -// B.putInt("SignedHashes", getSignedHashes()); -// B.putBoolean("WalletPublicKeyValid", isWalletPublicKeyValid()); -// if (pbWalletKey != null) -// B.putByteArray("PublicKey", pbWalletKey); -// if (pbWalletKeyRar != null) -// B.putByteArray("PublicKeyRar", pbWalletKeyRar); -// -// if (getOfflineBalance() != null) B.putByteArray("OfflineBalance", getOfflineBalance()); -// -// if (getDenomination() != null) B.putByteArray("Denomination", getDenomination()); -// if (getDenominationText() != null) B.putString("DenominationText", getDenominationText()); -// -// if (getIssuerData() != null && getIssuerDataSignature() != null) { -// B.putByteArray("IssuerData", getIssuerData()); -// B.putByteArray("IssuerDataSignature", getIssuerDataSignature()); -// B.putBoolean("NeedWriteIssuerData", getNeedWriteIssuerData()); -// } -// -// if (codeConfirmed != null) -// B.putBoolean("codeConfirmed", codeConfirmed); -// -// if (codeConfirmed != null) -// B.putBoolean("codeConfirmed", codeConfirmed); -// -// if (onlineVerified != null) -// B.putBoolean("onlineVerified", onlineVerified); -// -// if (onlineValidated != null) -// B.putBoolean("onlineValidated", onlineValidated); -// -// if (codeConfirmed != null) -// B.putBoolean("codeConfirmed", codeConfirmed); -// -// if (codeConfirmed != null) -// B.putBoolean("codeConfirmed", codeConfirmed); -// -// if (onlineVerified != null) -// B.putBoolean("onlineVerified", onlineVerified); -// -// if (onlineValidated != null) -// B.putBoolean("onlineValidated", onlineValidated); -// } catch (Exception e) { -// Log.e("Can't save to bundle ", e.getMessage()); -// } -// -// } -// -// public void loadFromBundle(Bundle B) { -// UID = B.getString("UID"); -// CID = B.getByteArray("CID"); -// PIN = B.getString("PIN"); -// PIN2 = PIN2_Mode.valueOf(B.getString("PIN2")); -// status = Status.valueOf(B.getString("Status")); -// blockchainID = B.getString("Blockchain"); -// tokensDecimal = B.getInt("TokensDecimal", 18); -// tokenSymbol = B.getString("TokenSymbol", ""); -// contractAddress = B.getString("ContractAddress", ""); -// if (B.containsKey("BlockchainName")) -// blockchainName = B.getString("BlockchainName", ""); -// if (B.containsKey("dtPersonalization")) { -// dtPersonalization = new Date(B.getLong("dtPersonalization")); -// } -// remainingSignatures = B.getInt("RemainingSignatures"); -// maxSignatures = B.getInt("MaxSignatures"); -// health = B.getInt("health"); -// if (B.containsKey("settingsMask")) settingsMask = B.getInt("settingsMask"); -// pauseBeforePIN2 = B.getInt("pauseBeforePIN2"); -// if (B.containsKey("signingMethod")) -// signingMethod = SigningMethod.valueOf(B.getString("signingMethod")); -// if (B.containsKey("Manufacturer")) -// manufacturer = Manufacturer.valueOf(B.getString("Manufacturer")); -// manufacturerConfirmed = B.getBoolean("ManufacturerConfirmed"); -// if (B.containsKey("EncryptionMode")) -// encryptionMode = EncryptionMode.valueOf(B.getString("EncryptionMode")); -// else -// encryptionMode = null; -// -// if (B.containsKey("SignedHashes")) setSignedHashes(B.getInt("SignedHashes")); -// -// if (B.containsKey("Issuer")) issuer = Issuer.FindIssuer(B.getString("Issuer")); -// if (B.containsKey("IssuerPublicDataKey")) -// issuerPublicDataKey = B.getByteArray("IssuerPublicDataKey"); -// -// if (B.containsKey("FirmwareVersion")) firmwareVersion = B.getString("FirmwareVersion"); -// if (B.containsKey("Batch")) batch = B.getString("Batch"); -// -// cardPublicKeyValid = B.getBoolean("CardPublicKeyValid"); -// if (B.containsKey("CardPublicKey")) setCardPublicKey(B.getByteArray("CardPublicKey")); -// -// if (B.containsKey("OfflineBalance")) setOfflineBalance(B.getByteArray("OfflineBalance")); -// else clearOfflineBalance(); -// -// if (B.containsKey("Denomination") && B.containsKey("DenominationText")) { -// setDenomination(B.getByteArray("Denomination"), B.getString("DenominationText")); -// } else if (B.containsKey("Denomination")) { -// setDenomination(B.getByteArray("Denomination"), "N/A"); -// } else clearDenomination(); -// -// if (B.containsKey("IssuerData") && B.containsKey("IssuerDataSignature")) -// setIssuerData(B.getByteArray("IssuerData"), B.getByteArray("IssuerDataSignature")); -// else setIssuerData(null, null); -// -// if (B.containsKey("NeedWriteIssuerData")) -// setNeedWriteIssuerData(B.getBoolean("NeedWriteIssuerData")); -// -// walletPublicKeyValid = B.getBoolean("WalletPublicKeyValid"); -// if (B.containsKey("PublicKey")) { -// pbWalletKey = B.getByteArray("PublicKey"); -// } -// if (B.containsKey("PublicKeyRar")) { -// pbWalletKeyRar = B.getByteArray("PublicKeyRar"); -// } -// -// if (B.containsKey("codeConfirmed")) -// codeConfirmed = B.getBoolean("codeConfirmed"); -// -// if (B.containsKey("onlineVerified")) -// onlineVerified = B.getBoolean("onlineVerified"); -// -// if (B.containsKey("onlineValidated")) -// onlineValidated = B.getBoolean("onlineValidated"); -// } - public enum EncryptionMode { None((byte) 0x0), Fast((byte) 0x1), Strong((byte) 0x2); diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/CardProtocol.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/CardProtocol.java index 94e1a7b799..518318a219 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/CardProtocol.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/CardProtocol.java @@ -1,5 +1,6 @@ package com.tangem.tangem_card.reader; +import com.tangem.tangem_card.data.Issuer; import com.tangem.tangem_card.data.Manufacturer; import com.tangem.tangem_card.data.TangemCard; import com.tangem.tangem_card.util.Log; @@ -17,6 +18,7 @@ import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; import javax.crypto.KeyAgreement; @@ -99,6 +101,11 @@ public class CardProtocol { private NfcReader mIsoDep; + public NfcReader getReader() + { + return mIsoDep; + } + public static final String DefaultPIN = "000000"; public static final String DefaultPIN2 = "000"; @@ -1095,6 +1102,12 @@ public class CardProtocol { if (issuerData == null || issuerDataSignature == null) throw new TangemException("Invalid answer format (GetIssuerData)"); + if( issuerData.Value.length==0 && issuerDataSignature.Value.length==0 ) + { + mCard.setIssuerData(null, null); + return new TLVList(); + } + ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream(); bsDataToVerify.write(mCard.getCID()); bsDataToVerify.write(issuerData.Value); @@ -1117,6 +1130,185 @@ public class CardProtocol { } } + /** + * Series of GET_ISSUER_DATA command to read extra data and verify issuer signature of returned extra data + * See [1] 3.3, 8.7 + * This command returns Issuer_Extra_Data data block and its issuer’s signature. + * + * @return TLVList with issuerData (if success read and verify) + * @throws Exception - if something went wrong + */ + public TLVList run_GetIssuerDataEx(Notifications notifications) throws Exception { + boolean needReadIssuerDataCounter = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0; + TLV issuerDataSignature = null; + TLV issuerDataCounter = null; + TLV issuerDataSize = null; + ByteArrayOutputStream bsIssuerDataEx = new ByteArrayOutputStream(); + do { + + CommandApdu rqApdu = StartPrepareCommand(INS.GetIssuerData); + rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x01); + rqApdu.addTLV_U16(TLV.Tag.TAG_Offset, bsIssuerDataEx.size()); + + Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" "))); + + ResponseApdu rspApdu = SendAndReceive(rqApdu, false); + + if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) { + Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" "))); + TLV issuerData = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data); + if (issuerData != null && issuerData.Value != null) + bsIssuerDataEx.write(issuerData.Value); + issuerDataSignature = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Signature); + issuerDataCounter = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Counter); + if (issuerDataSize == null) + issuerDataSize = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Size); + } else { + throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2())); + } + if (issuerDataSize != null && issuerDataSize.getAsInt() != 0) { + if (notifications != null) + notifications.onReadProgress(this, (int) Math.round(bsIssuerDataEx.size() * 100.0 / issuerDataSize.getAsInt())); + Log.i(logTag, String.format("Read %d/%d bytes", bsIssuerDataEx.size(), issuerDataSize.getAsInt())); + } + + } while ((issuerDataSignature == null) || (needReadIssuerDataCounter && (issuerDataCounter == null))); + boolean protectIssuerDataAgainstReplay = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0; + + if (notifications != null) notifications.onReadProgress(this, 100); + + if (bsIssuerDataEx.size() > 0 && issuerDataSignature != null && issuerDataSignature.Value != null) { + ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream(); + bsDataToVerify.write(mCard.getCID()); + bsDataToVerify.write(bsIssuerDataEx.toByteArray()); + if (protectIssuerDataAgainstReplay) { + bsDataToVerify.write(issuerDataCounter.Value); + } + try { + if (CardCrypto.VerifySignature(mCard.getIssuerPublicDataKey(), bsDataToVerify.toByteArray(), issuerDataSignature.Value)) { + mCard.setIssuerDataEx(bsIssuerDataEx.toByteArray(), issuerDataSignature.Value, issuerDataCounter.getAsInt()); + return TLVList.fromBytes(bsIssuerDataEx.toByteArray()); + } else { + throw new TangemException("Invalid issuer data read (signature verification failed)"); + } + } catch (Exception e) { + e.printStackTrace(); + throw new TangemException("Invalid issuer data read"); + } + } else { + mCard.setIssuerDataEx(null, null, issuerDataCounter.getAsInt()); + return null; + } + } + + /** + * Series of WRITE_ISSUER_DATA command to write extra data: + * 1. start write procedure + * 2. series of writing data blocks + * 3. finalize write procedure + * This command make signatures with provided issuer privateDataKey + * - to start write procedure (sign CID|Counter|Size) + * - to finalize write procedure (sign CID|dataEx|Counter) + * See [1] 3.3, 8.8 + * + * @throws Exception - if something went wrong + */ + public void run_WriteIssuerDataEx(Notifications notifications, byte[] dataEx, byte[] issuerPrivateDataKey) throws Exception { + boolean needCounter = readResult.getTLV(TLV.Tag.TAG_SettingsMask) != null && (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0; + + Log.i(logTag, "WriteIssuerDataEx - Start"); + + int issuerDataCounter = 0; + ByteArrayOutputStream osDataToSign = new ByteArrayOutputStream(); + osDataToSign.write(mCard.getCID()); + if (needCounter) { + issuerDataCounter = mCard.getIssuerDataExCounter() + 1; + osDataToSign.write(Util.intToByteArray4(issuerDataCounter)); + } + osDataToSign.write(Util.intToByteArray2(dataEx.length)); + + byte[] issuerDataSignature = CardCrypto.Signature(issuerPrivateDataKey, osDataToSign.toByteArray()); + + CommandApdu rqApdu = StartPrepareCommand(INS.WriteIssuerData); + rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x01); + if (needCounter) { + rqApdu.addTLV_U32(TLV.Tag.TAG_Issuer_Data_Counter, issuerDataCounter); + } + rqApdu.addTLV_U16(TLV.Tag.TAG_Size, dataEx.length); + rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature); + + Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" "))); + + ResponseApdu rspApdu = SendAndReceive(rqApdu, true); + + if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) { + Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" "))); + } else { + throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2())); + } + + if (notifications != null) + notifications.onReadProgress(this, 10); + + + Log.i(logTag, "WriteIssuerDataEx - Send data"); + int offset = 0; + int partSize = 1524; + while (offset < dataEx.length) { + + if (dataEx.length - offset < partSize) { + partSize = dataEx.length - offset; + } + byte[] part = Arrays.copyOfRange(dataEx, offset, offset + partSize); + + rqApdu = StartPrepareCommand(INS.WriteIssuerData); + rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x02); + rqApdu.addTLV_U16(TLV.Tag.TAG_Offset, offset); + rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data, part); + + Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" "))); + + rspApdu = SendAndReceive(rqApdu, true); + + if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) { + Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" "))); + } else { + throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2())); + } + + offset += partSize; + if (notifications != null) + notifications.onReadProgress(this, 10+(int) Math.round(offset * 80.0 / dataEx.length)); + } + + Log.i(logTag, "WriteIssuerDataEx - Finalize"); + + osDataToSign = new ByteArrayOutputStream(); + osDataToSign.write(mCard.getCID()); + osDataToSign.write(dataEx); + if (needCounter) { + osDataToSign.write(Util.intToByteArray4(issuerDataCounter)); + } + issuerDataSignature = CardCrypto.Signature(issuerPrivateDataKey, osDataToSign.toByteArray()); + + rqApdu = StartPrepareCommand(INS.WriteIssuerData); + rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x03); + rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature); + + Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" "))); + + rspApdu = SendAndReceive(rqApdu, true); + + if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) { + Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" "))); + } else { + throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2())); + } + if (notifications != null) + notifications.onReadProgress(this, 100); + } + + /** * Execute consecutive READ commands with increasing encryption level from EncryptionMode.None to EncryptionMode.Strong, see {@link TangemCard.EncryptionMode} * If card requires stricter encryption level it returns SW.NEED_ENCRYPTION status word diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/ProductMask.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/ProductMask.java new file mode 100644 index 0000000000..15516edaab --- /dev/null +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/ProductMask.java @@ -0,0 +1,25 @@ +package com.tangem.tangem_card.reader; + +/** + * Created by dvol on 15.07.2019. + */ + +public class ProductMask { + public static final int Note = 0x0001; + public static final int Tag = 0x0002; + public static final int IdCard = 0x0004; + + public static String getDescription(int iValue) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + if ((iValue & ProductMask.Note) != 0) sb.append("Note, "); + if ((iValue & ProductMask.Tag) != 0) + sb.append("Tag, "); + if ((iValue & ProductMask.IdCard) != 0) + sb.append("IdCard, "); + + if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/SettingsMask.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/SettingsMask.java index 81470d9586..0513936d3d 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/SettingsMask.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/SettingsMask.java @@ -29,8 +29,16 @@ public class SettingsMask { public static final int DisablePrecomputedNDEF = 0x00010000; + public static final int SkipSecurityDelayIfValidatedByIssuer = 0x00020000; + public static final int SkipCheckPIN2andCVCIfValidatedByIssuer = 0x00040000; public static final int SkipSecurityDelayIfValidatedByLinkedTerminal = 0x00080000; + public static final int RestrictOverwriteIssuerDataEx = 0x00100000; + + public static final int RequireTermTxSignature = 0x01000000; + public static final int RequireTermCertSignature = 0x02000000; + public static final int CheckPIN3onCard = 0x04000000; + public static String getDescription(int iValue) { StringBuilder sb=new StringBuilder(); sb.append("["); @@ -63,6 +71,24 @@ public class SettingsMask { if ((iValue & SettingsMask.SkipSecurityDelayIfValidatedByLinkedTerminal) != 0) sb.append("SkipSecurityDelayIfValidatedByLinkedTerminal, "); + + if ((iValue & SettingsMask.SkipSecurityDelayIfValidatedByIssuer) != 0) + sb.append("SkipSecurityDelayIfValidatedByIssuer, "); + if ((iValue & SettingsMask.SkipCheckPIN2andCVCIfValidatedByIssuer) != 0) + sb.append("SkipCheckPIN2andCVCIfValidatedByIssuer, "); + + if ((iValue & SettingsMask.RestrictOverwriteIssuerDataEx) != 0) + sb.append("RestrictOverwriteIssuerDataEx, "); + + if ((iValue & SettingsMask.RequireTermTxSignature) != 0) + sb.append("RequireTermTxSignature, "); + + if ((iValue & SettingsMask.RequireTermCertSignature) != 0) + sb.append("RequireTermCertSignature, "); + + if ((iValue & SettingsMask.CheckPIN3onCard) != 0) + sb.append("CheckPIN3onCard, "); + if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length()); sb.append("]"); return sb.toString(); diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java index 4188e0ca54..4e704fe1f0 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java @@ -44,15 +44,27 @@ public class TLV { TAG_Session_Key_B(0x1B), TAG_Pause(0x1C), + TAG_PIN3(0x1D), + TAG_NewPIN3(0x1E), + TAG_CrEx_Key(0x1F), + TAG_Manufacture_ID(0x20), TAG_Manufacturer_Signature(0x21), + TAG_Mode(0x23), + TAG_Offset(0x24), + TAG_Size(0x25), + + TAG_User_Data(0x2A), + TAG_User_ProtectedData(0x2B), + TAG_Issuer_Data_PublicKey(0x30), TAG_Issuer_Transaction_PublicKey(0x31), TAG_Issuer_Data(0x32), TAG_Issuer_Data_Signature(0x33), TAG_Issuer_Transaction_Signature(0x34), TAG_Issuer_Data_Counter(0x35), + TAG_Acquirer_PublicKey(0x37), TAG_IsActivated(0x3A), TAG_ActivationSeed(0x3B), @@ -65,12 +77,38 @@ public class TLV { TAG_TrOut_Hash(0x50), TAG_TrOut_HashSize(0x51), TAG_TrOut_Raw(0x52), + TAG_TrOut_Amount(0x53), + + TAG_Terminal_PaymentFlowVersion(0x54), + TAG_Terminal_Certificate(0x55), + // hash of extra data to transaction (for SIGN_HASH_EXTERNAL) + TAG_TrOut_Extra_Hash(0x56), + TAG_Terminal_Transaction_Signature(0x57), + TAG_Terminal_Certificate_Id(0x5A), + TAG_Terminal_Certificate_Param(0x5B), + TAG_Terminal_Certificate_PublicKey(0x5C), + TAG_Terminal_Certificate_Signature(0x5D), TAG_Wallet_PublicKey(0x60), TAG_Signature(0x61), TAG_RemainingSignatures(0x62), TAG_SignedHashes(0x63), + TAG_CheckWalletCounter(0x64), + + TAG_CrEx_Salt(0x65), + TAG_CrEx_Message(0x66), + TAG_CrEx_CryptedMessage(0x67), + TAG_CrEx_HMAC(0x68), + + TAG_CrEx_RequirePIN3(0x69), + TAG_TrOut_Extra_Signature(0x6A), + TAG_SignCommand_ChecksPassed(0x6B), + + TAG_Wallet_PrivateKey(0x70), + TAG_Card_PrivateKey(0x71), + TAG_Block_Reason(0x72), + TAG_Firmware(0x80), TAG_Batch(0x81), TAG_ManufactureDateTime(0x82), @@ -79,6 +117,10 @@ public class TLV { TAG_Manufacturer_PublicKey(0x85), TAG_CardID_Manufacturer_Signature(0x86), + TAG_ProductMask(0x8A), + + TAG_PinLessFloorLimit(0x90), + TAG_Token_Symbol(0xA0), TAG_Token_Contract_Address(0xA1), TAG_Token_Decimal(0xA2), @@ -87,6 +129,14 @@ public class TLV { TAG_LastSign_Date(0xC2), TAG_DenominationText(0xC3), + TAG_FullName (0xD0), + TAG_Birthday (0xD1), + TAG_Photo (0xD2), + TAG_Gender(0xD3), + TAG_IssueDate (0xD4), + TAG_ExpireDate(0xD5), + TAG_TrustedAddress (0xD6), + TAG_Terminal_IsLinked(0x58), TAG_Terminal_PublicKey(0x5C), TAG_Terminal_TransactionSignature(0x57); @@ -172,7 +222,7 @@ public class TLV { } public String getAsString() { - if( Value.length==0 ) return ""; + if (Value.length == 0) return ""; if (Value[Value.length - 1] == 0) { String s1 = new String(Arrays.copyOfRange(Value, 0, Value.length - 1), Charset.forName("utf-8")); @@ -214,18 +264,29 @@ public class TLV { return String.format("%s[]: [[NULL]]", tag.name()); } case TAG_SettingsMask: { - StringBuilder sb=new StringBuilder(); - if( Value!=null ) { + StringBuilder sb = new StringBuilder(); + if (Value != null) { try { int iValue = Util.byteArrayToInt(Value); return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), SettingsMask.getDescription(iValue)); - } - catch (Exception e) - { + } catch (Exception e) { e.printStackTrace(); return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value)); } - }else{ + } else { + return String.format("%s[]: [[NULL]]", tag.name()); + } + } + case TAG_ProductMask: { + if (Value != null) { + try { + int iValue = Util.byteArrayToInt(Value); + return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), ProductMask.getDescription(iValue)); + } catch (Exception e) { + e.printStackTrace(); + return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value)); + } + } else { return String.format("%s[]: [[NULL]]", tag.name()); } } diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java index 40c36a777b..8f7ef8875f 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java @@ -85,4 +85,8 @@ public class TLVList extends ArrayList { while (tlv != null); return tlvList; } + + public boolean hasTag(TLV.Tag tag) { + return getTLV(tag)!=null; + } } diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/CustomReadCardTask.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/CustomReadCardTask.java index 453ecd2b28..d0f5274e93 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/CustomReadCardTask.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/CustomReadCardTask.java @@ -7,6 +7,7 @@ import com.tangem.tangem_card.data.external.PINsProvider; import com.tangem.tangem_card.reader.CardCrypto; import com.tangem.tangem_card.reader.CardProtocol; import com.tangem.tangem_card.reader.NfcReader; +import com.tangem.tangem_card.reader.ProductMask; import com.tangem.tangem_card.reader.TLV; import com.tangem.tangem_card.reader.TLVException; import com.tangem.tangem_card.reader.TLVList; @@ -137,6 +138,12 @@ public class CustomReadCardTask extends Thread { mCard.setBatch(tlvCardData.getTLV(TLV.Tag.TAG_Batch).getAsHexString()); + if( tlvCardData.hasTag(TLV.Tag.TAG_ProductMask)) { + mCard.setProductMask(tlvCardData.getTagAsInt(TLV.Tag.TAG_ProductMask)); + }else{ + mCard.setProductMask(ProductMask.Note); + } + TLV tokenSymbol = tlvCardData.getTLV(TLV.Tag.TAG_Token_Symbol); TLV contractAddress = tlvCardData.getTLV(TLV.Tag.TAG_Token_Contract_Address); TLV tokens_decimal = tlvCardData.getTLV(TLV.Tag.TAG_Token_Decimal); diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/VerifyCardTask.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/VerifyCardTask.java index 57cb73c822..4b70353a3b 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/VerifyCardTask.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/VerifyCardTask.java @@ -46,6 +46,10 @@ public class VerifyCardTask extends CustomReadCardTask { } mNotifications.onReadProgress(protocol, 90); + if( protocol.getCard().isIDCard() ) + { + protocol.run_GetIssuerDataEx(mNotifications); + } } } \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/WriteIDCardTask.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/WriteIDCardTask.java new file mode 100644 index 0000000000..9329607641 --- /dev/null +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/tasks/WriteIDCardTask.java @@ -0,0 +1,36 @@ +package com.tangem.tangem_card.tasks; + +import com.tangem.tangem_card.data.TangemCard; +import com.tangem.tangem_card.data.external.CardDataSubstitutionProvider; +import com.tangem.tangem_card.data.external.PINsProvider; +import com.tangem.tangem_card.reader.CardProtocol; +import com.tangem.tangem_card.reader.NfcReader; + +/** + * Created by dvol on 19.01.2020. + */ + +public class WriteIDCardTask extends CustomReadCardTask { + public static final String TAG = WriteIDCardTask.class.getSimpleName(); + + private TangemCard.IDCardData idCardData; + private byte[] issuerPrivateDataKey; + public WriteIDCardTask(TangemCard card, NfcReader reader, CardDataSubstitutionProvider cardDataSubstitutionProvider, PINsProvider pinsProvider, TangemCard.IDCardData idCardData, byte[] issuerPrivateDataKey, CardProtocol.Notifications notifications) { + super(card, reader, cardDataSubstitutionProvider, pinsProvider, notifications); + this.idCardData=idCardData; + this.issuerPrivateDataKey=issuerPrivateDataKey; + } + + @Override + public void run_Task() throws Exception { + mNotifications.onReadProgress(protocol, 0); + if (isCancelled) return; + + if( !protocol.getCard().isIDCard() ) + throw new Exception("It's not an IDCard!"); + + protocol.run_WriteIssuerDataEx(mNotifications,idCardData.toTLVList().toBytes(),issuerPrivateDataKey); + mNotifications.onReadProgress(protocol, 100); + } + +} \ No newline at end of file diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt index 9c6356dd4e..2f0cd24013 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt @@ -61,6 +61,13 @@ fun TangemCard.loadFromBundle(B: Bundle) { setIssuerData(B.getByteArray("IssuerData"), B.getByteArray("IssuerDataSignature")) else setIssuerData(null, null) + if (B.containsKey("IssuerDataEx") && B.containsKey("IssuerDataExSignature") && B.containsKey("IssuerDataExCounter")) + setIssuerDataEx(B.getByteArray("IssuerDataEx"), B.getByteArray("IssuerDataExSignature"), B.getInt("IssuerDataExCounter")) + else setIssuerDataEx(null, null, 0) + + if( B.containsKey("ProductMask") ) + setProductMask(B.getInt("ProductMask")) + if (B.containsKey("NeedWriteIssuerData")) needWriteIssuerData = B.getBoolean("NeedWriteIssuerData") @@ -149,6 +156,14 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putBoolean("NeedWriteIssuerData", needWriteIssuerData) } + if (issuerDataEx != null && issuerDataExSignature != null) { + B.putByteArray("IssuerDataEx", issuerDataEx) + B.putByteArray("IssuerDataExSignature", issuerDataExSignature) + B.putInt("IssuerDataExCounter", issuerDataExCounter) + } + + B.putInt("ProductMask",productMask) + if (isCodeConfirmed != null) B.putBoolean("codeConfirmed", isCodeConfirmed) From a06d4f0cb585d15e353c7ef263731b76e5770a8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Jan 2020 11:23:10 +0300 Subject: [PATCH 12/26] Updated on 2026-08-14 --- .../data/network/model/BlockcypherResponse.kt | 5 +- .../com/tangem/wallet/ethID/EthIdData.java | 33 + .../com/tangem/wallet/ethID/EthIdEngine.java | 663 ++++++++++++++++++ 3 files changed, 700 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/wallet/ethID/EthIdData.java create mode 100644 app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java diff --git a/app/src/main/java/com/tangem/data/network/model/BlockcypherResponse.kt b/app/src/main/java/com/tangem/data/network/model/BlockcypherResponse.kt index b470cba5ed..484526bc4d 100644 --- a/app/src/main/java/com/tangem/data/network/model/BlockcypherResponse.kt +++ b/app/src/main/java/com/tangem/data/network/model/BlockcypherResponse.kt @@ -35,7 +35,10 @@ data class BlockcypherTxref( data class BlockcypherTx( @SerializedName("hex") - var hex: String? = null + var hex: String? = null, + + @SerializedName("addresses") + var addesses: List? = null ) data class BlockcypherFee( diff --git a/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java b/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java new file mode 100644 index 0000000000..a3197b35d0 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java @@ -0,0 +1,33 @@ +package com.tangem.wallet.ethID; + +import com.tangem.wallet.CoinData; + +public class EthIdData extends CoinData { + private boolean hasApprovalTx = false; + private long approvalAddressNonce; + private byte[] CKDpub; + + public boolean isHasApprovalTx() { + return hasApprovalTx; + } + + public void setHasApprovalTx(boolean hasApprovalTx) { + this.hasApprovalTx = hasApprovalTx; + } + + public long getApprovalAddressNonce() { + return approvalAddressNonce; + } + + public void setApprovalAddressNonce(long approvalAddressNonce) { + this.approvalAddressNonce = approvalAddressNonce; + } + + public byte[] getCKDpub() { + return CKDpub; + } + + public void setCKDpub(byte[] CKDpub) { + this.CKDpub = CKDpub; + } +} diff --git a/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java b/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java new file mode 100644 index 0000000000..d5f5522635 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java @@ -0,0 +1,663 @@ +package com.tangem.wallet.ethID; + +import android.net.Uri; +import android.text.InputFilter; +import android.util.Log; + +import com.google.common.primitives.Bytes; +import com.tangem.App; +import com.tangem.Constant; +import com.tangem.data.Blockchain; +import com.tangem.data.network.ServerApiBlockcypher; +import com.tangem.data.network.ServerApiInfura; +import com.tangem.data.network.model.BlockcypherFee; +import com.tangem.data.network.model.BlockcypherResponse; +import com.tangem.data.network.model.BlockcypherTx; +import com.tangem.data.network.model.BlockcypherTxref; +import com.tangem.data.network.model.InfuraResponse; +import com.tangem.tangem_card.data.TangemCard; +import com.tangem.tangem_card.reader.CardProtocol; +import com.tangem.tangem_card.tasks.SignTask; +import com.tangem.util.CryptoUtil; +import com.tangem.util.DecimalDigitsInputFilter; +import com.tangem.wallet.BTCUtils; +import com.tangem.wallet.BalanceValidator; +import com.tangem.wallet.BuildConfig; +import com.tangem.wallet.CoinData; +import com.tangem.wallet.CoinEngine; +import com.tangem.wallet.ECDSASignatureETH; +import com.tangem.wallet.EthTransaction; +import com.tangem.wallet.Keccak256; +import com.tangem.wallet.R; +import com.tangem.wallet.TangemContext; +import com.tangem.wallet.eth.EthData; + +import org.bitcoin.Secp256k1Context; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.SignatureDecodeException; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.Arrays; + +import io.github.novacrypto.bip32.ExtendedPublicKey; + +public class EthIdEngine extends CoinEngine { + + private static final String TAG = com.tangem.wallet.eth.EthEngine.class.getSimpleName(); + public EthIdData coinData = null; + String approvalAddress = ""; //TODO + byte[] approvalPubKey = new byte[1]; //TODO + + public EthIdEngine(TangemContext ctx) throws Exception { + super(ctx); + if (ctx.getCoinData() == null) { + coinData = new EthIdData(); + ctx.setCoinData(coinData); + } else if (ctx.getCoinData() instanceof EthData) { + coinData = (EthIdData) ctx.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for " + this.getClass().getSimpleName()); + } + } + + public EthIdEngine() { + super(); + } + + private static int getDecimals() { + return 18; + } + + private int getChainIdNum() { + return ctx.getBlockchain() == Blockchain.Ethereum ? EthTransaction.ChainEnum.Mainnet.getValue() : EthTransaction.ChainEnum.Rinkeby.getValue(); + } + + @Override + public boolean awaitingConfirmation() { + return !coinData.getUnconfirmedTXCount().equals(coinData.getConfirmedTXCount()) || App.pendingTransactionsStorage.hasTransactions(ctx.getCard()); + } + + @Override + public Amount getBalance() { + if (!hasBalanceInfo()) { + return null; + } + return convertToAmount(coinData.getBalanceInInternalUnits()); + } + + @Override + public String getBalanceHTML() { + Amount balance = getBalance(); + if (balance != null) { + return balance.toDescriptionString(getDecimals()); + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + return Blockchain.Ethereum.getCurrency(); + } + + @Override + public boolean isBalanceNotZero() { + if (coinData == null) return false; + if (coinData.getBalanceInInternalUnits() == null) return false; + return coinData.getBalanceInInternalUnits().notZero(); + } + + @Override + public String getFeeCurrency() { + return Blockchain.Ethereum.getCurrency(); + } + + public boolean isNeedCheckNode() { + return false; + } + + + @Override + public CoinData createCoinData() { + return new EthData(); + } + + @Override + public String getUnspentInputsDescription() { + return ""; + } + +// BigDecimal convertToEth(String value) { +// BigInteger m = new BigInteger(value, 10); +// BigDecimal n = new BigDecimal(m); +// BigDecimal d = n.divide(new BigDecimal("1000000000000000000")); +// d = d.setScale(8, RoundingMode.DOWN); +// return d; +// } + + @Override + public boolean validateAddress(String address) { + if (address == null || address.isEmpty()) { + return false; + } + + if (!address.startsWith("0x") && !address.startsWith("0X")) { + return false; + } + + if (address.length() != 42) { + return false; + } + + return true; + } + +// public String getBalanceValue(TangemCard mCard) { +// String dec = coinData.getBalanceInInternalUnits(); +// BigDecimal d = convertToEth(dec); +// String s = d.toString(); +// +// String pattern = "#0.##################"; // If you like 4 zeros +// DecimalFormat myFormatter = new DecimalFormat(pattern); +// String output = myFormatter.format(d); +// return output; +// } + +// public static String getAmountEquivalentDescription(Amount amount, double rateValue) { +// if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) +// return ""; +// +// if (rateValue > 0) { +// BigDecimal biRate = new BigDecimal(rateValue); +// BigDecimal exchangeCurs = biRate.multiply(amount); +// exchangeCurs = exchangeCurs.setScale(2, RoundingMode.DOWN); +// return "≈ USD  " + exchangeCurs.toString(); +// } else { +// return ""; +// } +// } + +// public static String getAmountEquivalentDescriptionETH(Double amount, float rate) { +// if (amount == 0) +// return ""; +// amount = amount / 100000; +// if (rate > 0) { +// return String.format("≈ USD %.2f", amount * rate); +// } else { +// return ""; +// } +// +// } + + + @Override + public String getBalanceEquivalent() { + Amount balance = getBalance(); + if (balance == null) return ""; + return balance.toEquivalentString(coinData.getRate()); + } + + @Override + public Amount convertToAmount(InternalAmount internalAmount) { + BigDecimal d = internalAmount.divide(new BigDecimal("1000000000000000000"), getDecimals(), RoundingMode.DOWN); + return new Amount(d, ctx.getBlockchain().getCurrency()); + } + + @Override + public Amount convertToAmount(String strAmount, String currency) { + return new Amount(strAmount, currency); + } + + @Override + public InternalAmount convertToInternalAmount(Amount amount) { + return new InternalAmount(amount.multiply(new BigDecimal("1000000000000000000")), "wei"); + } + + @Override + public InternalAmount convertToInternalAmount(byte[] bytes) { + //throw new Exception("Not implemented"); + return null; + } + + + @Override + public byte[] convertToByteArray(InternalAmount amount) throws Exception { + throw new Exception("Not implemented"); + } + + @Override + public boolean hasBalanceInfo() { + return coinData.getBalanceInInternalUnits() != null; + } + + @Override + public Uri getShareWalletUri() { + if (ctx.getCard().getDenomination() != null) { + return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18"); + } else { + return Uri.parse("ethereum:" + ctx.getCoinData().getWallet()); + } + } + + @Override + public Uri getWalletExplorerUri() { + if (ctx.getBlockchain() == Blockchain.EthereumTestNet) + return Uri.parse("https://rinkeby.etherscan.io/address/" + ctx.getCoinData().getWallet()); + else + return Uri.parse("https://etherscan.io/address/" + ctx.getCoinData().getWallet()); + } + + @Override + public boolean isExtractPossible() { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data); + } else if (!isBalanceNotZero()) { + ctx.setMessage(R.string.general_wallet_empty); + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.loaded_wallet_message_wait); + } else { + return true; + } + return false; + } + + @Override + public InputFilter[] getAmountInputFilters() { + return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())}; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (BuildConfig.FLAVOR == Constant.FLAVOR_TANGEM_CARDANO) { + return true; + } + Amount balance = getBalance(); + if (balance == null || amount.compareTo(balance) > 0) { + return false; + } + return true; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) { +// Long fee = null; +// Long amount = null; +// try { +// amount = mCard.internalUnitsFromString(amountValue); +// fee = mCard.internalUnitsFromString(feeValue); +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// +// if (fee == null || amount == null) +// return false; +// +// if (fee == 0 || amount == 0) +// return false; +// +// +// if (fee < minFeeInInternalUnits) +// return false; + + try { + BigDecimal cardBalance = getBalance(); + + if (isFeeIncluded && (amount.compareTo(cardBalance) > 0 || amount.compareTo(fee) < 0)) + return false; + + if (!isFeeIncluded && amount.add(fee).compareTo(cardBalance) > 0) + return false; + + } catch (NumberFormatException e) { + e.printStackTrace(); + } + + return true; + } + + @Override + public boolean validateBalance(BalanceValidator balanceValidator) { + if (getBalance() == null) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_unknown_balance); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance); + return false; + } + + if (!coinData.getUnconfirmedTXCount().equals(coinData.getConfirmedTXCount())) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_transaction_in_progress); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_wait_for_confirmation); + return false; + } + + if (coinData.isBalanceReceived()) { + balanceValidator.setScore(100); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_balance); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain); + if (getBalance().isZero()) { + balanceValidator.setFirstLine(R.string.balance_validator_first_line_empty_wallet); + balanceValidator.setSecondLine(R.string.empty_string); + } + } + + if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && getBalance().notZero()) { + balanceValidator.setScore(80); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online); + } + + return true; + + } + + @Override + public String evaluateFeeEquivalent(String fee) { + try { + Amount feeValue = new Amount(fee, ctx.getBlockchain().getCurrency()); + return feeValue.toEquivalentString(coinData.getRate()); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + @Override + public String calculateAddress(byte[] pkUncompressed) { + Keccak256 kec = new Keccak256(); + int lenPk = pkUncompressed.length; + if (lenPk < 2) { + throw new IllegalArgumentException("Uncompress public key length is invalid"); + } + byte[] cleanKey = new byte[lenPk - 1]; + for (int i = 0; i < cleanKey.length; ++i) { + cleanKey[i] = pkUncompressed[i + 1]; + } + byte[] r = kec.digest(cleanKey); + + byte[] address = new byte[20]; + for (int i = 0; i < 20; ++i) { + address[i] = r[i + 12]; + } + + return String.format("0x%s", BTCUtils.toHex(address)); + } + + @Override + public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) { + + Log.e(TAG, "Construct transaction " + amountValue.toString() + " with fee " + feeValue.toString() + (IncFee ? " including" : " excluding")); + + BigInteger nonceValue = coinData.getConfirmedTXCount(); + byte[] pbKey = ctx.getCard().getWalletPublicKey(); + + BigInteger weiFee = convertToInternalAmount(feeValue).toBigIntegerExact(); + BigInteger weiAmount = convertToInternalAmount(amountValue).toBigIntegerExact(); + + if (IncFee) { + weiAmount = weiAmount.subtract(weiFee); + } + + BigInteger gasPrice = weiFee.divide(BigInteger.valueOf(21000)); + BigInteger gasLimit = BigInteger.valueOf(21000); + Integer chainId = this.getChainIdNum(); + + String to = targetAddress; + + if (to.startsWith("0x") || to.startsWith("0X")) { + to = to.substring(2); + } + + final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId); + + return new SignTask.TransactionToSign() { + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash; + } + + @Override + public byte[][] getHashesToSign() { + byte[][] hashesForSign = new byte[1][]; + hashesForSign[0] = tx.getRawHash(); + return hashesForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); + } + + @Override + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Transaction validation by issuer not supported in this version"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + byte[] for_hash = tx.getRawHash(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64)); + s = CryptoUtil.toCanonicalised(s); + + boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), pbKey); + + if (!f) { + Log.e(this.getClass().getSimpleName() + "-CHECK", "sign Failed."); + } + + tx.signature = new ECDSASignatureETH(r, s); + int v = tx.BruteRecoveryID2(tx.signature, for_hash, pbKey); + if (v != 27 && v != 28) { + Log.e(this.getClass().getSimpleName(), "invalid v"); + throw new Exception("Error in " + this.getClass().getSimpleName() + " - invalid v"); + } + tx.signature.v = (byte) v; + Log.e(this.getClass().getSimpleName(), " V: " + String.valueOf(v)); + + byte[] txForSend = tx.getEncoded(); + notifyOnNeedSendTransaction(txForSend); + return txForSend; + } + }; + } + + @Override + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + + final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher(); + + ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() { + @Override + public void onSuccess(String method, BlockcypherResponse blockcypherResponse) { + Log.i(TAG, "onSuccess: " + method); + try { + //TODO: change request logic, 2000 tx max + if (blockcypherResponse.getTxrefs() != null) { + for (BlockcypherTxref txref : blockcypherResponse.getTxrefs()) { + serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txref.getTx_hash()); + } + + if (serverApiBlockcypher.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception"); + } + } + + public void onSuccess(String method, BlockcypherFee blockcypherFee) { + Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions"); + } + + @Override + public void onFail(String method, String message) { + Log.i(TAG, "onFail: " + method + " " + message); + } + }; + + ServerApiBlockcypher.TxResponseListener txListener = new ServerApiBlockcypher.TxResponseListener() { + @Override + public void onSuccess(BlockcypherTx response) { + Log.i(TAG, "onSuccess: BlockcypherTx"); + try { + if (response.getAddesses().contains(approvalAddress)) { + coinData.setHasApprovalTx(true); + } + } catch (Exception e) { + Log.e(TAG, "onFail: BlockcypherTx" + response); + } + if (serverApiBlockcypher.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onFail(String message) { + Log.i(TAG, "onFail: BlockcypherTx " + message); + if (serverApiBlockcypher.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + }; + + serverApiBlockcypher.setResponseListener(blockcypherListener); + serverApiBlockcypher.setTxResponseListener(txListener); + + serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), ""); + requestApprovalAddressNonce(); + } + + @Override + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + + String txStr = String.format("0x%s", BTCUtils.toHex(txForSend)); + + ServerApiInfura serverApiInfura = new ServerApiInfura(); + // request requestData eth gasPrice listener + ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() { + @Override + public void onSuccess(String method, InfuraResponse infuraResponse) { + if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) { + if (infuraResponse.getResult() == null || infuraResponse.getResult().isEmpty()) { + ctx.setError("Rejected by node: " + infuraResponse.getError()); + blockchainRequestsCallbacks.onComplete(false); + } else { + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } + } + } + + @Override + public void onFail(String method, String message) { + if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + + serverApiInfura.setResponseListener(responseListener); + + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr); + } + + public int pendingTransactionTimeoutInSeconds() { + return 10; + } + + @Override + public boolean needMultipleLinesForBalance() { + return true; + } + + @Override + public void defineWallet() throws CardProtocol.TangemException { + try { + String wallet = calculateAddress(calculateCKDpub(ctx.getIdHash)); //TODO + ctx.getCoinData().setWallet(wallet); + } catch (Exception e) { + ctx.getCoinData().setWallet("ERROR"); + throw new CardProtocol.TangemException("Can't define wallet address"); + } + + } + + private byte[] calculateCKDpub(byte[] idHash) { + DeterministicKey masterPubKey = HDKeyDerivation.createMasterPubKeyFromBytes(ctx.getCard().getWalletPublicKeyRar(), idHash); + DeterministicKey childPubKey = HDKeyDerivation.deriveChildKey(masterPubKey, new ChildNumber(1, false)); + return childPubKey.getPubKey(); + } + + private EthTransaction constructIdTxForSign(long nonce) { + BigInteger nonceValue = BigInteger.valueOf(nonce); + + BigInteger gasPrice = BigInteger.valueOf(10000000000L); + BigInteger gasLimit = BigInteger.valueOf(21000); + + BigInteger weiAmount = gasPrice.multiply(gasLimit).add(BigInteger.ONE); + + Integer chainId = this.getChainIdNum(); + + String to = coinData.getWallet(); + + if (to.startsWith("0x") || to.startsWith("0X")) { + to = to.substring(2); + } + + return EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId); + } + + private boolean checkSignature(byte[] signature, long nonce) throws SignatureDecodeException { + byte[] hashForSign = constructIdTxForSign(nonce).getRawHash(); +// new Secp256k1Context(); + return ECKey.verify(hashForSign, signature, coinData.getCKDpub()); + } + + private void requestApprovalAddressNonce() { + final ServerApiInfura serverApiInfura = new ServerApiInfura(); + + ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() { + @Override + public void onSuccess(String method, InfuraResponse infuraResponse) { + String pending = infuraResponse.getResult(); + pending = pending.substring(2); +// BigInteger count = new BigInteger(pending, 16); + Long count = Long.valueOf(pending, 16); + coinData.setApprovalAddressNonce(count); + } + @Override + public void onFail(String method, String message) { + Log.e(TAG, "onFail: " + method + " " + message); + } + }; + serverApiInfura.setResponseListener(responseListener); + + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, coinData.getWallet(), "", ""); + } +} From 07b30e1f02681e622f8e175ba23dd64bcff5c672 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Jan 2020 12:48:05 +0300 Subject: [PATCH 13/26] Updated on 2026-08-14 --- .../com/tangem/wallet/ethID/EthIdData.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java b/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java index a3197b35d0..6cdf9fcf0b 100644 --- a/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java +++ b/app/src/main/java/com/tangem/wallet/ethID/EthIdData.java @@ -1,11 +1,14 @@ package com.tangem.wallet.ethID; +import android.os.Bundle; +import android.util.Log; + import com.tangem.wallet.CoinData; public class EthIdData extends CoinData { private boolean hasApprovalTx = false; - private long approvalAddressNonce; - private byte[] CKDpub; + private Long approvalAddressNonce; + private byte[] CKDpub = null; public boolean isHasApprovalTx() { return hasApprovalTx; @@ -30,4 +33,36 @@ public class EthIdData extends CoinData { public void setCKDpub(byte[] CKDpub) { this.CKDpub = CKDpub; } + + @Override + public void clearInfo() { + super.clearInfo(); + hasApprovalTx = false; + approvalAddressNonce = null; + } + + @Override + public void loadFromBundle(Bundle B) { + super.loadFromBundle(B); + + if (B.containsKey("ApprovalAddressNonce")) { + approvalAddressNonce = B.getLong("ApprovalAddressNonce"); + } + if (B.containsKey("CKDpub")) CKDpub = B.getByteArray("CKDpub"); + hasApprovalTx = B.getBoolean("HasApprovalTx"); + } + + @Override + public void saveToBundle(Bundle B) { + super.saveToBundle(B); + try { + if (approvalAddressNonce != null) B.putLong("ApprovalAddressNonce", approvalAddressNonce); + if (CKDpub != null) B.putByteArray("CKDpub", CKDpub); + B.putBoolean("HasApprovalTx", hasApprovalTx); + + } catch (Exception e) { + Log.e("Can't save to bundle ", e.getMessage()); + } + + } } From a4de1112906dd4554f453899924715d3383666e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Jan 2020 19:27:46 +0300 Subject: [PATCH 14/26] Updated on 2026-08-14 --- .idea/navEditor.xml | 195 ++++++-- app/build.gradle | 3 + .../main/java/com/tangem/data/Blockchain.java | 1 + .../com/tangem/ui/fragment/MainFragment.kt | 21 +- .../tangem/ui/fragment/id/CameraFragment.kt | 46 ++ .../com/tangem/ui/fragment/id/IdFragment.kt | 465 ++++++++++++++++++ .../ui/fragment/id/IssueNewIdFragment.kt | 253 ++++++++++ .../ui/fragment/id/ValidateIdFragment.kt | 323 ++++++++++++ .../tangem/ui/fragment/id/WriteIdFragment.kt | 285 +++++++++++ .../com/tangem/wallet/CoinEngineFactory.kt | 4 + .../java/com/tangem/wallet/TangemContext.java | 3 + .../com/tangem/wallet/ethID/EthIdEngine.java | 209 ++------ app/src/main/res/drawable/ic_add_a_photo.xml | 10 + app/src/main/res/drawable/ic_flip_camera.xml | 9 + app/src/main/res/drawable/ic_person.xml | 4 + app/src/main/res/drawable/ic_take_picture.xml | 9 + app/src/main/res/layout/fragment_camera.xml | 52 ++ app/src/main/res/layout/fragment_id.xml | 242 +++++++++ .../main/res/layout/fragment_issue_new_id.xml | 144 ++++++ .../main/res/layout/fragment_validate_id.xml | 51 ++ app/src/main/res/layout/fragment_write_id.xml | 61 +++ app/src/main/res/layout/layout_id.xml | 59 +++ app/src/main/res/layout/layout_touch_card.xml | 1 + app/src/main/res/navigation/main_graph.xml | 41 ++ app/src/main/res/values/strings.xml | 4 + .../tangem/tangem_card/data/ProductMask.kt | 12 + .../tangem/tangem_card/data/TangemCard.java | 75 ++- .../tangem_card/reader/CardProtocol.java | 8 +- .../tangem/tangem_card/reader/TLVList.java | 3 +- .../tangem_sdk/data/TangemCardExtensions.kt | 6 + .../src/main/res/drawable/id_card_default.xml | 6 + 31 files changed, 2365 insertions(+), 240 deletions(-) create mode 100644 app/src/main/java/com/tangem/ui/fragment/id/CameraFragment.kt create mode 100644 app/src/main/java/com/tangem/ui/fragment/id/IdFragment.kt create mode 100644 app/src/main/java/com/tangem/ui/fragment/id/IssueNewIdFragment.kt create mode 100644 app/src/main/java/com/tangem/ui/fragment/id/ValidateIdFragment.kt create mode 100644 app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt create mode 100644 app/src/main/res/drawable/ic_add_a_photo.xml create mode 100644 app/src/main/res/drawable/ic_flip_camera.xml create mode 100644 app/src/main/res/drawable/ic_person.xml create mode 100644 app/src/main/res/drawable/ic_take_picture.xml create mode 100644 app/src/main/res/layout/fragment_camera.xml create mode 100644 app/src/main/res/layout/fragment_id.xml create mode 100644 app/src/main/res/layout/fragment_issue_new_id.xml create mode 100644 app/src/main/res/layout/fragment_validate_id.xml create mode 100644 app/src/main/res/layout/fragment_write_id.xml create mode 100644 app/src/main/res/layout/layout_id.xml create mode 100644 tangem-card-old/src/main/java/com/tangem/tangem_card/data/ProductMask.kt create mode 100644 tangem-sdk-old/src/main/res/drawable/id_card_default.xml diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml index 4548c6069e..09d3bc259e 100644 --- a/.idea/navEditor.xml +++ b/.idea/navEditor.xml @@ -8,13 +8,25 @@ @@ -200,8 +274,8 @@ @@ -212,8 +286,8 @@ @@ -224,8 +298,8 @@ @@ -236,8 +310,8 @@