From 857913f36a955dfe34bd4a64234b0c92044fd870 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Nov 2019 18:06:23 +0300 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 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 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/16] 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 5dcffccc790cc5848d6b69b50cf306d900ecc997 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Jan 2020 17:08:32 +0300 Subject: [PATCH 11/16] Updated on 2026-08-14 --- .../com/tangem/ui/fragment/MainFragment.kt | 3 +- .../ui/fragment/additional/TagFragment.kt | 20 ++- .../com/tangem/wallet/CoinEngineFactory.kt | 2 +- .../java/com/tangem/wallet/TangemContext.java | 10 +- .../java/com/tangem/wallet/xlm/XlmEngine.java | 4 +- .../wallet/{xlmtag => xlmTag}/XlmTagData.kt | 2 +- .../wallet/{xlmtag => xlmTag}/XlmTagEngine.kt | 115 ++++++------------ .../server_android/data/LocalStorage.kt | 3 +- .../src/main/res/drawable/card_tgslix.png | Bin 0 -> 50098 bytes 9 files changed, 56 insertions(+), 103 deletions(-) rename app/src/main/java/com/tangem/wallet/{xlmtag => xlmTag}/XlmTagData.kt (99%) rename app/src/main/java/com/tangem/wallet/{xlmtag => xlmTag}/XlmTagEngine.kt (78%) create mode 100644 server-android/src/main/res/drawable/card_tgslix.png 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 ffd3d3f097..3c4b5285c5 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -7,7 +7,6 @@ import android.net.Uri import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.IsoDep -import android.nfc.tech.Ndef import android.nfc.tech.NfcV import android.os.Bundle import android.text.Spannable @@ -55,7 +54,7 @@ import com.tangem.wallet.BuildConfig import com.tangem.wallet.CoinEngineFactory import com.tangem.wallet.R import com.tangem.wallet.TangemContext -import com.tangem.wallet.xlmtag.XlmTagEngine +import com.tangem.wallet.xlmTag.XlmTagEngine import kotlinx.android.synthetic.main.fragment_main.* import kotlinx.android.synthetic.main.layout_touch_card.* import kotlinx.coroutines.CoroutineScope 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 index dd2d27594c..8514ccf37c 100644 --- a/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt @@ -20,7 +20,7 @@ import com.tangem.ui.navigation.NavigationResultListener import com.tangem.util.LOG import com.tangem.util.UtilHelper import com.tangem.wallet.* -import com.tangem.wallet.xlmtag.XlmTagEngine +import com.tangem.wallet.xlmTag.XlmTagEngine import kotlinx.android.synthetic.main.fr_loaded_wallet.* import kotlinx.android.synthetic.main.layout_btn_details.* import kotlinx.android.synthetic.main.layout_tangem_card.* @@ -67,7 +67,7 @@ class TagFragment : BaseFragment(), NavigationResultListener { btnExtract.backgroundTintList = ContextCompat.getColorStateList(requireContext(), R.color.btn_dark) - ivTangemCard.setImageResource(R.drawable.card_default_nft) + ivTangemCard.setImageResource(R.drawable.card_tgslix) tvBalance.setSingleLine(!engine.needMultipleLinesForBalance()) tvWallet.text = ctx.coinData.wallet @@ -78,7 +78,6 @@ class TagFragment : BaseFragment(), NavigationResultListener { srl?.setOnRefreshListener { refresh(true) } requestBalanceAndUnspentTransactions() -// update() } @@ -129,14 +128,13 @@ class TagFragment : BaseFragment(), NavigationResultListener { else -> tvBalance.text = "" } - if (ctx.card!!.tokenSymbol.length > 1) { - @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY) - else - Html.fromHtml(ctx.blockchainName) - tvBlockchain.text = html - } else - tvBlockchain.text = ctx.blockchainName + + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(ctx.blockchainName) + tvBlockchain.text = html + } diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index c65b0da0e2..1f888ee616 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -19,7 +19,7 @@ import com.tangem.wallet.token.TokenEngine import com.tangem.wallet.tezos.TezosEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine -import com.tangem.wallet.xlmtag.XlmTagEngine +import com.tangem.wallet.xlmTag.XlmTagEngine import com.tangem.wallet.xrp.XrpEngine /** diff --git a/app/src/main/java/com/tangem/wallet/TangemContext.java b/app/src/main/java/com/tangem/wallet/TangemContext.java index 07cb229329..1f71dc9873 100644 --- a/app/src/main/java/com/tangem/wallet/TangemContext.java +++ b/app/src/main/java/com/tangem/wallet/TangemContext.java @@ -55,14 +55,16 @@ public class TangemContext { public String getBlockchainName() { Blockchain blockchain = getBlockchain(); if (blockchain == Blockchain.Token || blockchain == Blockchain.RootstockToken) { - String token = card.getTokenSymbol(); - return token + "
" + getBlockchain().getOfficialName() + " smart contract token"; + return card.getTokenSymbol()+ "
" + getBlockchain().getOfficialName() + " smart contract token"; } if (blockchain == Blockchain.NftToken) { - return card.getTokenSymbol().substring(4) + "
" + getBlockchain().getOfficialName() + " NFT token"; + return card.getTokenSymbol().substring(4) + "
" + getBlockchain().getOfficialName() + " non-fungible token"; } if (blockchain == Blockchain.StellarAsset) { - return card.getTokenSymbol() + "
" + getBlockchain().getOfficialName() + " asset"; + return card.getTokenSymbol() + "
" + getBlockchain().getOfficialName() + " asset"; + } + if (blockchain == Blockchain.StellarTag) { + return "TANGEM TAG
"+ getBlockchain().getOfficialName() + " non-fungible token "; } return blockchain.getOfficialName(); } diff --git a/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java b/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java index c707517abc..297991bd3f 100644 --- a/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java +++ b/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java @@ -76,7 +76,7 @@ public class XlmEngine extends CoinEngine { public String getBalanceHTML() { Amount balance = getBalance(); if (balance != null) { - return " " + balance.toDescriptionString(getDecimals()) + "
+ " + coinData.getReserve().toDescriptionString(getDecimals()) + " reserve"; + return " " + balance.toDescriptionString(getDecimals()) + "
+ " + coinData.getReserve().toDescriptionString(getDecimals()) + " reserve1"; } else { return ""; } @@ -243,7 +243,7 @@ public class XlmEngine extends CoinEngine { // return; // } - if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalance().notZero()) { + if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures())) { balanceValidator.setScore(80); balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline); balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance); diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt similarity index 99% rename from app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt rename to app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt index bdd8f4e3af..e8287ccd86 100644 --- a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt +++ b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt @@ -1,4 +1,4 @@ -package com.tangem.wallet.xlmtag +package com.tangem.wallet.xlmTag import android.os.Bundle import android.util.Log diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt similarity index 78% rename from app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt rename to app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt index 4fe5de60ca..3a4548442d 100644 --- a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt +++ b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt @@ -1,4 +1,4 @@ -package com.tangem.wallet.xlmtag +package com.tangem.wallet.xlmTag import android.net.Uri import android.os.StrictMode @@ -53,12 +53,18 @@ class XlmTagEngine : CoinEngine { } override fun getBalanceHTML(): String { - return if (hasBalanceInfo() && isBalanceNotZero && coinData!!.fundsFromTrustedSource) { - ctx.getString(R.string.tag_genuine) - } else if (coinData!!.fundsSentToTrustedSource) { - ctx.getString(R.string.tag_claimed) + return if (hasBalanceInfo()) { + if (coinData!!.fundsFromTrustedSource) { +// if (coinData!!.fundsSentToTrustedSource) { //TODO: shut down for example tags to appear genuine, turn on for production +// ctx.getString(R.string.tag_claimed) +// } else { + ctx.getString(R.string.tag_genuine) +// } + } else { + ctx.getString(R.string.tag_not_genuine) + } } else { - ctx.getString(R.string.tag_not_genuine) + "" } } @@ -72,7 +78,7 @@ class XlmTagEngine : CoinEngine { } override fun hasBalanceInfo(): Boolean { - return if (coinData == null) false else coinData!!.getBalance() != null || coinData!!.isError404 + return if (coinData == null) false else coinData!!.isBalanceReceived } override fun isExtractPossible(): Boolean { @@ -130,43 +136,30 @@ class XlmTagEngine : CoinEngine { } override fun validateBalance(balanceValidator: BalanceValidator): Boolean { - return try { - if (ctx.card.offlineBalance == null && !ctx.coinData.isBalanceReceived || !ctx.coinData.isBalanceReceived && ctx.card.remainingSignatures != ctx.card.maxSignatures) { - if (coinData!!.isError404) { - balanceValidator.setScore(0) - balanceValidator.firstLine = R.string.balance_validator_first_line_no_account - balanceValidator.setSecondLine(R.string.balance_validator_second_line_create_account_instruction) - } else { - balanceValidator.setScore(0) - balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance - balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance) - return false - } + try { + if (!hasBalanceInfo()) { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_no_connection + balanceValidator.setSecondLine(R.string.balance_validator_second_line_authenticity_not_verified) + return false } - - if (coinData!!.isBalanceReceived && coinData!!.isBalanceEqual) { + if (coinData!!.fundsFromTrustedSource) { balanceValidator.setScore(100) - balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance - balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain) - if (coinData!!.getBalance()!!.isZero) { - balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet - balanceValidator.setSecondLine(R.string.empty_string) - } + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_in_blockchain + balanceValidator.setSecondLine(R.string.empty_string) + } else { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_authenticity + balanceValidator.setSecondLine(R.string.empty_string) } - - if (ctx.card.offlineBalance != null && !coinData!!.isBalanceReceived && ctx.card.remainingSignatures == ctx.card.maxSignatures && coinData!!.getBalance()!!.notZero()) { - balanceValidator.setScore(80) - balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline - balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance) - } - - true + return true } catch (e: Exception) { e.printStackTrace() - false + return false } } + override fun getBalance(): Amount? { return if (!hasBalanceInfo()) null else coinData!!.getBalance()!! } @@ -310,7 +303,7 @@ class XlmTagEngine : CoinEngine { val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet) try { - var operationsPage = server.payments().forAccount(accountKeyPair).order(RequestBuilder.Order.DESC).execute() + var operationsPage = server.payments().forAccount(accountKeyPair).limit(200).order(RequestBuilder.Order.DESC).execute() Log.e("Stellar", operationsPage.records.toString()) operations.addAll(operationsPage.records) @@ -324,6 +317,8 @@ class XlmTagEngine : CoinEngine { } parsePayments(blockchainRequestsCallbacks) + + CoroutineScope(Dispatchers.Main).launch { blockchainRequestsCallbacks.onComplete(!ctx.hasError()) } } catch (e: Exception) { if (e.message != null) { ctx.error = e.message @@ -342,53 +337,11 @@ class XlmTagEngine : CoinEngine { Log.e("Stellar", "fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " + "fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource") - blockchainRequestsCallbacks.onComplete(!ctx.hasError()) + coinData!!.isBalanceReceived = true } override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { CoroutineScope(Dispatchers.IO).launch { requestPayments(blockchainRequestsCallbacks) } - val serverApi = ServerApiStellar(Blockchain.Stellar) - val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { - override fun onSuccess(request: StellarRequest.Base) { - Log.i(TAG, "onSuccess: " + request.javaClass.simpleName) - if (request is StellarRequest.Balance) { - coinData!!.accountResponse = request.accountResponse - coinData!!.validationNodeDescription = serverApi.currentURL - - blockchainRequestsCallbacks.onProgress() - } else if (request is Ledgers) { - coinData!!.setLedgerResponse(request.ledgerResponse) - if (serverApi.isRequestsSequenceCompleted) { - } else { - blockchainRequestsCallbacks.onProgress() - } - } else { - ctx.error = "Invalid request logic" - blockchainRequestsCallbacks.onComplete(false) - } - } - - override fun onFail(request: StellarRequest.Base) { - Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error) - if (request.errorResponse != null && request.errorResponse.code == 404) { - coinData!!.isError404 = true - } else { - ctx.error = request.error - } - if (serverApi.isRequestsSequenceCompleted) { - if (ctx.hasError()) { - blockchainRequestsCallbacks.onComplete(false) - } else { - blockchainRequestsCallbacks.onComplete(true) - } - } else { - blockchainRequestsCallbacks.onProgress() - } - } - } - serverApi.setListener(listener) - serverApi.requestData(ctx, StellarRequest.Balance(coinData!!.wallet)) - serverApi.requestData(ctx, Ledgers()) } @Throws(Exception::class) @@ -463,7 +416,7 @@ class XlmTagEngine : CoinEngine { private val multiplier = BigDecimal("10000000") - const val OPERATIONS_LIMIT = 50 + const val OPERATIONS_LIMIT = 200 private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM" private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2" } 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 81dc1f368a..3fa43bfe6b 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() < 31) { + if (artworks.count() < 32) { // forceSave=true only on the last one putResourceArtworkToCatalog(R.drawable.card_default, false) putResourceArtworkToCatalog(R.drawable.card_default_nft, false) @@ -78,6 +78,7 @@ class LocalStorage putResourceArtworkToCatalog(R.drawable.card_ru043, false) putResourceArtworkToCatalog(R.drawable.card_tg044, false) putResourceArtworkToCatalog(R.drawable.card_tg046, false) + putResourceArtworkToCatalog(R.drawable.card_tgslix, false) putResourceArtworkToCatalog(R.drawable.card_bc00, false) putResourceArtworkToCatalog(R.drawable.card_ff32, true) } diff --git a/server-android/src/main/res/drawable/card_tgslix.png b/server-android/src/main/res/drawable/card_tgslix.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2f54459841855af88c108a7df19c6dae60d04b GIT binary patch literal 50098 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� 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 literal 0 HcmV?d00001 From 1a382ce0e615b13bf66ddd52be77189015de91fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Jan 2020 18:01:51 +0300 Subject: [PATCH 12/16] Updated on 2026-08-14 --- .../src/main/java/com/tangem/CardManager.kt | 22 +++++- .../tangem/commands/CreateWalletCommand.kt | 73 +++++++++++++++++++ .../main/java/com/tangem/common/tlv/TlvTag.kt | 2 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt index 197c82891a..2c74c35b94 100644 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ b/tangem-core/src/main/java/com/tangem/CardManager.kt @@ -122,6 +122,26 @@ class CardManager( runTask(task, cardId, callback) } + /** + * This command will create a new wallet on the card having ‘Empty’ state. + * A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card. + * App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand] + * and then transform it into an address of corresponding blockchain wallet + * according to a specific blockchain algorithm. + * WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand]. + * RemainingSignature is set to MaxSignatures. + * + * @property cardId CID, Unique Tangem card ID number. + * @property cvc Optional 3-digit code printed on the card. Required if Use_CVC flag is set in Settings_Mask. + */ + fun createWallet(cardId: String, + cvc: ByteArray? = null, + callback: (result: TaskEvent) -> Unit) { + val createWalletCommand = CreateWalletCommand(cardId) + val task = SingleCommandTask(createWalletCommand) + runTask(task, cardId, callback) + } + /** */ @@ -157,6 +177,6 @@ class CardManager( } private fun fetchCardEnvironment(cardId: String?): CardEnvironment { - return cardEnvironmentRepository[cardId] ?: CardEnvironment() + return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt new file mode 100644 index 0000000000..416ebad2f6 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt @@ -0,0 +1,73 @@ +package com.tangem.commands + +import com.tangem.CardEnvironment +import com.tangem.common.apdu.CommandApdu +import com.tangem.common.apdu.Instruction +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.tlv.Tlv +import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvTag +import com.tangem.tasks.TaskError + +class CreateWalletResponse( + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, + /** + * Current status of the card [1 - Empty, 2 - Loaded, 3- Purged] + */ + val status: CardStatus, + /** + + */ + val walletPublicKey: ByteArray +) : CommandResponse + +/** + * This command will create a new wallet on the card having ‘Empty’ state. + * A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card. + * App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand] + * and then transform it into an address of corresponding blockchain wallet + * according to a specific blockchain algorithm. + * WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand]. + * RemainingSignature is set to MaxSignatures. + * + * @property cardId CID, Unique Tangem card ID number. + * @property cvc Optional 3-digit code printed on the card. Required if Use_CVC flag is set in Settings_Mask. + */ +class CreateWalletCommand( + private val cardId: String, + private val cvc: ByteArray? = null +) : CommandSerializer() { + + override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + val tlvData = mutableListOf( + Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()), + Tlv(TlvTag.CardId, cardId.hexToBytes()), + Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256()) + ) + if (cvc != null) { + tlvData.add(Tlv(TlvTag.Cvc, cvc)) + } + + return CommandApdu(Instruction.CreateWallet, tlvData) + } + + override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? { + val tlvData = responseApdu.getTlvData() ?: return null + + return try { + val mapper = TlvMapper(tlvData) + CreateWalletResponse( + cardId = mapper.map(TlvTag.CardId), + status = mapper.map(TlvTag.Status), + walletPublicKey = mapper.map(TlvTag.WalletPublicKey) + ) + } catch (exception: Exception) { + throw TaskError.SerializeCommandError() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt index 491509ae08..ca292d37f4 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt @@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) { Pause(0x1C), ManufactureId(0x20), - ManufacturerSignature(0x21), + ManufacturerSignature(0x86), IssuerDataPublicKey(0x30), IssuerTransactionPublicKey(0x31), From a19e2e487646ee13352aa7049e1cd5f797e20080 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Jan 2020 18:06:56 +0300 Subject: [PATCH 13/16] Updated on 2026-08-14 --- .../src/main/java/com/tangem/CardManager.kt | 15 +++++ .../com/tangem/commands/PurgeWalletCommand.kt | 58 +++++++++++++++++++ .../com/tangem/tangemtest/MainActivity.kt | 28 +++++++++ .../src/main/res/layout/activity_main.xml | 25 +++++++- 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt index 2c74c35b94..d581ae951c 100644 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ b/tangem-core/src/main/java/com/tangem/CardManager.kt @@ -142,6 +142,21 @@ class CardManager( runTask(task, cardId, callback) } + /** + * This command deletes all wallet data. If Is_Reusable flag is enabled during personalization, + + * If Is_Reusable flag is disabled, the card switches to ‘Purged’ state. + * ‘Purged’ state is final, it makes the card useless. + * @property cardId CID, Unique Tangem card ID number. + * @property cvc Optional 3-digit code printed on the card. Required if Use_CVC flag is set in Settings_Mask. + */ + fun purgeWallet(cardId: String, + callback: (result: TaskEvent) -> Unit) { + val purgeWalletCommand = PurgeWalletCommand(cardId) + val task = SingleCommandTask(purgeWalletCommand) + runTask(task, cardId, callback) + } + /** */ diff --git a/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt new file mode 100644 index 0000000000..ea9edb8949 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt @@ -0,0 +1,58 @@ +package com.tangem.commands + +import com.tangem.CardEnvironment +import com.tangem.common.apdu.CommandApdu +import com.tangem.common.apdu.Instruction +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.tlv.Tlv +import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvTag +import com.tangem.tasks.TaskError + +class PurgeWalletResponse( + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, + /** + * Current status of the card [1 - Empty, 2 - Loaded, 3- Purged] + */ + val status: CardStatus +) : CommandResponse + +/** +* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization, + + * If Is_Reusable flag is disabled, the card switches to ‘Purged’ state. + * ‘Purged’ state is final, it makes the card useless. + * @property cardId CID, Unique Tangem card ID number. + * @property cvc Optional 3-digit code printed on the card. Required if Use_CVC flag is set in Settings_Mask. + */ +class PurgeWalletCommand( + private val cardId: String +) : CommandSerializer() { + + override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + val tlvData = mutableListOf( + Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()), + Tlv(TlvTag.CardId, cardId.hexToBytes()), + Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256()) + ) + return CommandApdu(Instruction.PurgeWallet, tlvData) + } + + override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? { + val tlvData = responseApdu.getTlvData() ?: return null + + return try { + val mapper = TlvMapper(tlvData) + PurgeWalletResponse( + cardId = mapper.map(TlvTag.CardId), + status = mapper.map(TlvTag.Status)) + } catch (exception: Exception) { + throw TaskError.SerializeCommandError() + } + } +} \ No newline at end of file diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt index 018ca60ce5..52b2951309 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt @@ -45,6 +45,8 @@ class MainActivity : AppCompatActivity() { tv_card_cid?.text = cardId btn_sign.isEnabled = true btn_read_issuer_data.isEnabled = true + btn_purge_wallet.isEnabled = true + btn_create_wallet.isEnabled = true } } } @@ -103,6 +105,32 @@ class MainActivity : AppCompatActivity() { } } } + btn_purge_wallet?.setOnClickListener { _ -> + cardManager.purgeWallet( + cardId) { + when (it) { + is TaskEvent.Completion -> { + if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName } + } + is TaskEvent.Event -> runOnUiThread { + tv_card_cid?.text = it.data.status.name + } + } + } + } + btn_create_wallet?.setOnClickListener { _ -> + cardManager.createWallet( + cardId) { + when (it) { + is TaskEvent.Completion -> { + if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName } + } + is TaskEvent.Event -> runOnUiThread { + tv_card_cid?.text = it.data.status.name + } + } + } + } } private fun createSampleHashes(): Array { diff --git a/tangem-demo/src/main/res/layout/activity_main.xml b/tangem-demo/src/main/res/layout/activity_main.xml index a1cecca79b..5759633c21 100644 --- a/tangem-demo/src/main/res/layout/activity_main.xml +++ b/tangem-demo/src/main/res/layout/activity_main.xml @@ -9,12 +9,12 @@ +