From d35dc8a87b2a2e906ea3f7adbaf82e533da8b589 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Jan 2020 18:57:45 +0300 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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)