diff --git a/app/build.gradle b/app/build.gradle index c30dedd2c2..07bb7d859f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'com.google.firebase.crashlytics' +apply plugin: 'com.google.firebase.firebase-perf' apply plugin: 'com.google.gms.google-services' static def generateVersionName() { @@ -72,6 +73,7 @@ repositories { } configurations { all*.exclude group: 'com.google.guava', module: 'listenablefuture' + implementation.exclude module:'protobuf-lite' } dependencies { @@ -79,22 +81,22 @@ dependencies { implementation project(':tangem-sdk-old') implementation project(':server-android') - implementation 'androidx.navigation:navigation-fragment:2.1.0' - implementation 'androidx.navigation:navigation-ui-ktx:2.1.0' + implementation 'androidx.navigation:navigation-fragment:2.2.2' + implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0' - implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' - implementation "androidx.lifecycle:lifecycle-runtime:2.1.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation "androidx.lifecycle:lifecycle-runtime:2.2.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.fragment:fragment:1.1.0' + implementation 'androidx.fragment:fragment:1.2.4' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation "androidx.preference:preference:1.1.0" - implementation 'androidx.biometric:biometric:1.0.0-beta01' - implementation 'androidx.core:core-ktx:1.1.0' - implementation 'com.google.android.material:material:1.1.0-beta01' + implementation "androidx.preference:preference:1.1.1" + implementation 'androidx.biometric:biometric:1.0.1' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'com.google.android.material:material:1.1.0' - implementation 'com.google.dagger:dagger:2.21' + implementation 'com.google.dagger:dagger:2.24' kapt 'com.google.dagger:dagger-compiler:2.21' annotationProcessor 'com.google.dagger:dagger-compiler:2.21' implementation 'com.google.zxing:core:3.4.0' @@ -113,8 +115,8 @@ dependencies { implementation "com.orhanobut:hawk:2.0.1" implementation 'co.nstant.in:cbor:0.8' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.2' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3" // implementation 'org.bitcoinj:bitcoinj-parent:0.14.7' //TODO: is this needed? implementation 'org.bitcoinj:bitcoinj-core:0.15.2' implementation 'org.greenrobot:eventbus:3.1.1' @@ -126,8 +128,9 @@ dependencies { // implementation 'com.github.stellar:java-stellar-sdk:0.4.1' implementation 'net.i2p.crypto:eddsa:0.3.0' - implementation 'com.google.firebase:firebase-analytics:17.2.3' - implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta02' + implementation 'com.google.firebase:firebase-analytics:17.3.0' + implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' + implementation 'com.google.firebase:firebase-perf:19.0.6' implementation files('libs/ripple-core-0.0.1.jar') //4 dependencies for ripple-core TODO: move to module? @@ -158,4 +161,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/App.kt b/app/src/main/java/com/tangem/App.kt index ef34464937..ba734b7bba 100644 --- a/app/src/main/java/com/tangem/App.kt +++ b/app/src/main/java/com/tangem/App.kt @@ -2,8 +2,6 @@ package com.tangem import android.app.Application import androidx.appcompat.app.AppCompatDelegate -import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.tangem.data.dp.PrefsManager @@ -80,10 +78,7 @@ class App : Application() { ) } - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(Analytics.isEnabled()) - FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled( - Analytics.isEnabled() && !BuildConfig.DEBUG - ) + Analytics.setFirebaseEnabled(this) } private fun buildToastHelperComponent(): ToastHelperComponent { diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 0e0df81a93..402348982e 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/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/ServerApiTokenEmv.kt b/app/src/main/java/com/tangem/data/network/ServerApiTokenEmv.kt new file mode 100644 index 0000000000..d3fdba5f87 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiTokenEmv.kt @@ -0,0 +1,53 @@ +package com.tangem.data.network + +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import com.tangem.data.network.model.TokenEmvGetTransferFeeAnswer +import com.tangem.data.network.model.TokenEmvGetTransferFeeBody +import com.tangem.data.network.model.TokenEmvTransferAnswer +import com.tangem.data.network.model.TokenEmvTransferBody +import com.tangem.tangem_card.util.Log +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +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 +import java.util.concurrent.TimeUnit + +class ServerApiTokenEmv { + private val TAG = ServerApiTokenEmv::class.java.simpleName + + private val tangemServer = "https://emvsupport.appspot.com/" + + 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: SingleObserver) { + Log.i(TAG, "new transfer request") + + tokenEmvApi.transfer(tokenEmvTransferBody) + .timeout(30, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(transferObserver) + } + + fun getTransferFee(tokenEmvGetTransferFeeBody: TokenEmvGetTransferFeeBody, transferObserver: SingleObserver) { + Log.i(TAG, "new get transfer fee request") + + tokenEmvApi.getTransferFee(tokenEmvGetTransferFeeBody) + .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/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/data/network/TokenEmvApi.kt b/app/src/main/java/com/tangem/data/network/TokenEmvApi.kt new file mode 100644 index 0000000000..43e09897c0 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/TokenEmvApi.kt @@ -0,0 +1,17 @@ +package com.tangem.data.network + +import com.tangem.data.network.model.TokenEmvGetTransferFeeAnswer +import com.tangem.data.network.model.TokenEmvGetTransferFeeBody +import com.tangem.data.network.model.TokenEmvTransferAnswer +import com.tangem.data.network.model.TokenEmvTransferBody +import io.reactivex.Completable +import io.reactivex.Single +import retrofit2.http.Body +import retrofit2.http.POST + +interface TokenEmvApi { + @POST("./card/transfer") + fun transfer(@Body tokenEmvTransferBody: TokenEmvTransferBody): Single + @POST("./card/transfer/fee") + fun getTransferFee(@Body tokenEmvGetTransferFeeBody: TokenEmvGetTransferFeeBody): Single +} \ 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..ea89dd88d9 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TokenEmvBody.kt @@ -0,0 +1,36 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class TokenEmvTransferBody( + val CID: String, + val publicKey: String, + val amount: String, + val currency: String, + val recipient: String, + @SerializedName("fee_limit") + val feeLimit: String, + val sequence: Int, + val signature: String +) + +data class TokenEmvTransferAnswer( + val error: String?, + val errorCode: Int?, + val success: Boolean?, + val tx_id: String?, + val blockchain_tx_id: String? +) + +data class TokenEmvGetTransferFeeBody( + val CID: String, + val publicKey: String +) + +data class TokenEmvGetTransferFeeAnswer( + val error: String?, + val errorCode: Int?, + val success: Boolean?, + val fee: String?, + val currency: String? +) \ No newline at end of file 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/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index b8a9ddf427..d380de4fc2 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -22,6 +22,8 @@ import androidx.core.os.bundleOf import androidx.lifecycle.ViewModelProviders import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.perf.FirebasePerformance +import com.google.firebase.perf.metrics.Trace import com.tangem.App import com.tangem.Constant import com.tangem.data.Blockchain @@ -77,6 +79,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader override val layoutId = R.layout.fragment_main private lateinit var viewModel: MainViewModel private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation + private var tapTimerTrace: Trace? = null private var unsuccessReadCount = 0 private var task: CustomReadCardTask? = null private var lastTag: Tag? = null @@ -156,6 +159,9 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } override fun onResume() { + tapTimerTrace = FirebasePerformance.getInstance() + .newTrace(AnalyticsEvent.CARD_TAP_USER_TIMER.event) + tapTimerTrace?.start() super.onResume() nfcDeviceAntenna.animate() } @@ -167,6 +173,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader override fun onStop() { task?.cancel(true) + tapTimerTrace = null super.onStop() } @@ -248,6 +255,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } override fun onReadFinish(cardProtocol: CardProtocol?) { + tapTimerTrace?.stop() task = null if (cardProtocol != null) { if (cardProtocol.error == null) { @@ -338,7 +346,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader lastTag = null ReadCardInfoTask.resetLastReadInfo() - (activity as MainActivity).nfcManager.notifyReadResult(false) + (activity as? MainActivity)?.nfcManager?.notifyReadResult(false) } } } diff --git a/app/src/main/java/com/tangem/ui/fragment/SettingsFragment.kt b/app/src/main/java/com/tangem/ui/fragment/SettingsFragment.kt index 0ff8fef384..84aad44a88 100644 --- a/app/src/main/java/com/tangem/ui/fragment/SettingsFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/SettingsFragment.kt @@ -11,7 +11,6 @@ import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.util.Analytics import com.tangem.util.AnalyticsEvent import com.tangem.wallet.BuildConfig @@ -48,15 +47,13 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == getString(R.string.pref_analytics)) { - activity?.let { - val analytics = FirebaseAnalytics.getInstance(it) + activity?.let {activity -> + val analytics = FirebaseAnalytics.getInstance(activity) if (!Analytics.isEnabled()) { analytics.logEvent(AnalyticsEvent.ANALYTICS_TURNED_OFF.event, bundleOf()) } - analytics.setAnalyticsCollectionEnabled(Analytics.isEnabled() && !BuildConfig.DEBUG) + Analytics.setFirebaseEnabled(activity) } - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(Analytics.isEnabled()) - } } diff --git a/app/src/main/java/com/tangem/util/Analytics.kt b/app/src/main/java/com/tangem/util/Analytics.kt index d1120edf6e..3885d57da0 100644 --- a/app/src/main/java/com/tangem/util/Analytics.kt +++ b/app/src/main/java/com/tangem/util/Analytics.kt @@ -1,8 +1,13 @@ package com.tangem.util +import android.content.Context import android.os.Bundle import androidx.core.os.bundleOf +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.perf.FirebasePerformance import com.tangem.data.dp.PrefsManager +import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import com.tangem.wallet.TangemContext @@ -13,6 +18,7 @@ enum class AnalyticsEvent(val event: String) { READY_TO_SIGN("ready_to_sign"), SIGNED("signed"), ANALYTICS_TURNED_OFF("analytics_turned_off"), + CARD_TAP_USER_TIMER("CardTapUserTimer"), ; } @@ -36,5 +42,13 @@ class Analytics { fun isEnabled(): Boolean { return PrefsManager.getInstance().getSettingsBoolean(R.string.pref_analytics, true) } + + fun setFirebaseEnabled(context: Context, enable: Boolean = isEnabled()) { + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enable) + FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled( + enable && !BuildConfig.DEBUG + ) + FirebasePerformance.getInstance().isPerformanceCollectionEnabled = enable + } } } 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..77e0c7ac42 100644 --- a/app/src/main/java/com/tangem/wallet/token/TokenData.java +++ b/app/src/main/java/com/tangem/wallet/token/TokenData.java @@ -8,31 +8,46 @@ 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; +// } } @Override @@ -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..c63851d57c --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tokenEmv/TokenEmvEngine.kt @@ -0,0 +1,349 @@ +package com.tangem.wallet.tokenEmv + +import android.net.Uri +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.* +import com.tangem.tangem_card.data.TangemCard +import com.tangem.tangem_card.reader.CardProtocol.TangemException +import com.tangem.tangem_card.reader.TLV +import com.tangem.tangem_card.reader.TLVList +import com.tangem.tangem_card.tasks.SignTask +import com.tangem.tangem_card.util.Util +import com.tangem.util.DecimalDigitsInputFilter +import com.tangem.wallet.EthTransaction +import com.tangem.wallet.Keccak256 +import com.tangem.wallet.R +import com.tangem.wallet.TangemContext +import com.tangem.wallet.token.TokenEngine +import io.reactivex.observers.DisposableSingleObserver +import org.apache.commons.lang3.SerializationUtils +import org.kethereum.extensions.toBytesPadded +import java.math.BigInteger + +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 == 44 + } + + override fun getBlockchain(): Blockchain { + return Blockchain.TokenEmv + } + + override fun getChainIdNum(): Int { + return EthTransaction.ChainEnum.Ropsten.value + } + + 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 = TLVList.fromBytes(ctx.card.issuerData).getTLV(TLV.Tag.TAG_Token_Contract_Address).asString + } 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 getWalletExplorerUri(): Uri { + return Uri.parse("https://ropsten.etherscan.io/token/" + getContractAddress(ctx.card) + "?a=" + ctx.coinData.wallet) + } + + 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 sequence = ctx.card.SignedHashes + val sequenceBytes = 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( +// CID = Util.bytesToHex(ctx.card.cid), +// publicKey = Util.bytesToHex(ctx.card.walletPublicKey), +// contract = contractHex, +// amount = Util.byteArrayToHexString(amountBytes), +// recipient = recipientHex, +// feeLimit = Util.byteArrayToHexString(feeLimitBytes), +// sequence = sequence, +// r = Util.byteArrayToHexString(rBytes), +// s = Util.byteArrayToHexString(sBytes), +// v = v +// ) + + val tokenEmvTransferBody = TokenEmvTransferBody( + CID = Util.bytesToHex(ctx.card.cid), + publicKey = Util.bytesToHex(ctx.card.walletPublicKey), + amount = amountValue.toValueString(), + currency = amountValue.currency, + recipient = targetAddress, + feeLimit = feeValue!!.toValueString(), + sequence = sequence, + signature = Util.bytesToHex(signFromCard) + ) + + val jsonBody = Gson().toJson(tokenEmvTransferBody) + val serializedBody = SerializationUtils.serialize(jsonBody) + + notifyOnNeedSendTransaction(serializedBody) + return serializedBody + } + } + } + + override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + val serverApiInfura = ServerApiInfura(ctx.blockchain) + + val responseListener: ServerApiInfura.ResponseListener = object : ServerApiInfura.ResponseListener { + override fun onSuccess(method: String, infuraResponse: InfuraResponse) { + 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) { + onFail(method, e.message ?: "invalid response") + } + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onFail(method: String, message: String) { + Log.e(TAG, "onFail: $method $message") + ctx.error = message + blockchainRequestsCallbacks.onComplete(false) + } + } + + serverApiInfura.setResponseListener(responseListener) + + if (validateAddress(getContractAddress(ctx.card))) { + serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_CALL, 67, coinData.wallet, getContractAddress(ctx.card), "") + } else { + ctx.error = "Smart contract address not defined" + blockchainRequestsCallbacks.onComplete(false) + } + } + + override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String?, amount: Amount) { + + val tokenEmvTransferBody = TokenEmvGetTransferFeeBody( + CID = Util.bytesToHex(ctx.card.cid), + publicKey = Util.bytesToHex(ctx.card.walletPublicKey) + ) + + val observer = object : DisposableSingleObserver() { + override fun onError(e: Throwable) { + ctx.error = "Can't get transfer fee, ${e.message}" + blockchainRequestsCallbacks.onComplete(false) + } + + override fun onSuccess(t: TokenEmvGetTransferFeeAnswer) { + if (t.success != null && t.success) { + ctx.error = null + coinData.minFee = Amount(t.fee, t.currency) + coinData.normalFee = Amount(t.fee, t.currency) + coinData.maxFee = Amount(t.fee, t.currency) + blockchainRequestsCallbacks.onComplete(true) + } else { + if (t.error != null) { + ctx.error = t.error + } else { + ctx.error = "Can't get transfer fee, code ${t.errorCode}" + } + blockchainRequestsCallbacks.onComplete(false) + } + } + + } + + ServerApiTokenEmv().getTransferFee(tokenEmvTransferBody, observer) + blockchainRequestsCallbacks.onComplete(true) + } + + override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray?) { + val jsonBody = SerializationUtils.deserialize(txForSend) + + Log.e(TAG, jsonBody) + + val tokenEmvTransferBody = Gson().fromJson(jsonBody, TokenEmvTransferBody::class.java) + + val observer = object : DisposableSingleObserver() { + override fun onError(e: Throwable) { + ctx.error = "Can't send transfer, ${e.message}" + blockchainRequestsCallbacks.onComplete(false) + } + + override fun onSuccess(t: TokenEmvTransferAnswer) { + if (t.success != null && t.success) { + ctx.error = null + blockchainRequestsCallbacks.onComplete(true) + } else { + if (t.error != null) { + ctx.error = t.error + } else { + ctx.error = "Can't send transfer, code ${t.errorCode}" + } + blockchainRequestsCallbacks.onComplete(false) + } + + } + } + + ServerApiTokenEmv().transfer(tokenEmvTransferBody, observer) + } + + override fun allowSelectFeeLevel(): Boolean { + return false + } + + override fun pendingTransactionTimeoutInSeconds(): Int { + return 60 + } + +} \ No newline at end of file diff --git a/blockchain/build.gradle b/blockchain/build.gradle index 1c30f07eaa..24ee17af7f 100644 --- a/blockchain/build.gradle +++ b/blockchain/build.gradle @@ -24,6 +24,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -80,6 +84,17 @@ dependencies { implementation 'org.json:json:20180813' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + //dependencies for binance + implementation 'com.google.protobuf:protobuf-java:3.6.1' + implementation 'io.grpc:grpc-protobuf:1.17.1' + implementation 'io.grpc:grpc-stub:1.17.1' + implementation 'commons-codec:commons-codec:1.10' + implementation 'org.apache.commons:commons-lang3:3.6' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.8' + implementation 'joda-time:joda-time:2.10.1' + testImplementation 'junit:junit:4.12' + implementation 'com.squareup.retrofit2:converter-jackson:2.6.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testImplementation "com.google.truth:truth:1.0" androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceAddress.kt new file mode 100644 index 0000000000..f0634cb9fd --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceAddress.kt @@ -0,0 +1,37 @@ +package com.tangem.blockchain.binance + +import com.tangem.blockchain.binance.client.encoding.Crypto +import com.tangem.common.extensions.calculateRipemd160 +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.toCompressedPublicKey +import org.bitcoinj.core.Bech32 + +class BinanceAddressFactory { + companion object { + fun makeAddress(walletPublicKey: ByteArray, testNet: Boolean = false): String { + val publicKeyHash = walletPublicKey.toCompressedPublicKey().calculateSha256().calculateRipemd160() + return if (testNet) { + Bech32.encode("tbnb", Crypto.convertBits(publicKeyHash, 0, publicKeyHash.size, 8, 5, false)) + } else { + Bech32.encode("bnb", Crypto.convertBits(publicKeyHash, 0, publicKeyHash.size, 8, 5, false)) + } + } + } +} + +class BinanceAddressValidator { + companion object { + fun validate(address: String, testNet: Boolean = false): Boolean { + return try { + Crypto.decodeAddress(address) + if (testNet) { + address.startsWith("tbnb1") + } else { + address.startsWith("bnb1") + } + } catch (exception: Exception) { + false + } + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceTransactionBuilder.kt new file mode 100644 index 0000000000..d9f770f25e --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceTransactionBuilder.kt @@ -0,0 +1,67 @@ +package com.tangem.blockchain.binance + +import com.tangem.blockchain.binance.client.BinanceDexApiClientFactory +import com.tangem.blockchain.binance.client.BinanceDexEnvironment +import com.tangem.blockchain.binance.client.domain.broadcast.TransactionOption +import com.tangem.blockchain.binance.client.domain.broadcast.Transfer +import com.tangem.blockchain.binance.client.encoding.message.MessageType +import com.tangem.blockchain.binance.client.encoding.message.TransactionRequestAssemblerExtSign +import com.tangem.blockchain.binance.client.encoding.message.TransferMessage +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.toCompressedPublicKey +import org.bitcoinj.core.ECKey +import org.bitcoinj.core.Utils +import java.math.BigInteger + +class BinanceTransactionBuilder(publicKey: ByteArray, isTestNet: Boolean) { + val chainId = if (isTestNet) "Binance-Chain-Nile" else "Binance-Chain-Tigris" + val prefixedPubKey = MessageType.PubKey.typePrefixBytes + 33.toByte() + publicKey.toCompressedPublicKey() + val client = BinanceDexApiClientFactory.newInstance().newRestClient( + if (isTestNet) BinanceDexEnvironment.PROD.baseUrl else BinanceDexEnvironment.TEST_NET.baseUrl + ) + + var accountNumber: Long? = null + var sequence: Long? = null + var transactionAssembler: TransactionRequestAssemblerExtSign? = null + var transferMessage: TransferMessage? = null + + fun buildToSign(transactionData: TransactionData): ByteArray { + val transfer = Transfer() + transfer.coin = transactionData.amount.currencySymbol + transfer.fromAddress = transactionData.sourceAddress + transfer.toAddress = transactionData.destinationAddress + transfer.amount = transactionData.amount.value!! + .setScale(Blockchain.Binance.decimals.toInt()).toPlainString() + + val options = TransactionOption.DEFAULT_INSTANCE + + val accountData = BinanceAccountData(chainId, accountNumber!!, sequence!!) + + transactionAssembler = client.prepareTransfer(transfer, accountData, prefixedPubKey, options, true) + transferMessage = transactionAssembler!!.createTransferMessage(transfer) + + return transactionAssembler!!.prepareForSign(transferMessage).calculateSha256() + } + + fun buildToSend(signature: ByteArray): ByteArray { + val r = BigInteger(1, signature.copyOfRange(0, 32)) + val s = BigInteger(1, signature.copyOfRange(32, 64)) + val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s + + //bigIntegerToBytes cuts leading zero if present + val canonicalSignature = Utils.bigIntegerToBytes(r, 32) + Utils.bigIntegerToBytes(canonicalS, 32) + val encodedSignature = transactionAssembler!!.encodeSignature(canonicalSignature) + + val encodedTransferMessage = transactionAssembler!!.encodeTransferMessage(transferMessage) + + return transactionAssembler!!.encodeStdTx(encodedTransferMessage, encodedSignature) + } +} + +data class BinanceAccountData( + val chainId: String, + val accountNumber: Long, + val sequence: Long +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceWalletManager.kt new file mode 100644 index 0000000000..5c131c16e0 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/binance/BinanceWalletManager.kt @@ -0,0 +1,71 @@ +package com.tangem.blockchain.binance + +import android.util.Log +import com.tangem.blockchain.binance.network.BinanceInfoResponse +import com.tangem.blockchain.binance.network.BinanceNetworkManager +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.wallets.CurrencyWallet +import com.tangem.common.CompletionResult + +class BinanceWalletManager( + private val cardId: String, + walletPublicKey: ByteArray, + override var wallet: CurrencyWallet, + isTestNet: Boolean = false +) : WalletManager, + TransactionSender, + FeeProvider { + + override val blockchain = if (isTestNet) Blockchain.BinanceTestnet else Blockchain.Binance + private val address = blockchain.makeAddress(walletPublicKey) + private val transactionBuilder = BinanceTransactionBuilder(walletPublicKey, isTestNet) + private val networkManager = BinanceNetworkManager(isTestNet) + + init { + wallet.balances[AmountType.Coin] = Amount(null, blockchain, address) + } + + override suspend fun update() { + val result = networkManager.getInfo(address) + when (result) { + is Result.Success -> updateWallet(result.data) + is Result.Failure -> updateError(result.error) + } + } + + private fun updateWallet(response: BinanceInfoResponse) { + Log.d(this::class.java.simpleName, "Balance is ${response.balance}") + wallet.balances[AmountType.Coin]?.value = response.balance + + transactionBuilder.accountNumber = response.accountNumber + transactionBuilder.sequence = response.sequence + } + + private fun updateError(error: Throwable?) { + Log.e(this::class.java.simpleName, error?.message ?: "") + if (error != null) throw error + } + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + val transactionHash = transactionBuilder.buildToSign(transactionData) + + when (val signerResponse = signer.sign(arrayOf(transactionHash), cardId)) { + is CompletionResult.Success -> { + val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature) + return networkManager.sendTransaction(transactionToSend) + } + is CompletionResult.Failure -> return SimpleResult.Failure(signerResponse.error) + } + } + + override suspend fun getFee(amount: Amount, destination: String): Result> { + when (val result = networkManager.getFee()) { + is Result.Success -> return Result.Success(listOf(Amount(result.data, blockchain))) + is Result.Failure -> return result + } + } + + +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/binance/client/BinanceDexApi.java b/blockchain/src/main/java/com/tangem/blockchain/binance/client/BinanceDexApi.java new file mode 100644 index 0000000000..a7fdf9f46a --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/binance/client/BinanceDexApi.java @@ -0,0 +1,84 @@ +package com.tangem.blockchain.binance.client; + +import java.util.List; + +import com.tangem.blockchain.binance.client.domain.*; +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.http.*; + +import java.util.List; + +public interface BinanceDexApi { + @GET("/api/v1/time") + Call