From bf1c374582a4dc9fed6321ca045d8a82c8fbdcaf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2020 14:53:21 +0300 Subject: [PATCH 01/41] Updated on 2026-08-14 --- app/build.gradle | 3 + .../main/java/com/tangem/data/Blockchain.java | 3 +- .../tangem/data/network/ServerApiTokenEmv.kt | 43 +++ .../com/tangem/data/network/TokenEmvApi.kt | 11 + .../tangem/data/network/model/TokenEmvBody.kt | 15 + .../com/tangem/wallet/CoinEngineFactory.kt | 4 + .../com/tangem/wallet/EthTransaction.java | 2 +- .../com/tangem/wallet/token/TokenData.java | 28 +- .../tangem/wallet/tokenEmv/TokenEmvEngine.kt | 310 ++++++++++++++++++ 9 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/tangem/data/network/ServerApiTokenEmv.kt create mode 100644 app/src/main/java/com/tangem/data/network/TokenEmvApi.kt create mode 100644 app/src/main/java/com/tangem/data/network/model/TokenEmvBody.kt create mode 100644 app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt diff --git a/app/build.gradle b/app/build.gradle index c30dedd2c2..10bac3b34e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,4 +158,7 @@ dependencies { //dependencies for flow demo implementation 'io.grpc:grpc-okhttp:1.28.0' implementation 'io.grpc:grpc-stub:1.28.0' + + //dependencies for TokenEmvEngine + implementation 'com.github.walleth.kethereum:extensions_kotlin:0.81.4' } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index c9747e62f8..dee0c71b2c 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -32,7 +32,8 @@ public enum Blockchain { Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"), Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"), Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.ic_logo_tezos, "Tezos"), - FlowDemo("FLOW/demo", "", 1.0, R.drawable.tangem2, "Flow demo"); + FlowDemo("FLOW/demo", "", 1.0, R.drawable.tangem2, "Flow demo"), + TokenEmv("TTW", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/ServerApiTokenEmv.kt b/app/src/main/java/com/tangem/data/network/ServerApiTokenEmv.kt new file mode 100644 index 0000000000..25e3eb03d9 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiTokenEmv.kt @@ -0,0 +1,43 @@ +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.TokenEmvTransferBody +import com.tangem.tangem_card.util.Log +import io.reactivex.CompletableObserver +import io.reactivex.Single +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.BiConsumer +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory + +class ServerApiTokenEmv { + private val TAG = ServerApiTokenEmv::class.java.simpleName + + private val tangemServer = "" + + private val tokenEmvApi = Retrofit.Builder() + .baseUrl(tangemServer) + .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(ScalarsConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //logging for testing + .client(OkHttpClient.Builder().addInterceptor( + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + ).build()) + .build() + .create(TokenEmvApi::class.java) + + fun transfer(tokenEmvTransferBody: TokenEmvTransferBody, transferObserver: CompletableObserver) { + Log.i(TAG, "new transfer request") + + tokenEmvApi.transfer(tokenEmvTransferBody) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(transferObserver) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/TokenEmvApi.kt b/app/src/main/java/com/tangem/data/network/TokenEmvApi.kt new file mode 100644 index 0000000000..f885cad2c4 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/TokenEmvApi.kt @@ -0,0 +1,11 @@ +package com.tangem.data.network + +import com.tangem.data.network.model.TokenEmvTransferBody +import io.reactivex.Completable +import retrofit2.http.Body +import retrofit2.http.POST + +interface TokenEmvApi { + @POST("./") + fun transfer(@Body tokenEmvTransferBody: TokenEmvTransferBody): Completable +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/TokenEmvBody.kt b/app/src/main/java/com/tangem/data/network/model/TokenEmvBody.kt new file mode 100644 index 0000000000..cbfdcf038e --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TokenEmvBody.kt @@ -0,0 +1,15 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class TokenEmvTransferBody( + val contract: String, + val amount: String, + val recipient: String, + @SerializedName("fee_limit") + val feeLimit: String, + val sequence: Int, + val r: String, + val s: String, + val v: Int +) \ 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 310f567aea..8bfbb7f361 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -20,6 +20,7 @@ import com.tangem.wallet.rsk.RskEngine import com.tangem.wallet.rsk.RskTokenEngine import com.tangem.wallet.token.TokenEngine import com.tangem.wallet.tezos.TezosEngine +import com.tangem.wallet.tokenEmv.TokenEmvEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine import com.tangem.wallet.xlmTag.XlmTagEngine @@ -60,6 +61,7 @@ object CoinEngineFactory { Blockchain.Tezos -> TezosEngine() Blockchain.BitcoinDual -> BtcMultisigEngine() Blockchain.FlowDemo -> FlowDemoEngine() + Blockchain.TokenEmv -> TokenEmvEngine() else -> null } } @@ -109,6 +111,8 @@ object CoinEngineFactory { BtcMultisigEngine(context) else if (Blockchain.FlowDemo == context.blockchain) FlowDemoEngine(context) + else if (Blockchain.TokenEmv == context.blockchain) + TokenEmvEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/EthTransaction.java b/app/src/main/java/com/tangem/wallet/EthTransaction.java index c7ba6e98b3..48e86ecf6a 100644 --- a/app/src/main/java/com/tangem/wallet/EthTransaction.java +++ b/app/src/main/java/com/tangem/wallet/EthTransaction.java @@ -110,7 +110,7 @@ public class EthTransaction { return kec.digest(plainMsg); } - public int BruteRecoveryID2(ECDSASignatureETH sig, byte[] messageHash, byte[] thisKey) { + public static int BruteRecoveryID2(ECDSASignatureETH sig, byte[] messageHash, byte[] thisKey) { Log.e("ETH_KZ", BTCUtils.toHex(thisKey)); int recId = -1; for (int i = 0; i < 4; i++) { diff --git a/app/src/main/java/com/tangem/wallet/token/TokenData.java b/app/src/main/java/com/tangem/wallet/token/TokenData.java index ef67680617..7cf2c24255 100644 --- a/app/src/main/java/com/tangem/wallet/token/TokenData.java +++ b/app/src/main/java/com/tangem/wallet/token/TokenData.java @@ -8,30 +8,45 @@ import com.tangem.wallet.eth.EthData; public class TokenData extends EthData { private CoinEngine.InternalAmount balanceAlter = null; - + private Integer sequence = null; @Override public void clearInfo() { super.clearInfo(); balanceAlter = null; + sequence = null; } public CoinEngine.InternalAmount getBalanceAlterInInternalUnits() { return balanceAlter; } + public void setBalanceAlterInInternalUnits(CoinEngine.InternalAmount value) { balanceAlter = value; } + public Integer getSequence() { + return sequence; + } + + public void setSequence(Integer sequence) { + this.sequence = sequence; + } + @Override public void loadFromBundle(Bundle B) { super.loadFromBundle(B); - if( B.containsKey("BalanceDecimalAlter" )) { + if (B.containsKey("BalanceDecimalAlter")) { balanceAlter = new CoinEngine.InternalAmount(B.getString("BalanceDecimalAlter"), "wei"); - }else{ - balanceAlter=null; + } else { + balanceAlter = null; + } + if (B.containsKey("Sequence")) { + sequence = B.getInt("Sequence"); + } else { + sequence = null; } } @@ -40,9 +55,12 @@ public class TokenData extends EthData { super.saveToBundle(B); try { - if( balanceAlter!=null ) { + if (balanceAlter != null) { B.putString("BalanceDecimalAlter", balanceAlter.toString()); } + if (sequence != null) { + B.putInt("Sequence", sequence); + } } catch (Exception e) { Log.e("Can't save to bundle ", e.getMessage()); } diff --git a/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt b/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt new file mode 100644 index 0000000000..9813ee6d80 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt @@ -0,0 +1,310 @@ +package com.tangem.wallet.tokenEmv + +import android.text.InputFilter +import android.util.Log +import com.google.gson.Gson +import com.tangem.data.Blockchain +import com.tangem.data.network.ServerApiTokenEmv +import com.tangem.data.network.model.TokenEmvTransferBody +import com.tangem.tangem_card.data.TangemCard +import com.tangem.tangem_card.reader.CardProtocol.TangemException +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 com.tangem.wallet.EthTransaction.BruteRecoveryID2 +import com.tangem.wallet.token.TokenEngine +import io.reactivex.observers.DisposableCompletableObserver +import org.apache.commons.lang3.SerializationUtils +import org.bitcoinj.core.ECKey +import org.kethereum.extensions.toBytesPadded +import org.kethereum.extensions.toFixedLengthByteArray +import java.math.BigInteger +import java.util.* + +class TokenEmvEngine : TokenEngine { + constructor() : super() + constructor(context: TangemContext) : super(context) + + private val TAG = TokenEmvEngine::class.java.simpleName + + private fun hasLinkedContract(): Boolean { + return ctx.card.issuerData != null && ctx.card.issuerData.size == 20 + } + + override fun getBlockchain(): Blockchain { + return Blockchain.TokenEmv + } + + override fun getBalance(): Amount? { + return if (!hasBalanceInfo()) { + null + } else { + convertToAmount(coinData.balanceInInternalUnits) + } + } + + override fun getBalanceHTML(): String? { + return if (hasLinkedContract()) { + if (balance != null) { + balance!!.toDescriptionString(tokenDecimals) + } else { + "" + } + } else { + "NO LINKED CONTRACT" + } + } + + override fun getBalanceCurrency(): String? { + return ctx.card.getTokenSymbol() + } + + override fun getAmountInputFilters(): Array? { + return arrayOf(DecimalDigitsInputFilter(tokenDecimals)) + } + + override fun getFeeCurrency(): String? { + return balanceCurrency + } + + override fun isBalanceNotZero(): Boolean { + if (coinData == null) return false + return if (coinData.balanceInInternalUnits == null) { + false + } else { + coinData.balanceInInternalUnits.notZero() + } + } + + override fun getBalanceEquivalent(): String? { + return "" + } + + override fun evaluateFeeEquivalent(fee: String?): String? { + return "" + } + + override fun defineWallet() { + try { + if (hasLinkedContract()) { + ctx.coinData.wallet = String.format("0x%s", BTCUtils.toHex(ctx.card.issuerData)) + } else { + ctx.coinData.wallet = calculateAddress(ctx.card.walletPublicKey) + } + } catch (e: Exception) { + ctx.coinData.wallet = "ERROR" + throw TangemException("Can't define wallet address") + } + } + + override fun hasBalanceInfo(): Boolean { + return coinData.balanceInInternalUnits != 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 checkNewTransactionAmountAndFee(amount: Amount, fee: Amount?, isFeeIncluded: Boolean): Boolean { + try { + if (isFeeIncluded && (amount > balance || amount < fee)) return false + if (!isFeeIncluded && amount.add(fee) > balance) return false + } catch (e: NumberFormatException) { + e.printStackTrace() + } + return true + } + + override fun constructTransaction(amountValue: Amount, feeValue: Amount?, IncFee: Boolean, targetAddress: String?): SignTask.TransactionToSign? { + val functionBytes = "transfer".toByteArray() + + val contractHex = coinData.wallet.substring(2) + val contractBytes = Util.hexToBytes(contractHex) + val recipientHex = targetAddress!!.substring(2) + val recipientBytes = Util.hexToBytes(recipientHex) + + val amountBytes = convertToInternalAmount(amountValue).toBigInteger().toBytesPadded(32) + val feeLimitBytes = convertToInternalAmount(feeValue).toBigInteger().toBytesPadded(32) + + val sequenceBytes = coinData.sequence.toBigInteger().toBytesPadded(4) + + val hashToSign = Keccak256().digest(contractBytes + functionBytes + + amountBytes + recipientBytes + feeLimitBytes + sequenceBytes) + + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash + } + + override fun getHashesToSign(): Array { + return arrayOf(hashToSign) + } + + @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 r = BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32)) + var s: BigInteger? = BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64)) + s = CryptoUtil.toCanonicalised(s) + + val publicKey = ctx.getCard().getWalletPublicKey() + + val verified = ECKey.verify(hashToSign, ECKey.ECDSASignature(r, s), publicKey) + if (!verified) { + Log.e(this.javaClass.simpleName + "-CHECK", "sign Failed.") + } + + val v = BruteRecoveryID2(ECDSASignatureETH(r, s), hashToSign, publicKey) + if (v != 27 && v != 28) { + Log.e(TAG, "invalid v") + throw java.lang.Exception("Error in " + this.javaClass.simpleName + " - invalid v") + } + Log.e(TAG, this.javaClass.simpleName + " V: " + v.toString()) + + var rBytes = r.toByteArray() + if (rBytes.size == 33) { + rBytes = rBytes.copyOfRange(1,33) + } + val sBytes = s.toByteArray() + + val tokenEmvTransferBody = TokenEmvTransferBody( + contract = contractHex, + amount = Util.byteArrayToHexString(amountBytes), + recipient = recipientHex, + feeLimit = Util.byteArrayToHexString(feeLimitBytes), + sequence = coinData.sequence, + r = Util.byteArrayToHexString(rBytes), + s = Util.byteArrayToHexString(sBytes), + v = v + ) + + val jsonBody = Gson().toJson(tokenEmvTransferBody) + val serializedBody = SerializationUtils.serialize(jsonBody) + + notifyOnNeedSendTransaction(serializedBody) + return serializedBody + } + } + } + + override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray?) { + val jsonBody = SerializationUtils.deserialize(txForSend) + val tokenEmvTransferBody = Gson().fromJson(jsonBody, TokenEmvTransferBody::class.java) + + val transferObserver = object : DisposableCompletableObserver() { + override fun onComplete() { + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onError(e: Throwable) { + blockchainRequestsCallbacks.onComplete(false) + } + } + + ServerApiTokenEmv().transfer(tokenEmvTransferBody, transferObserver) + } + + // TODO: move all below to the server + fun constructTransfer(tokenEmvTransferBody: TokenEmvTransferBody, feeValue: Amount?): SignTask.TransactionToSign { + val contractAddress = tokenEmvTransferBody.contract + + val nonceValue = coinData.confirmedTXCount + val weiFee: BigInteger = convertToInternalAmount(feeValue).toBigIntegerExact() //TODO: get fee as usual but set multiplier m = BigInteger.valueOf(100000), use normal? + var gasLimitInt = 100000 + + val gasPrice = weiFee.divide(BigInteger.valueOf(gasLimitInt.toLong())) + val gasLimit = BigInteger.valueOf(gasLimitInt.toLong()) + val chainId = this.chainIdNum + val amountZero = BigInteger.ZERO + + val transferSignature = "transfer(uint256,address,uint8,bytes32,bytes32,uint256,uint256,uint32)".toByteArray() + val selector = Keccak256().digest(transferSignature).copyOf(4) + + val tokenAmount = Util.hexToBytes(tokenEmvTransferBody.amount).toFixedLengthByteArray(32) + val recipient = Util.hexToBytes(tokenEmvTransferBody.recipient).toFixedLengthByteArray(32) + val sigV = BigInteger.valueOf(tokenEmvTransferBody.v.toLong()).toBytesPadded(32) + val sigR = Util.hexToBytes(tokenEmvTransferBody.r).toFixedLengthByteArray(32) + val sigS = Util.hexToBytes(tokenEmvTransferBody.s).toFixedLengthByteArray(32) + val tokenFee = BigInteger.valueOf(1).toBytesPadded(32) //TODO: calculate using equivalent data + val tokenFeeLimit = Util.hexToBytes(tokenEmvTransferBody.feeLimit).toFixedLengthByteArray(32) + val sequence = BigInteger.valueOf(tokenEmvTransferBody.sequence.toLong()).toBytesPadded(32) + + val data = selector + tokenAmount + recipient + sigV + sigR + sigS + tokenFee + tokenFeeLimit + sequence + + val tx = EthTransaction.create(contractAddress, amountZero, nonceValue, gasPrice, gasLimit, chainId, data) + + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash + } + + override fun getHashesToSign(): Array { + return arrayOf(tx.rawHash) + } + + @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 publicKey = ctx.getCard().getWalletPublicKey() + val for_hash = tx.rawHash + val r = BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32)) + var s: BigInteger? = BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64)) + s = CryptoUtil.toCanonicalised(s) + val f = ECKey.verify(for_hash, ECKey.ECDSASignature(r, s), publicKey) + if (!f) { + Log.e(this.javaClass.simpleName + "-CHECK", "sign Failed.") + } + tx.signature = ECDSASignatureETH(r, s) + val v = BruteRecoveryID2(tx.signature, for_hash, publicKey) + if (v != 27 && v != 28) { + Log.e(TAG, "invalid v") + throw java.lang.Exception("Error in " + this.javaClass.simpleName + " - invalid v") + } + tx.signature.v = v.toByte() + Log.e(TAG, this.javaClass.simpleName + " V: " + v.toString()) + val txForSend = tx.encoded + notifyOnNeedSendTransaction(txForSend) + return txForSend + } + } + } +} \ No newline at end of file From 44c69929b22bb4936fab2c5cda336c2c43a9a251 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2020 21:16:17 +0300 Subject: [PATCH 02/41] Updated on 2026-08-14 --- .../java/com/tangem/data/network/Server.java | 8 ++ .../tangem/data/network/ServerApiInfura.java | 2 + .../com/tangem/data/network/ServerURL.java | 1 + .../java/com/tangem/di/NetworkComponent.kt | 3 + .../main/java/com/tangem/di/NetworkModule.kt | 12 +++ .../tangem/wallet/tokenEmv/TokenEmvEngine.kt | 89 ++++++++++++++++++- 6 files changed, 113 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/data/network/Server.java b/app/src/main/java/com/tangem/data/network/Server.java index c31e4cf46b..8ef83ebd52 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -40,6 +40,14 @@ public class Server { } } + public static class ApiInfuraRopsten { + public static final String URL_INFURA_ROPSTEN = ServerURL.API_INFURA_ROPSTEN; + + public static class Method { + public static final String MAIN = "v3/613a0b14833145968b1f656240c7d245"; + } + } + public static class ApiSoChain { public static final String URL = ServerURL.API_SOCHAIN_V2; diff --git a/app/src/main/java/com/tangem/data/network/ServerApiInfura.java b/app/src/main/java/com/tangem/data/network/ServerApiInfura.java index e168293fa4..29ded17af1 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiInfura.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiInfura.java @@ -42,6 +42,8 @@ public class ServerApiInfura { public ServerApiInfura(Blockchain blockchain) { if (blockchain == Blockchain.EthereumTestNet) { infuraApi = App.Companion.getNetworkComponent().getRetrofitInfuraTestnet().create(InfuraApi.class); + } else if (blockchain == Blockchain.TokenEmv) { + infuraApi = App.Companion.getNetworkComponent().getRetrofitInfuraRopsten().create(InfuraApi.class); } } diff --git a/app/src/main/java/com/tangem/data/network/ServerURL.java b/app/src/main/java/com/tangem/data/network/ServerURL.java index b30fc802ce..dedda6858d 100644 --- a/app/src/main/java/com/tangem/data/network/ServerURL.java +++ b/app/src/main/java/com/tangem/data/network/ServerURL.java @@ -5,6 +5,7 @@ class ServerURL { static final String API_COINMARKETCAP = "https://pro-api.coinmarketcap.com/"; static final String API_INFURA = "https://mainnet.infura.io/"; static final String API_INFURA_TESTNET = "https://rinkeby.infura.io/"; + static final String API_INFURA_ROPSTEN = "https://ropsten.infura.io/"; static final String API_SOCHAIN_V2 = "https://chain.so/"; static final String API_ESTIMATEFEE = "https://estimatefee.com/"; static final String API_UPDATE_VERSION = "https://raw.githubusercontent.com/"; diff --git a/app/src/main/java/com/tangem/di/NetworkComponent.kt b/app/src/main/java/com/tangem/di/NetworkComponent.kt index b09c235aba..3d100a99e6 100644 --- a/app/src/main/java/com/tangem/di/NetworkComponent.kt +++ b/app/src/main/java/com/tangem/di/NetworkComponent.kt @@ -17,6 +17,9 @@ interface NetworkComponent { @get:Named(Server.ApiInfuraTestnet.URL_INFURA_TESTNET) val retrofitInfuraTestnet: Retrofit + @get:Named(Server.ApiInfuraRopsten.URL_INFURA_ROPSTEN) + val retrofitInfuraRopsten: Retrofit + @get:Named(Server.ApiMaticTesnet.URL_MATIC_TESTNET) val retrofitMaticTesnet: Retrofit diff --git a/app/src/main/java/com/tangem/di/NetworkModule.kt b/app/src/main/java/com/tangem/di/NetworkModule.kt index 0358c3bd9b..339a0ca5d8 100644 --- a/app/src/main/java/com/tangem/di/NetworkModule.kt +++ b/app/src/main/java/com/tangem/di/NetworkModule.kt @@ -44,6 +44,18 @@ internal class NetworkModule { return builder.build() } + @Singleton + @Provides + @Named(Server.ApiInfuraRopsten.URL_INFURA_ROPSTEN) + fun provideRetrofitInfuraRopsten(): Retrofit { + val builder = Retrofit.Builder() + .baseUrl(Server.ApiInfuraRopsten.URL_INFURA_ROPSTEN) + .addConverterFactory(GsonConverterFactory.create()) + if (BuildConfig.DEBUG) + builder.client(createOkHttpClient()) + return builder.build() + } + @Singleton @Provides @Named(Server.ApiRootstock.URL_ROOTSTOCK) diff --git a/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt b/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt index 9813ee6d80..225094e7a8 100644 --- a/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt +++ b/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt @@ -4,7 +4,9 @@ import android.text.InputFilter import android.util.Log import com.google.gson.Gson import com.tangem.data.Blockchain +import com.tangem.data.network.ServerApiInfura import com.tangem.data.network.ServerApiTokenEmv +import com.tangem.data.network.model.InfuraResponse import com.tangem.data.network.model.TokenEmvTransferBody import com.tangem.tangem_card.data.TangemCard import com.tangem.tangem_card.reader.CardProtocol.TangemException @@ -20,6 +22,7 @@ import org.apache.commons.lang3.SerializationUtils import org.bitcoinj.core.ECKey import org.kethereum.extensions.toBytesPadded import org.kethereum.extensions.toFixedLengthByteArray +import java.math.BigDecimal import java.math.BigInteger import java.util.* @@ -30,13 +33,17 @@ class TokenEmvEngine : TokenEngine { private val TAG = TokenEmvEngine::class.java.simpleName private fun hasLinkedContract(): Boolean { - return ctx.card.issuerData != null && ctx.card.issuerData.size == 20 + return ctx.card.issuerData != null && ctx.card.issuerData.size == 44 } override fun getBlockchain(): Blockchain { return Blockchain.TokenEmv } + override fun getChainIdNum(): Int { + return EthTransaction.ChainEnum.Mainnet.value + } + override fun getBalance(): Amount? { return if (!hasBalanceInfo()) { null @@ -89,7 +96,8 @@ class TokenEmvEngine : TokenEngine { override fun defineWallet() { try { if (hasLinkedContract()) { - ctx.coinData.wallet = String.format("0x%s", BTCUtils.toHex(ctx.card.issuerData)) + val issuerData = ctx.card.issuerData + ctx.coinData.wallet = String(issuerData.copyOfRange(2, issuerData.size)) } else { ctx.coinData.wallet = calculateAddress(ctx.card.walletPublicKey) } @@ -212,6 +220,79 @@ class TokenEmvEngine : TokenEngine { } } + override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + val serverApiInfura = ServerApiInfura(ctx.blockchain) + + val responseListener: ServerApiInfura.ResponseListener = object : ServerApiInfura.ResponseListener { + override fun onSuccess(method: String, infuraResponse: InfuraResponse) { + when (method) { + ServerApiInfura.INFURA_ETH_GET_TRANSACTION_COUNT -> { + var nonce = infuraResponse.result + nonce = nonce!!.substring(2) + val count = BigInteger(nonce, 16) + coinData.confirmedTXCount = count + if (serverApiInfura.isRequestsSequenceCompleted) { //getting balances after checking for pending to avoid showing old balance as verified + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_CALL, 67, coinData.wallet, getContractAddress(ctx.card), "") + } + } + ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT -> { + var pending = infuraResponse.result + pending = pending!!.substring(2) + val count = BigInteger(pending, 16) + coinData.unconfirmedTXCount = count + if (serverApiInfura.isRequestsSequenceCompleted) { //getting balances after checking for pending to avoid showing old balance as verified + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_CALL, 67, coinData.wallet, getContractAddress(ctx.card), "") + } + } + ServerApiInfura.INFURA_ETH_CALL -> { + try { + var balanceCap = infuraResponse.result + balanceCap = balanceCap!!.substring(2) + val l = BigInteger(balanceCap, 16) + coinData.balanceInInternalUnits = InternalAmount(l, ctx.card.tokenSymbol) + coinData.isBalanceReceived = true + // Log.i("$TAG eth_call", balanceCap) + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + } + } + if (serverApiInfura.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + + override fun onFail(method: String, message: String) { + Log.e(TAG, "onFail: $method $message") + ctx.error = message + if (serverApiInfura.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(false) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + + serverApiInfura.setResponseListener(responseListener) + + if (validateAddress(getContractAddress(ctx.card))) { + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_TRANSACTION_COUNT, 67, coinData.wallet, "", "") + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, coinData.wallet, "", "") + } else { + ctx.error = "Smart contract address not defined" + blockchainRequestsCallbacks.onComplete(false) + } + } + + override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String?, amount: Amount) { + val fee = Amount(BigDecimal.ONE, balanceCurrency) + coinData.minFee = fee + coinData.normalFee = fee + coinData.maxFee = fee + } + override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray?) { val jsonBody = SerializationUtils.deserialize(txForSend) val tokenEmvTransferBody = Gson().fromJson(jsonBody, TokenEmvTransferBody::class.java) @@ -229,6 +310,10 @@ class TokenEmvEngine : TokenEngine { ServerApiTokenEmv().transfer(tokenEmvTransferBody, transferObserver) } + override fun allowSelectFeeLevel(): Boolean { + return false + } + // TODO: move all below to the server fun constructTransfer(tokenEmvTransferBody: TokenEmvTransferBody, feeValue: Amount?): SignTask.TransactionToSign { val contractAddress = tokenEmvTransferBody.contract From 402284dd27c55a04506417314f3b4255d006865a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2020 13:33:44 +0300 Subject: [PATCH 03/41] Updated on 2026-08-14 --- tangem-demo/build.gradle | 5 +- .../tangemtest/_main/entryPoint/RvActions.kt | 2 - .../ucase/ui/BaseCardActionFragment.kt | 16 +++-- .../ui/DepersonalizeActionFragment.kt | 3 - .../personalize/ui/PersonalizationFragment.kt | 71 ++++++++++++------- .../variants/responses/ui/ResponseFragment.kt | 2 +- .../variants/scan/ui/ScanActionFragment.kt | 32 ++++++++- .../variants/sign/ui/SignActionFragment.kt | 3 - .../main/res/layout/fg_action_card_scan.xml | 50 ------------- .../main/res/layout/fg_action_card_sign.xml | 40 ----------- ...lization.xml => fg_base_action_layout.xml} | 18 +---- .../src/main/res/layout/fg_card_response.xml | 2 +- .../src/main/res/layout/fg_depersonalize.xml | 40 ----------- .../main/res/layout/v_content_container.xml | 20 ++++++ .../main/res/layout/v_item_list_container.xml | 26 +++++++ .../src/main/res/navigation/navigation.xml | 8 +-- .../src/main/res/values/strings_scan.xml | 3 +- 17 files changed, 148 insertions(+), 193 deletions(-) delete mode 100644 tangem-demo/src/main/res/layout/fg_action_card_scan.xml delete mode 100644 tangem-demo/src/main/res/layout/fg_action_card_sign.xml rename tangem-demo/src/main/res/layout/{fg_personalization.xml => fg_base_action_layout.xml} (64%) delete mode 100644 tangem-demo/src/main/res/layout/fg_depersonalize.xml create mode 100644 tangem-demo/src/main/res/layout/v_content_container.xml create mode 100644 tangem-demo/src/main/res/layout/v_item_list_container.xml diff --git a/tangem-demo/build.gradle b/tangem-demo/build.gradle index fa9e64d58f..fd56546bed 100644 --- a/tangem-demo/build.gradle +++ b/tangem-demo/build.gradle @@ -46,10 +46,11 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4" implementation "androidx.navigation:navigation-fragment-ktx:2.2.1" implementation "androidx.navigation:navigation-ui-ktx:2.2.1" - implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01" + implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "com.google.android.material:material:1.2.0-alpha05" implementation "androidx.viewpager2:viewpager2:1.0.0" implementation 'com.google.code.gson:gson:2.8.6' - implementation 'com.github.gbIxaHue:eu4d:0.3.7' + implementation 'com.github.gbIxaHue:eu4d:0.3.8' } diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/_main/entryPoint/RvActions.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/_main/entryPoint/RvActions.kt index df045a572d..5433027e92 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/_main/entryPoint/RvActions.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/_main/entryPoint/RvActions.kt @@ -59,8 +59,6 @@ class RvActionsVH( containerDescription.visibility = if (wrapper.descriptionIsVisible) View.VISIBLE else View.GONE } - - override fun onDataBound(data: ActionType) {} } fun RecyclerView.ViewHolder.getString(@StringRes id: Int?, ifNull: String = ""): String { diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/ui/BaseCardActionFragment.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/ui/BaseCardActionFragment.kt index e9916b33f0..1449441016 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/ui/BaseCardActionFragment.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/ui/BaseCardActionFragment.kt @@ -8,6 +8,7 @@ import androidx.core.view.plusAssign import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Observer +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import com.tangem.TangemSdk import com.tangem.commands.Card @@ -28,7 +29,8 @@ import ru.dev.gbixahue.eu4d.lib.android.global.log.Log */ abstract class BaseCardActionFragment : BaseFragment(), ActionView { - protected lateinit var itemContainer: ViewGroup + protected lateinit var swrLayout: SwipeRefreshLayout + protected lateinit var contentContainer: ViewGroup protected lateinit var actionFab: FloatingActionButton protected abstract val itemsManager: ItemsManager @@ -38,6 +40,8 @@ abstract class BaseCardActionFragment : BaseFragment(), ActionView { private val paramsWidgetList = mutableListOf() + override fun getLayoutId(): Int = R.layout.fg_base_action_layout + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Log.d(this, "onViewCreated") @@ -47,7 +51,7 @@ abstract class BaseCardActionFragment : BaseFragment(), ActionView { actionVM.setCardManager(TangemSdk.init(requireActivity())) actionVM.attachToPayload(mutableMapOf(PayloadKey.actionView to this as ActionView)) - initFab() + initViews() createWidgets { widgetsWasCreated() subscribeToViewModelChanges() @@ -57,11 +61,13 @@ abstract class BaseCardActionFragment : BaseFragment(), ActionView { protected open fun widgetsWasCreated() {} protected open fun bindViews() { - itemContainer = mainView.findViewById(R.id.ll_container) + swrLayout = mainView.findViewById(R.id.swr_layout) + contentContainer = mainView.findViewById(R.id.ll_content_container) actionFab = mainView.findViewById(R.id.fab_action) } - protected open fun initFab() { + protected open fun initViews() { + swrLayout.isEnabled = false enableActionFab(false) actionFab.setOnClickListener { actionVM.invokeMainAction() } } @@ -70,7 +76,7 @@ abstract class BaseCardActionFragment : BaseFragment(), ActionView { Log.d(this, "createWidgets") actionVM.ldItemList.observe(viewLifecycleOwner, Observer { itemList -> itemList.forEach { param -> - val widget = ParameterWidget(inflateParamView(itemContainer), param) + val widget = ParameterWidget(inflateParamView(contentContainer), param) widget.onValueChanged = { id, value -> actionVM.userChangedItem(id, value) } widget.onActionBtnClickListener = actionVM.getItemAction(param.id) paramsWidgetList.add(widget) diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt index e2ae9cdbdd..107ab9b3b3 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt @@ -1,6 +1,5 @@ package com.tangem.tangemtest.ucase.variants.depersonalize.ui -import com.tangem.tangemtest.R import com.tangem.tangemtest.ucase.domain.paramsManager.ItemsManager import com.tangem.tangemtest.ucase.domain.paramsManager.managers.DepersonalizeItemsManager import com.tangem.tangemtest.ucase.ui.BaseCardActionFragment @@ -11,6 +10,4 @@ import com.tangem.tangemtest.ucase.ui.BaseCardActionFragment class DepersonalizeActionFragment : BaseCardActionFragment() { override val itemsManager: ItemsManager by lazy { DepersonalizeItemsManager() } - - override fun getLayoutId(): Int = R.layout.fg_depersonalize } \ No newline at end of file diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/personalize/ui/PersonalizationFragment.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/personalize/ui/PersonalizationFragment.kt index 40133ebfd6..313410115c 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/personalize/ui/PersonalizationFragment.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/ucase/variants/personalize/ui/PersonalizationFragment.kt @@ -4,7 +4,10 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.LinearLayout import androidx.lifecycle.Observer +import androidx.transition.Fade +import androidx.transition.TransitionManager import com.tangem.commands.Card import com.tangem.tangemtest.R import com.tangem.tangemtest._arch.structure.Id @@ -24,6 +27,8 @@ import com.tangem.tangemtest.ucase.variants.personalize.PersonalizationConfigSto import com.tangem.tangemtest.ucase.variants.personalize.ui.widgets.PersonalizationItemBuilder import ru.dev.gbixahue.eu4d.lib.android._android.views.inflate import ru.dev.gbixahue.eu4d.lib.android.global.log.Log +import ru.dev.gbixahue.eu4d.lib.android.global.threading.postUI +import ru.dev.gbixahue.eu4d.lib.android.global.threading.postWork /** [REDACTED_AUTHOR] @@ -32,17 +37,56 @@ class PersonalizationFragment : BaseCardActionFragment() { override val itemsManager: ItemsManager by lazy { PersonalizationItemsManager(PersonalizationConfigStore(requireContext())) } - override fun getLayoutId(): Int = R.layout.fg_personalization + override fun getLayoutId(): Int = R.layout.fg_base_action_layout override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycle.addObserver(itemsManager as PersonalizationItemsManager) } + override fun bindViews() { + super.bindViews() + swrLayout.isRefreshing = true + } + + override fun initViews() { + actionFab.setOnClickListener { actionVM.invokeMainAction() } + } + + override fun createWidgets(widgetCreatedCallback: () -> Unit) { + Log.d(this, "createWidgets") + val itemList = mutableListOf() + + val maxDelay = 500 + val timeStart = System.currentTimeMillis() + actionVM.ldItemList.observe(viewLifecycleOwner, Observer { list -> + Log.d(this, "ldBlockList size: ${list.size}") + itemList.addAll(list) + val llContainer = LinearLayout(requireContext()).apply { orientation = LinearLayout.VERTICAL } + postWork { + val builder = WidgetBuilder(PersonalizationItemBuilder()) + itemList.forEach { builder.build(it, llContainer) } + actionVM.attachToPayload(mutableMapOf( + PayloadKey.actionView to this as ActionView, + PayloadKey.itemList to itemList + )) + val timeEnd = System.currentTimeMillis() + val diff = timeEnd - timeStart + postUI(maxDelay - diff) { + TransitionManager.beginDelayedTransition(contentContainer, Fade()) + contentContainer.addView(llContainer) + widgetCreatedCallback() + swrLayout.isRefreshing = false + swrLayout.isEnabled = false + } + } + }) + } + override fun widgetsWasCreated() { super.widgetsWasCreated() - val btnContainer = itemContainer.inflate(R.layout.view_simple_button) + val btnContainer = contentContainer.inflate(R.layout.view_simple_button) val btn = btnContainer.findViewById