diff --git a/app/build.gradle b/app/build.gradle index 0c5338992b..c65ef4028a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,6 +65,8 @@ repositories { } dependencies { + implementation fileTree(include: ['*.aar'], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' @@ -75,9 +77,9 @@ dependencies { implementation 'com.google.android.play:core-ktx:1.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.tangem:blockchain:develop-11' - implementation 'com.tangem:core:develop-15' - implementation 'com.tangem:sdk:develop-15' + implementation 'com.tangem:blockchain:develop-17' + implementation 'com.tangem:core:develop-42' + implementation 'com.tangem:sdk:develop-42' // WebView implementation "androidx.browser:browser:1.3.0" @@ -106,6 +108,20 @@ dependencies { implementation 'com.google.firebase:firebase-config-ktx' implementation 'com.google.firebase:firebase-analytics-ktx' + // WalletConnect + implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' + + // Network and Json + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' + implementation 'com.squareup.moshi:moshi:1.9.2' + implementation "com.squareup.moshi:moshi-kotlin:1.9.2" + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' +// +// //Crypto + implementation "com.madgag.spongycastle:core:1.58.0.0" + + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.0.1" androidTestImplementation 'androidx.test.ext:junit:1.1.2' diff --git a/app/libs/walletconnect.aar b/app/libs/walletconnect.aar new file mode 100644 index 0000000000..129de60a79 Binary files /dev/null and b/app/libs/walletconnect.aar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ee1acb1fe8..97e4157f97 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,6 +78,14 @@ + + + + + + + + diff --git a/app/src/main/assets/erc20_tokens.json b/app/src/main/assets/erc20_tokens.json index 5f2bfeb629..22fe8bd802 100644 --- a/app/src/main/assets/erc20_tokens.json +++ b/app/src/main/assets/erc20_tokens.json @@ -882,6 +882,12 @@ "name" : "WAX", "contractAddress" : "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022" }, + { + "decimalCount" : 18, + "symbol" : "WETH", + "name" : "Wrapped Ether", + "contractAddress" : "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, { "decimalCount" : 18, "symbol" : "WPR", diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 78c6c7dbd6..544e1d7cda 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -2,8 +2,6 @@ package com.tangem.tap import android.content.Intent import android.content.pm.ActivityInfo -import android.nfc.NfcAdapter -import android.nfc.Tag import android.os.Bundle import android.util.Log import android.view.View @@ -14,10 +12,14 @@ import com.tangem.Config import com.tangem.TangemSdk import com.tangem.commands.common.card.CardType import com.tangem.tangem_sdk_new.extensions.init +import com.tangem.tap.common.DialogManager +import com.tangem.tap.common.IntentHandler +import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.home.redux.HomeAction import com.tangem.wallet.R import kotlinx.android.synthetic.main.activity_main.* @@ -53,9 +55,11 @@ private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler { } } -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), SnackbarHandler { private var snackbar: Snackbar? = null + private val dialogManager = DialogManager() + private val intentHandler = IntentHandler() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -65,43 +69,37 @@ class MainActivity : AppCompatActivity() { store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) tangemSdk = TangemSdk.init( - this, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java))) + this, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java))) ) tangemSdkManager = TangemSdkManager(this) + store.dispatch(WalletConnectAction.RestoreSessions) } override fun onResume() { super.onResume() notificationsHandler = NotificationsHandler(fragment_container) if (supportFragmentManager.backStackEntryCount == 0 || - store.state.globalState.scanNoteResponse == null) { + store.state.globalState.scanNoteResponse == null + ) { store.dispatch(HomeAction.CheckIfFirstLaunch) store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) } - handleBackgroundScan(intent) + intentHandler.handleIntent(intent) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - handleBackgroundScan(intent) + intentHandler.handleIntent(intent) } - private fun handleBackgroundScan(intent: Intent?) { - if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action || - NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action - )) { - val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) - if (tag != null) { - intent.action = null - store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) - store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) - store.dispatch(HomeAction.ReadCard) - } - } + override fun onStart() { + super.onStart() + dialogManager.onStart(this) } override fun onStop() { notificationsHandler = null + dialogManager.onStop() super.onStop() } @@ -110,11 +108,11 @@ class MainActivity : AppCompatActivity() { super.onDestroy() } - fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null) { + override fun showSnackbar(text: Int, buttonTitle: Int?, action: View.OnClickListener?) { if (snackbar != null) return snackbar = Snackbar.make( - fragment_container, getString(text), Snackbar.LENGTH_INDEFINITE + fragment_container, getString(text), Snackbar.LENGTH_INDEFINITE ) if (buttonTitle != null && action != null) { snackbar?.setAction(getString(buttonTitle), action) @@ -122,7 +120,7 @@ class MainActivity : AppCompatActivity() { snackbar?.show() } - fun dismissSnackbar() { + override fun dismissSnackbar() { snackbar?.dismiss() snackbar = null } diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index e458fd4e04..491f1c9cfb 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -15,6 +15,7 @@ import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tokens.CurrenciesRepository +import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.features.feedback.AdditionalEmailInfo import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.features.feedback.TangemLogCollector @@ -32,6 +33,7 @@ val store = Store( ) lateinit var preferencesStorage: PreferencesStorage lateinit var currenciesRepository: CurrenciesRepository +lateinit var walletConnectRepository: WalletConnectRepository class TapApplication : Application() { override fun onCreate() { @@ -52,6 +54,7 @@ class TapApplication : Application() { preferencesStorage = PreferencesStorage(this) PicassoHelper.initPicassoWithCaching(this) currenciesRepository = CurrenciesRepository(this) + walletConnectRepository = WalletConnectRepository(this) initFeedbackManager() loadConfigs() diff --git a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt index 0ca9b929bd..2ae1553d38 100644 --- a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt @@ -12,7 +12,7 @@ class CurrencyConverter( private val rateValue: BigDecimal, private val decimals: Int ) { - private val roundingMode = RoundingMode.DOWN + private val roundingMode = RoundingMode.HALF_UP fun toFiat(crypto: BigDecimal, fiatDecimals: Int = 2): BigDecimal { return toFiatUnscaled(crypto).setScale(fiatDecimals, roundingMode) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt new file mode 100644 index 0000000000..dd997b7997 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -0,0 +1,55 @@ +package com.tangem.tap.common + +import android.app.Dialog +import android.content.Context +import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog +import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog +import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog +import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog +import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog +import com.tangem.tap.store +import org.rekotlin.StoreSubscriber + +class DialogManager : StoreSubscriber { + var context: Context? = null + private var dialog: Dialog? = null + + fun onStart(context: Context) { + this.context = context + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.globalState == newState.globalState + }.select { it.globalState } + } + } + + fun onStop() { + this.context = null + store.unsubscribe(this) + } + + + override fun newState(state: GlobalState) { + if (state.dialog == null) { + dialog?.dismiss() + dialog = null + return + } + val context = context ?: return + if (dialog != null) return + + when (state.dialog) { + is WalletConnectDialog.ApproveWcSession -> + dialog = ApproveWcSessionDialog.create(state.dialog.session, context) + + is WalletConnectDialog.ClipboardOrScanQr -> + dialog = ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context) + is WalletConnectDialog.RequestTransaction -> + dialog = TransactionDialog.create(state.dialog.dialogData, context) + is WalletConnectDialog.PersonalSign -> + dialog = PersonalSignDialog.create(state.dialog.data, context) + } + dialog?.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt new file mode 100644 index 0000000000..f30a481c98 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.common + +import android.content.Intent +import android.nfc.NfcAdapter +import android.nfc.Tag +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.walletconnect.WalletConnectManager +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.home.redux.HomeAction +import com.tangem.tap.store + +class IntentHandler { + + fun handleIntent(intent: Intent?) { + handleBackgroundScan(intent) + handleWalletConnectLink(intent) + } + + private fun handleBackgroundScan(intent: Intent?) { + if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action || + NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action + ) + ) { + val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) + if (tag != null) { + intent.action = null + store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatch(HomeAction.ReadCard) + } + } + } + + private fun handleWalletConnectLink(intent: Intent?) { + if (intent?.scheme == WalletConnectManager.WC_SCHEME) { + store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString())) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt new file mode 100644 index 0000000000..c10b8e6f37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common + +import android.view.View + +interface SnackbarHandler { + fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null) + fun dismissSnackbar() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 6e348ea8cf..d56ae0af85 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -9,6 +9,8 @@ import com.tangem.tap.features.details.ui.DetailsFragment import com.tangem.tap.features.details.ui.DetailsSecurityFragment import com.tangem.tap.features.details.ui.twins.CreateTwinWalletFragment import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment +import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment +import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment import com.tangem.tap.features.home.HomeFragment import com.tangem.tap.features.send.ui.SendFragment @@ -50,5 +52,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.TwinsOnboarding -> TwinsOnboardingFragment() AppScreen.AddTokens -> AddTokensFragment() AppScreen.WalletDetails -> WalletDetailsFragment() + AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment() + AppScreen.QrScan -> QrScanFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index f97a4cd062..a792b8b25e 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -22,13 +22,16 @@ fun BigDecimal.toFormattedString( return df.format(this) } -fun BigDecimal.toFormattedCurrencyString(decimals: Int, currency: String): String { - return "${this.toFormattedString(decimals)} $currency" +fun BigDecimal.toFormattedCurrencyString( + decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN +): String { + val formattedAmount = this.toFormattedString(decimals = decimals, roundingMode = roundingMode) + return "$formattedAmount $currency" } fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrencyName): String { var fiatValue = rateValue.multiply(this) - fiatValue = fiatValue.setScale(2, RoundingMode.DOWN) + fiatValue = fiatValue.setScale(2, RoundingMode.HALF_UP) return "≈ ${fiatCurrencyName}  $fiatValue" } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt new file mode 100644 index 0000000000..99e6d8fe5d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -0,0 +1,15 @@ +package com.tangem.tap.common.extensions + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.Store + +fun Store.dispatchOnMain(action: Action) { + scope.launch(Dispatchers.Main) { + store.dispatch(action) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 03dcb4a441..57ed1eaf2c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.common.redux.navigation.NavigationReducer import com.tangem.tap.features.details.redux.DetailsReducer +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer @@ -23,6 +24,7 @@ fun appReducer(action: Action, state: AppState?): AppState { detailsState = DetailsReducer.reduce(action, state), disclaimerState = DisclaimerReducer.reduce(action, state), tokensState = TokensReducer.reduce(action, state), + walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState) ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 4548ca046f..02deb7d980 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -6,6 +6,8 @@ import com.tangem.tap.common.redux.navigation.NavigationState import com.tangem.tap.common.redux.navigation.navigationMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.features.home.redux.HomeMiddleware @@ -20,14 +22,15 @@ import org.rekotlin.Middleware import org.rekotlin.StateType data class AppState( - val navigationState: NavigationState = NavigationState(), - val globalState: GlobalState = GlobalState(), - val homeState: HomeState = HomeState(), - val walletState: WalletState = WalletState(), - val sendState: SendState = SendState(), - val detailsState: DetailsState = DetailsState(), - val disclaimerState: DisclaimerState = DisclaimerState(), - val tokensState: TokensState = TokensState(), + val navigationState: NavigationState = NavigationState(), + val globalState: GlobalState = GlobalState(), + val homeState: HomeState = HomeState(), + val walletState: WalletState = WalletState(), + val sendState: SendState = SendState(), + val detailsState: DetailsState = DetailsState(), + val disclaimerState: DisclaimerState = DisclaimerState(), + val tokensState: TokensState = TokensState(), + val walletConnectState: WalletConnectState = WalletConnectState(), ) : StateType { companion object { @@ -40,6 +43,7 @@ data class AppState( DetailsMiddleware().detailsMiddleware, DisclaimerMiddleware().disclaimerMiddleware, TokensMiddleware().tokensMiddleware, + WalletConnectMiddleware().walletConnectMiddleware ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 38f45e4ca7..419fa5737d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux.global +import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage @@ -38,4 +39,8 @@ sealed class GlobalAction : Action { data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() data class SendFeedback(val emailData: EmailData) : GlobalAction() + data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() + + data class ShowDialog(val stateDialog: StateDialog) : GlobalAction() + object HideDialog : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index b06dc1b5df..2f79a5bbcc 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -56,6 +56,10 @@ val globalMiddleware: Middleware = { dispatch, appState -> is GlobalAction.UpdateWalletSignedHashes -> { store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures)) } + is GlobalAction.UpdateFeedbackInfo -> { + store.state.globalState.feedbackManager?.infoHolder + ?.setWalletsInfo(action.walletManagers) + } } nextDispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index c7ed09486a..b7208034f7 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -67,6 +67,12 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.SetFeedbackManager -> { globalState.copy(feedbackManager = action.feedbackManager) } + is GlobalAction.ShowDialog -> { + globalState.copy(dialog = action.stateDialog) + } + is GlobalAction.HideDialog -> { + globalState.copy(dialog = null) + } else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index a0ac13bc06..c7e173310b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -22,6 +22,7 @@ data class GlobalState( val feedbackManager: FeedbackManager? = null, val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY, val scanCardFailsCounter: Int = 0, + val dialog: StateDialog? = null ) : StateType typealias CryptoCurrencyName = String diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 26344b0175..ed59b9b2d5 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -11,5 +11,6 @@ data class NavigationState( enum class AppScreen { Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, - CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens + CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens, WalletConnectSessions, + QrScan } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index d4ce5e4b9f..af401f69bc 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -33,10 +33,14 @@ class TangemSdkManager(val activity: ComponentActivity) { activity, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java))) ) - suspend fun scanNote(analyticsHandler: AnalyticsHandler): CompletionResult { + suspend fun scanNote( + analyticsHandler: AnalyticsHandler, messageRes: Int? = null + ): CompletionResult { analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null) return runTaskAsyncReturnOnMain(ScanNoteTask(), - initialMessage = Message(activity.getString(R.string.initial_message_scan_header))) + initialMessage = Message( + activity.getString(messageRes ?: R.string.initial_message_scan_header) + )) } suspend fun createWallet(cardId: String?): CompletionResult { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 360c6f1b07..357db63124 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -184,23 +184,32 @@ class TapWalletManager { private fun loadMultiWalletData( card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager? ) { - val presetTokens = primaryWalletManager?.presetTokens?.toList() ?: emptyList() + val presetTokens = primaryWalletManager?.presetTokens ?: emptySet() val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId) if (savedCurrencies == null) { if (primaryBlockchain != null && primaryWalletManager != null) { store.dispatch(WalletAction.MultiWallet.SaveCurrencies( CardCurrencies( - blockchains = listOf(primaryBlockchain), tokens = presetTokens + blockchains = setOf(primaryBlockchain), tokens = presetTokens ))) store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager)) store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain))) - store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens)) + store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens.toList())) + } else { + val blockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) + store.dispatch(WalletAction.MultiWallet.SaveCurrencies( + CardCurrencies(blockchains = blockchains, tokens = emptySet()) + )) + val walletManagers = + walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList()) + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) + store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList())) } store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse(card, walletManagerFactory)) store.dispatch(WalletAction.MultiWallet.FindTokensInUse) } else { - val blockchains = savedCurrencies.blockchains + val blockchains = savedCurrencies.blockchains.toList() val walletManagers = if ( presetTokens.isNotEmpty() && primaryWalletManager != null && primaryBlockchain != null @@ -214,8 +223,7 @@ class TapWalletManager { store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains)) - store.dispatch(WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens)) - store.dispatch(WalletAction.MultiWallet.FindTokensInUse) + store.dispatch(WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens.toList())) } } diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index afcac855d5..ace735abd2 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain import com.tangem.commands.common.card.Card import com.tangem.commands.common.card.EllipticCurve import com.tangem.commands.common.card.masks.Product +import com.tangem.tap.domain.extensions.getSingleWallet import java.util.* object TapWorkarounds { @@ -43,6 +44,6 @@ val Card.isMultiwalletAllowed: Boolean get() { return cardData?.productMask?.contains(Product.TwinCard) != true && !TapWorkarounds.isStart2Coin - && (this.firmwareVersion.major >= 4 || - this.getWallets().getOrNull(0)?.curve == EllipticCurve.Secp256k1) + && (firmwareVersion.major >= 4 || + getSingleWallet()?.curve == EllipticCurve.Secp256k1) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt b/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt index 5f5ae80096..dfa301ab78 100644 --- a/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt @@ -1,8 +1,8 @@ package com.tangem.tap.domain import android.net.Uri +import android.util.Base64 import com.tangem.tap.common.redux.global.CryptoCurrencyName -import org.spongycastle.util.encoders.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -37,7 +37,8 @@ class TopUpHelper { val sha256Hmac = Mac.getInstance("HmacSHA256") val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256") sha256Hmac.init(secretKey) - return Base64.toBase64String(sha256Hmac.doFinal(data.toByteArray())) + val sha256encoded = sha256Hmac.doFinal(data.toByteArray()) + return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt index 6baaf8f668..17cac3c3e6 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt @@ -3,16 +3,13 @@ package com.tangem.tap.domain.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.commands.common.card.EllipticCurve -import org.stellar.sdk.requests.ErrorResponse import java.math.BigDecimal fun Blockchain.isNoAccountError(exception: Throwable): Boolean { return when (this) { - Blockchain.Stellar -> { - (exception is ErrorResponse) && (exception.code == 404) - } - Blockchain.XRP -> exception.message?.contains("Account not found") == true + Blockchain.Stellar, Blockchain.XRP -> + exception.message?.contains("Account not found") == true else -> false } } diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 99e2cba747..9e805a29de 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -13,6 +13,7 @@ fun Card.getToken(): Token? { val symbol = cardData?.tokenSymbol ?: return null val contractAddress = cardData?.tokenContractAddress ?: return null val decimals = cardData?.tokenDecimal ?: return null + if (symbol.isBlank() || contractAddress.isBlank()) return null return Token(symbol, contractAddress, decimals) } @@ -28,7 +29,7 @@ fun Card.getSingleWallet(): CardWallet? { fun Card.getStatus(): CardStatus { if (firmwareVersion < FirmwareConstraints.AvailabilityVersions.walletData) return status!! - return if (getWallets().any { it.status == WalletStatus.Loaded }) { + return if (wallets.any { it.status == WalletStatus.Loaded }) { CardStatus.Loaded } else { CardStatus.Empty @@ -36,11 +37,11 @@ fun Card.getStatus(): CardStatus { } fun Card.hasSignedHashes(): Boolean { - return getWallets().any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 } + return wallets.any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 } } fun Card.signedHashesCount(): Int { - return getWallets().map { it.signedHashes ?: 0 }.sum() + return wallets.map { it.signedHashes ?: 0 }.sum() } val Card.remainingSignatures: Int? diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index 8b985f728f..68a0ee5b84 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -15,7 +15,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( blockchain: Blockchain, ): WalletManager? { val supportedCurves = blockchain.getSupportedCurves() ?: return null - val wallets = card.getWallets().filter { wallet -> supportedCurves.contains(wallet.curve) } + val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) } val wallet = selectWallet(wallets) val publicKey = wallet?.publicKey ?: return null val curveToUse = wallet.curve ?: return null @@ -42,7 +42,7 @@ fun WalletManagerFactory.makePrimaryWalletManager( val card = data.card val blockchain = card.getBlockchain() val supportedCurves = blockchain?.getSupportedCurves() ?: return null - val wallets = card.getWallets().filter { wallet -> supportedCurves.contains(wallet.curve) } + val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) } val wallet = selectWallet(wallets) val publicKey = wallet?.publicKey ?: return null val curveToUse = wallet.curve ?: return null diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index c7a8a6cb8e..0415316f32 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -43,8 +43,8 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { val card = this.card?.copy( isPin1Default = result.data.isPin1Default, - isPin2Default = result.data.isPin2Default - )?.also { it.setWallets(card.getWallets()) } ?: result.data + isPin2Default = result.data.isPin2Default, + ) ?: result.data val error = getErrorIfExcludedCard(card) if (error != null) { @@ -88,7 +88,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable> = moshi.adapter( - Types.newParameterizedType(List::class.java, TokenDao::class.java) + private val tokensAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(Set::class.java, TokenDao::class.java) ) - private val blockchainsAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, Blockchain::class.java) + private val blockchainsAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(Set::class.java, Blockchain::class.java) ) fun loadCardCurrencies(cardId: String): CardCurrencies? { @@ -43,45 +43,45 @@ class CurrenciesRepository(val context: Application) { } fun removeToken(cardId: String, token: Token) { - val tokens = loadSavedTokens(cardId).filterNot { it == token } + val tokens = loadSavedTokens(cardId).filterNot { it == token }.toSet() saveTokens(cardId, tokens) } fun removeBlockchain(cardId: String, blockchain: Blockchain) { - val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain } + val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }.toSet() saveBlockchains(cardId, blockchains) } - private fun loadSavedTokens(cardId: String): List { + private fun loadSavedTokens(cardId: String): Set { return try { val json = context.readFileText(getFileNameForTokens(cardId)) - tokensAdapter.fromJson(json)!!.map { it.toToken() } + tokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet() } catch (exception: Exception) { - emptyList() + emptySet() } } - private fun saveTokens(cardId: String, tokens: List) { - val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }) + private fun saveTokens(cardId: String, tokens: Set) { + val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }.toSet()) context.rewriteFile(json, getFileNameForTokens(cardId)) } - private fun loadSavedBlockchains(cardId: String): List { + private fun loadSavedBlockchains(cardId: String): Set { return try { val json = context.readFileText(getFileNameForBlockchains(cardId)) - blockchainsAdapter.fromJson(json) ?: emptyList() + blockchainsAdapter.fromJson(json) ?: emptySet() } catch (exception: Exception) { - emptyList() + emptySet() } } - private fun saveBlockchains(cardId: String, blockchains: List) { + private fun saveBlockchains(cardId: String, blockchains: Set) { val json = blockchainsAdapter.toJson(blockchains) context.rewriteFile(json, getFileNameForBlockchains(cardId)) } private fun Context.readFileText(fileName: String): String = - this.openFileInput(fileName).bufferedReader().readText() + this.openFileInput(fileName).bufferedReader().readText() private fun Context.rewriteFile(content: String, fileName: String) { this.openFileOutput(fileName, Context.MODE_PRIVATE).use { @@ -108,12 +108,13 @@ class CurrenciesRepository(val context: Application) { private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains" fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId" - fun getFileNameForBlockchains(cardId: String): String = "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId" + fun getFileNameForBlockchains(cardId: String): String = + "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId" private val secp256k1Blochcains = listOf( - Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin, - Blockchain.XRP, Blockchain.Tezos, - Blockchain.Ethereum, Blockchain.RSK) + Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin, + Blockchain.XRP, Blockchain.Tezos, + Blockchain.Ethereum, Blockchain.RSK) private val ed25519Blockchains = listOf(Blockchain.CardanoShelley, Blockchain.Stellar) } } @@ -121,18 +122,24 @@ class CurrenciesRepository(val context: Application) { @JsonClass(generateAdapter = true) data class TokenDao( - val name: String, - val symbol: String, - val contractAddress: String, - val decimalCount: Int + val name: String, + val symbol: String, + val contractAddress: String, + val decimalCount: Int, ) { fun toToken(): Token { - return Token(name = name, symbol = symbol, contractAddress = contractAddress, decimals = decimalCount) + return Token(name = name, + symbol = symbol, + contractAddress = contractAddress, + decimals = decimalCount) } companion object { fun fromToken(token: Token): TokenDao { - return TokenDao(name = token.name, symbol = token.symbol, contractAddress = token.contractAddress, decimalCount = token.decimals) + return TokenDao(name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimalCount = token.decimals) } } @@ -140,21 +147,25 @@ data class TokenDao( @JsonClass(generateAdapter = true) data class CardCurrenciesDao( - val tokens: List, - val blockchains: List + val tokens: Set, + val blockchains: Set, ) { fun toCardCurrencies(): CardCurrencies { - return CardCurrencies(tokens = tokens.map { it.toToken() }, blockchains = blockchains) + return CardCurrencies(tokens = tokens.map { it.toToken() }.toSet(), + blockchains = blockchains) } companion object { fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao { - return CardCurrenciesDao(tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }, blockchains = cardCurrencies.blockchains) + return CardCurrenciesDao( + tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.toSet(), + blockchains = cardCurrencies.blockchains + ) } } } data class CardCurrencies( - val tokens: List, - val blockchains: List + val tokens: Set, + val blockchains: Set, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt index 9140cf5321..c534afbec6 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt @@ -86,13 +86,11 @@ fun Card.getTwinCardIdForUser(): String { } fun Card.changeStatusToLoaded(): Card { - val wallets = getWallets().map { it.copy(status = WalletStatus.Loaded) } - return copy(status = CardStatus.Loaded) - .also { it.setWallets(wallets) } + val wallets = wallets.map { it.copy(status = WalletStatus.Loaded) } + return copy(status = CardStatus.Loaded, wallets = wallets) } fun Card.changeStatusToEmpty(): Card { - val wallets = getWallets().map { it.copy(status = WalletStatus.Empty) } - return copy(status = CardStatus.Empty) - .also { it.setWallets(wallets) } + val wallets = wallets.map { it.copy(status = WalletStatus.Empty) } + return copy(status = CardStatus.Empty, wallets = wallets) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt new file mode 100644 index 0000000000..442b46ce83 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -0,0 +1,324 @@ +package com.tangem.tap.domain.walletconnect + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.guard +import com.tangem.common.extensions.hexToBytes +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.redux.walletconnect.* +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.walletConnectRepository +import com.trustwallet.walletconnect.WCClient +import com.trustwallet.walletconnect.models.WCPeerMeta +import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage +import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction +import com.trustwallet.walletconnect.models.session.WCSession +import com.trustwallet.walletconnect.models.session.WCSessionUpdate +import kotlinx.coroutines.launch +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import timber.log.Timber +import java.util.* +import java.util.concurrent.TimeUnit + +class WalletConnectManager { + + private val okHttpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor(interceptor) + .addInterceptor(RetryInterceptor()) + .build() + } + private val interceptor by lazy { + HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + } + + private var sessions: MutableMap = mutableMapOf() + + fun connect(wcUri: String, wallet: WalletForSession) { + val session = WCSession.from(wcUri) ?: return + val client = WCClient(httpClient = okHttpClient) + setListeners(client) + val peerId = UUID.randomUUID().toString() + client.connect(session, tangemPeerMeta, peerId) + sessions[session] = WalletConnectActiveData( + peerId = peerId, + remotePeerId = null, + session = session, + client = client, + wallet = wallet + ) + } + + fun restoreSessions() { + val sessions = walletConnectRepository.loadSavedSessions() + this.sessions = sessions + .map { session -> + WalletConnectActiveData( + peerId = session.peerId, + remotePeerId = session.remotePeerId, + client = WCClient(httpClient = okHttpClient), + session = session.session, + peerMeta = session.peerMeta, + wallet = session.wallet + ) + .also { + setListeners(it.client) + it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId) + } + } + .map { it.session to it }.toMap().toMutableMap() + store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions)) + } + + fun approve(session: WCSession) { + val activeData = sessions[session] ?: return + removeSimilarSessions(activeData) + + val approved = activeData.client.approveSession( + accounts = listOf(Blockchain.Ethereum.makeAddresses( + activeData.wallet.walletPublicKey.hexToBytes()).first().value + ), + chainId = activeData.wallet.chainId + ) + if (approved) { + val walletConnectSession = WalletConnectSession( + peerId = activeData.peerId, + remotePeerId = activeData.remotePeerId, + wallet = activeData.wallet, + session = session, + peerMeta = activeData.peerMeta!! + ) + walletConnectRepository.saveSession(walletConnectSession) + store.dispatchOnMain(WalletConnectAction.ApproveSession.Success( + walletConnectSession)) + + } + } + + fun removeSimilarSessions(activeData: WalletConnectActiveData) { + val sessionsToRemove = sessions.filter { + it.value.wallet.walletPublicKey == activeData.wallet.walletPublicKey + && it.value.peerMeta?.url == activeData.peerMeta?.url + && it.value.session != activeData.session + } + Timber.d("RemoveSimilarSessions: ${sessionsToRemove.values.map { it.client.session }}") + sessionsToRemove.forEach { disconnect(it.value.session) } + } + + fun rejectRequest(session: WCSession, id: Long) { + val activeData = sessions[session] ?: return + activeData.client.rejectRequest(id) + } + + fun acceptRequest(session: WCSession, id: Long, data: String) { + val activeData = sessions[session] ?: return + activeData.client.approveRequest(id, data) + } + + fun disconnect(session: WCSession) { + val activeData = sessions[session] ?: return + val disconnected = activeData.client.killSession() + if (disconnected) { + onSessionClosed(session) + } + } + + private fun onSessionClosed(session: WCSession) { + sessions.remove(session) + walletConnectRepository.removeSession(session) + store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) + } + + fun handleTransactionRequest( + transaction: WCEthereumTransaction, + session: WalletConnectSession, + id: Long, + type: WcTransactionType, + ) { + val activeData = sessions[session.session] ?: return + scope.launch { + val data = WalletConnectSdkHelper().prepareTransactionData( + transaction = transaction, + session = session, + id = id, + type = type + ).guard { + sessions[session.session] = activeData.copy(transactionData = null) + store.dispatchOnMain(WalletConnectAction.RejectRequest( + session.session, + id + )) + return@launch + } + sessions[session.session] = activeData.copy(transactionData = data) + + store.dispatchOnMain(WalletConnectAction.SetDataToSend(data)) + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.RequestTransaction( + data.dialogData))) + } + } + + fun completeTransaction(session: WCSession) { + val activeData = sessions[session] + val data = activeData?.transactionData ?: return + scope.launch { + val hash = WalletConnectSdkHelper().completeTransaction(data).guard { + sessions[data.session.session] = activeData.copy(transactionData = null) + store.dispatchOnMain(WalletConnectAction.RejectRequest(data.session.session, + data.id + )) + return@launch + } + acceptRequest(data.session.session, data.id, hash) + sessions[data.session.session] = activeData.copy(transactionData = null) + } + + } + + fun handlePersonalSignRequest( + message: WCEthereumSignMessage, + session: WalletConnectSession, + id: Long, + ) { + val activeData = sessions[session.session] ?: return + scope.launch { + val data = WalletConnectSdkHelper().prepareDataForPersonalSign( + message = message, session = session, id = id + ) + sessions[session.session] = activeData.copy(personalSignData = data) + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.PersonalSign( + data.dialogData + ))) + } + } + + fun sendSignedMessage(session: WCSession) { + val activeData = sessions[session] + val data = activeData?.personalSignData ?: return + scope.launch { + val hash = WalletConnectSdkHelper().signPersonalMessage(data.hash, activeData.wallet) + .guard { + sessions[data.session.session] = activeData.copy(transactionData = null) + store.dispatchOnMain(WalletConnectAction.RejectRequest(data.session.session, + data.id + )) + return@launch + } + sessions[session] = activeData.copy(personalSignData = data) + acceptRequest(data.session.session, data.id, hash) + sessions[data.session.session] = activeData.copy(transactionData = null) + } + } + + + fun setListeners(client: WCClient) { + client.onSessionRequest = { id: Long, peer: WCPeerMeta -> + Timber.d("OnSessionRequest: $peer") + val session = client.session + val data = sessions[session]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId) + if (data != null && session != null) { + sessions[session] = data + val sessionData = data.toWalletConnectSession() + sessionData?.let { + store.dispatchOnMain(WalletConnectAction.AcceptOpeningSession( + sessionData)) + } + } + } + client.onSessionUpdate = { id: Long, update: WCSessionUpdate -> + Timber.d("onSessionUpdate: $update") + val session = client.session + if (session != null && !update.approved) onSessionClosed(session) + } + client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction -> + Timber.d("onEthSendTransaction: $transaction") + sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest( + transaction = transaction, + session = sessionData, + id = id, + type = WcTransactionType.EthSendTransaction + )) + } + + } + client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction -> + Timber.d("onEthSignTransaction: $transaction") + sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest( + transaction = transaction, + session = sessionData, + id = id, + type = WcTransactionType.EthSignTransaction + )) + } + } + client.onEthSign = { id: Long, message: WCEthereumSignMessage -> + Timber.d("onEthSign: $message") + sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + store.dispatchOnMain(WalletConnectAction.HandlePersonalSignRequest( + message, + sessionData, + id)) + } + } + client.onDisconnect = { code: Int, reason: String -> + val session = client.session + if (session != null) { + onSessionClosed(session) + } + } + } + + companion object { + private val tangemPeerMeta = + WCPeerMeta(name = "Tangem Wallet", url = "https://tangem.com") + + fun isCorrectWcUri(string: String): Boolean = WCSession.from(string) != null + + const val WC_SCHEME = "wc" + } +} + +data class WalletConnectActiveData( + val peerId: String, + val remotePeerId: String?, + val client: WCClient, + val session: WCSession, + val peerMeta: WCPeerMeta? = null, + val wallet: WalletForSession, + val transactionData: WcTransactionData? = null, + val personalSignData: WcPersonalSignData? = null, +) { + fun toWalletConnectSession(): WalletConnectSession? { + if (peerMeta == null) return null + return WalletConnectSession( + peerId = peerId, + remotePeerId = remotePeerId, + wallet = wallet, + session = session, + peerMeta = peerMeta + ) + } +} + +class RetryInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + val response = chain.proceed(request) + when (response.code) { + 502 -> { + return chain.proceed(request) + } + } + return response + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt new file mode 100644 index 0000000000..a4654d6429 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt @@ -0,0 +1,94 @@ +package com.tangem.tap.domain.walletconnect + +import android.app.Application +import android.content.Context +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Types +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import com.tangem.tap.features.details.redux.walletconnect.WalletForSession +import com.tangem.tap.network.createMoshi +import com.trustwallet.walletconnect.models.WCPeerMeta +import com.trustwallet.walletconnect.models.session.WCSession + +class WalletConnectRepository(val context: Application) { + private val moshi = createMoshi() + private val walletConnectAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, SessionDao::class.java) + ) + + fun saveSession(session: WalletConnectSession) { + val sessions = loadSavedSessions() + session + saveSessions(sessions) + } + + fun removeSession(session: WalletConnectSession) { + val sessions = loadSavedSessions().filterNot { it == session } + saveSessions(sessions) + } + + fun removeSession(session: WCSession) { + val sessions = loadSavedSessions().filterNot { it.session == session } + saveSessions(sessions) + } + + fun loadSavedSessions(): List { + return try { + val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS) + walletConnectAdapter.fromJson(json)!!.map { it.toSession() } + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveSessions(sessions: List) { + val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) }) + context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS) + } + + private fun Context.readFileText(fileName: String): String = + this.openFileInput(fileName).bufferedReader().readText() + + private fun Context.rewriteFile(content: String, fileName: String) { + this.openFileOutput(fileName, Context.MODE_PRIVATE).use { + it.write(content.toByteArray(), 0, content.length) + } + } + + companion object { + private const val FILE_NAME_PREFIX_SESSIONS = "wc_sessions" + } +} + + +@JsonClass(generateAdapter = true) +data class SessionDao( + val peerId: String, + val remotePeerId: String?, + val wallet: WalletForSession, + val session: WCSession, + val peerMeta: WCPeerMeta, +) { + fun toSession(): WalletConnectSession { + return WalletConnectSession( + peerId = peerId, + remotePeerId = remotePeerId, + wallet = wallet, + session = session, + peerMeta = peerMeta + ) + } + + companion object { + fun fromSession(session: WalletConnectSession): SessionDao { + return SessionDao( + peerId = session.peerId, + remotePeerId = session.remotePeerId, + wallet = session.wallet, + session = session.session, + peerMeta = session.peerMeta + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt new file mode 100644 index 0000000000..a78b81d7ce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -0,0 +1,221 @@ +package com.tangem.tap.domain.walletconnect + +import com.tangem.Message +import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.extensions.* +import com.tangem.commands.SignCommand +import com.tangem.commands.wallet.WalletIndex +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.hexToBytes +import com.tangem.tap.common.extensions.toFormattedString +import com.tangem.tap.features.details.redux.walletconnect.* +import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData +import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData +import com.tangem.tap.store +import com.tangem.tap.tangemSdk +import com.tangem.tap.tangemSdkManager +import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage +import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction +import timber.log.Timber +import java.math.BigDecimal + +class WalletConnectSdkHelper { + + suspend fun prepareTransactionData( + transaction: WCEthereumTransaction, + session: WalletConnectSession, + id: Long, + type: WcTransactionType, + ): WcTransactionData? { + val factory = store.state.globalState.tapWalletManager.walletManagerFactory + val walletManager = factory.makeEthereumWalletManager( + session.wallet.cardId, + session.wallet.walletPublicKey.hexToBytes(), + emptyList(), + isTestNet = session.wallet.isTestNet + ) ?: return null + + + try { + walletManager.update() + } catch (exception: Exception) { + Timber.e(exception) + return null + } + + val blockchain = walletManager.wallet.blockchain + + val balance = + walletManager.wallet.amounts[AmountType.Coin]?.value ?: return null + + val gas = transaction.gas?.hexToBigDecimal() + ?: transaction.gasLimit?.hexToBigDecimal() ?: return null + + val decimals = blockchain.decimals() + + val value = transaction.value?.hexToBigDecimal() + ?.movePointLeft(decimals) ?: return null + + val gasPrice = transaction.gasPrice?.hexToBigDecimal() + ?: when (val result = + (walletManager as? EthereumGasLoader)?.getGasPrice()) { + is Result.Success -> result.data.toBigDecimal() + is Result.Failure -> { + Timber.e(result.error) + return null + } + null -> return null + } + + val fee = (gas * gasPrice).movePointLeft(decimals) + val total = value + fee + + val transactionData = TransactionData( + amount = Amount(value, blockchain), + fee = Amount(fee, blockchain), + sourceAddress = transaction.from, + destinationAddress = transaction.to!!, + extras = EthereumTransactionExtras( + data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(), + gasLimit = gas.toBigInteger(), + nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger() + ) + ) + val dialogData = TransactionRequestDialogData( + cardId = session.wallet.cardId, + dAppName = session.peerMeta.name, + dAppUrl = session.peerMeta.url, + amount = value.toFormattedString(decimals), + gasAmount = fee.toFormattedString(decimals), + totalAmount = total.toFormattedString(decimals), + balance = balance.toFormattedString(decimals), + isEnoughFundsToSend = (balance - total) >= BigDecimal.ZERO, + session = session.session, + id = id, + type = type + ) + return WcTransactionData( + type = type, + transaction = transactionData, + session = session, + id = id, + walletManager = walletManager, + dialogData = dialogData + ) + } + + suspend fun completeTransaction(data: WcTransactionData): String? { + return when (data.type) { + WcTransactionType.EthSendTransaction -> sendTransaction(data) + WcTransactionType.EthSignTransaction -> signTransaction(data) + } + } + + private suspend fun sendTransaction(data: WcTransactionData): String? { + val result = (data.walletManager as TransactionSender).send( + transactionData = data.transaction, + signer = Signer(tangemSdk) + ) + return when (result) { + SimpleResult.Success -> { + HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash + } + is SimpleResult.Failure -> { + Timber.e(result.error) + null + } + } + } + + private suspend fun signTransaction(data: WcTransactionData): String? { + val dataToSign = EthereumUtils.buildTransactionToSign( + transactionData = data.transaction, + nonce = null, + blockchain = data.walletManager.wallet.blockchain, + gasLimit = null + ) ?: return null + + val command = SignCommand( + hashes = arrayOf(dataToSign.hash), + walletIndex = WalletIndex.PublicKey(data.walletManager.wallet.publicKey) + ) + val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message()) + return when (result) { + is CompletionResult.Success -> { + HEX_PREFIX + result.data + } + is CompletionResult.Failure -> { + Timber.e(result.error.customMessage) + null + } + } + } + + fun prepareDataForPersonalSign( + message: WCEthereumSignMessage, + session: WalletConnectSession, + id: Long, + ): WcPersonalSignData { + val messageData = message.data.removePrefix(HEX_PREFIX).hexToBytes() + val messageString = message.data.hexToAscii() ?: message.data + + val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray() + val hashToSign = (prefixData + messageData).toKeccak() + + + val dialogData = PersonalSignDialogData( + cardId = session.wallet.cardId, + dAppName = session.peerMeta.name, + message = messageString, + session = session.session, + id = id + ) + return WcPersonalSignData( + hash = hashToSign, + session = session, + id = id, + dialogData = dialogData + ) + } + + private fun String.hexToAscii(): String? { + return removePrefix(HEX_PREFIX).hexToBytes() + .map { + val char = it.toInt().toChar() + if (char.isAscii()) char else return null + } + .joinToString("") + } + + suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? { + val publicKey = wallet.walletPublicKey.hexToBytes() + val command = SignCommand( + arrayOf(hashToSign), + WalletIndex.PublicKey(publicKey)) + return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) { + is CompletionResult.Success -> { + val hash = result.data.signatures.first() + + return EthereumUtils.prepareSignedMessageData( + hash, hashToSign, publicKey + ) + } + is CompletionResult.Failure -> { + Timber.e(result.error.customMessage) + null + } + } + } + + companion object { + private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" + private const val HEX_PREFIX = "0x" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index d68c0ba268..3a7b39e788 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -12,6 +12,7 @@ import com.tangem.tap.domain.twins.TwinsHelper import com.tangem.tap.domain.twins.isTwinCard import com.tangem.tap.features.details.redux.twins.CreateTwinWalletReducer import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState +import com.tangem.tap.features.wallet.models.toPendingTransactions import org.rekotlin.Action import java.util.* @@ -89,7 +90,8 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS || state.card?.settingsMask?.contains(Settings.IsReusable) == false || state.card?.isWalletDataSupported == true val notEmpty = state.wallets.any { - !it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty() + !it.recentTransactions.toPendingTransactions(it.address).isNullOrEmpty() + || it.amounts.toSendableAmounts().isNotEmpty() } val eraseWalletState = when { notAllowedByCard -> EraseWalletState.NotAllowedByCard diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt new file mode 100644 index 0000000000..6e950a1cab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.features.details.redux.walletconnect + +import android.app.Activity +import com.tangem.tap.common.redux.NotificationAction +import com.tangem.wallet.R +import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage +import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction +import com.trustwallet.walletconnect.models.session.WCSession +import org.rekotlin.Action + +sealed class WalletConnectAction : Action { + + data class HandleDeepLink(val wcUri: String?) : WalletConnectAction() + + object RestoreSessions : WalletConnectAction() + + data class StartWalletConnect( + val activity: Activity, + ) : WalletConnectAction() + + data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() + + data class ScanCard(val wcUri: String) : WalletConnectAction() + + object UnsupportedCard : WalletConnectAction() + + data class OpenSession( + val wcUri: String, + val wallet: WalletForSession, + ) : WalletConnectAction() + + data class AcceptOpeningSession(val session: WalletConnectSession) : WalletConnectAction() + + data class ApproveSession( + val session: WCSession, + ) : WalletConnectAction() { + data class Success(val session: WalletConnectSession) : WalletConnectAction() + } + + object FailureEstablishingSession : WalletConnectAction() + + + data class SetSessionsRestored(val sessions: List) : + WalletConnectAction() + + data class DisconnectSession(val session: WCSession) : WalletConnectAction() + + data class RemoveSession(val session: WCSession) : WalletConnectAction() + + data class HandleTransactionRequest( + val transaction: WCEthereumTransaction, + val session: WalletConnectSession, + val id: Long, + val type: WcTransactionType, + ) : + WalletConnectAction() + + data class SetDataToSend(val transactionData: WcTransactionData) : WalletConnectAction() + + data class HandlePersonalSignRequest( + val message: WCEthereumSignMessage, + val session: WalletConnectSession, + val id: Long, + ) : WalletConnectAction() + + data class SendTransaction(val session: WCSession) : WalletConnectAction() + + data class SignMessage(val session: WCSession) : WalletConnectAction() + + data class RejectRequest(val session: WCSession, val id: Long) : WalletConnectAction() + + object NotEnoughFunds : WalletConnectAction(), NotificationAction { + override val messageResource = R.string.wallet_connect_create_tx_not_enough_funds + } + + object NotifyCameraPermissionIsRequired : WalletConnectAction(), NotificationAction { + override val messageResource = R.string.common_camera_denied_alert_message + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt new file mode 100644 index 0000000000..896e3c06d2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -0,0 +1,156 @@ +package com.tangem.tap.features.details.redux.walletconnect + +import com.tangem.blockchain.common.* +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.toHexString +import com.tangem.tap.* +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.getFromClipboard +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.extensions.makeWalletManagerForApp +import com.tangem.tap.domain.isMultiwalletAllowed +import com.tangem.tap.domain.walletconnect.WalletConnectManager +import com.tangem.wallet.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.rekotlin.Middleware + +class WalletConnectMiddleware { + private val walletConnectManager = WalletConnectManager() + + val walletConnectMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + when (action) { + + is WalletConnectAction.RestoreSessions -> { + walletConnectManager.restoreSessions() + } + + is WalletConnectAction.HandleDeepLink -> { + if (!action.wcUri.isNullOrBlank()) { + if (WalletConnectManager.isCorrectWcUri(action.wcUri)) { + store.dispatchOnMain(WalletConnectAction.ScanCard(action.wcUri)) + } + } + } + + is WalletConnectAction.StartWalletConnect -> { + val uri = action.activity.getFromClipboard()?.toString() + if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) { + store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) + } else { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan)) + } + } + + is WalletConnectAction.ShowClipboardOrScanQrDialog -> { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.ClipboardOrScanQr( + action.wcUri))) + } + + is WalletConnectAction.ScanCard -> { + scope.launch { + val result = tangemSdkManager.scanNote( + FirebaseAnalyticsHandler, + R.string.wallet_connect_scan_card_message + ) + withContext(Dispatchers.Main) { + when (result) { + is CompletionResult.Success -> { + val card = result.data.card + val factory = + store.state.globalState.tapWalletManager.walletManagerFactory + + val walletManager = if (card.isMultiwalletAllowed) { + if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains( + Blockchain.Ethereum) == true + ) { + factory.makeWalletManagerForApp(result.data.card, + Blockchain.Ethereum) + } else { + factory.makeWalletManagerForApp(result.data.card, + Blockchain.Ethereum)?.also { + currenciesRepository.saveAddedBlockchain(card.cardId, + Blockchain.Ethereum) + } + } + } else { + null + } + if (walletManager == null) { + store.dispatchOnMain(WalletConnectAction.UnsupportedCard) + return@withContext + }; + + val key = walletManager.wallet.publicKey + store.dispatchOnMain(WalletConnectAction.OpenSession( + wcUri = action.wcUri, + wallet = WalletForSession( + card.cardId, key.toHexString(), + isTestNet = false + ), + )) + } + is CompletionResult.Failure -> + store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession) + } + } + } + } + + is WalletConnectAction.OpenSession -> { + walletConnectManager.connect( + wcUri = action.wcUri, + wallet = action.wallet + ) + } + + is WalletConnectAction.AcceptOpeningSession -> { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.ApproveWcSession( + action.session))) + } + + is WalletConnectAction.ApproveSession -> { + walletConnectManager.approve(action.session) + } + + is WalletConnectAction.DisconnectSession -> { + walletConnectManager.disconnect(action.session) + } + + is WalletConnectAction.HandleTransactionRequest -> { + walletConnectManager.handleTransactionRequest( + transaction = action.transaction, + session = action.session, + id = action.id, + type = action.type + ) + } + is WalletConnectAction.HandlePersonalSignRequest -> { + walletConnectManager.handlePersonalSignRequest( + message = action.message, + session = action.session, + id = action.id + ) + } + is WalletConnectAction.RejectRequest -> { + walletConnectManager.rejectRequest(action.session, action.id) + } + is WalletConnectAction.SendTransaction -> { + walletConnectManager.completeTransaction(action.session) + } + is WalletConnectAction.SignMessage -> { + walletConnectManager.sendSignedMessage(action.session) + } + } + next(action) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt new file mode 100644 index 0000000000..84ef36046b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.details.redux.walletconnect + +import org.rekotlin.Action + +class WalletConnectReducer { + companion object { + fun reduce( + action: Action, state: WalletConnectState, + ): WalletConnectState { + if (action !is WalletConnectAction) return state + + return when (action) { + is WalletConnectAction.ApproveSession.Success -> { + state.copy( + loading = false, + sessions = state.sessions + action.session + ) + } + is WalletConnectAction.SetSessionsRestored -> + WalletConnectState(sessions = action.sessions) + + is WalletConnectAction.RemoveSession -> { + val sessions = + state.sessions.filterNot { it.session.toUri() == action.session.toUri() } + state.copy(sessions = sessions) + } + is WalletConnectAction.ScanCard -> state.copy(loading = true) + + is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false) + + else -> state + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt new file mode 100644 index 0000000000..75dad54452 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -0,0 +1,70 @@ +package com.tangem.tap.features.details.redux.walletconnect + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.WalletManager +import com.tangem.common.extensions.hexToBytes +import com.tangem.tap.common.redux.global.StateDialog +import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData +import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData +import com.trustwallet.walletconnect.models.WCPeerMeta +import com.trustwallet.walletconnect.models.session.WCSession + +data class WalletConnectState( + val loading: Boolean = false, + val sessions: List = listOf(), +) + +data class WalletConnectSession( + val peerId: String, + val remotePeerId: String?, + val wallet: WalletForSession, + val session: WCSession, + val peerMeta: WCPeerMeta, +) + +data class WalletForSession( + val cardId: String, + val walletPublicKey: String, + val isTestNet: Boolean = false, +) { + val chainId + get() = if (isTestNet) 4 else 1 + + fun getAddress(): String { + val blockchain = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum + return blockchain.makeAddresses(walletPublicKey.hexToBytes()).first().value + } +} + + +sealed class WalletConnectDialog : StateDialog { + data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog() + data class ApproveWcSession(val session: WalletConnectSession) : WalletConnectDialog() + data class RequestTransaction(val dialogData: TransactionRequestDialogData) : + WalletConnectDialog() + + data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog() +} + +data class WcTransactionData( + val type: WcTransactionType, + val transaction: TransactionData, + val session: WalletConnectSession, + val id: Long, + val walletManager: WalletManager, + val dialogData: TransactionRequestDialogData, +) + +enum class WcTransactionType { + EthSignTransaction, + EthSendTransaction, +} + +data class WcPersonalSignData( + val hash: ByteArray, + val session: WalletConnectSession, + val id: Long, + val dialogData: PersonalSignDialogData, + + ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 895f379b09..3b0e9c83a1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -9,6 +9,7 @@ import androidx.transition.TransitionInflater import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.twins.getTwinCardIdForUser import com.tangem.tap.domain.twins.isTwinCard @@ -120,6 +121,10 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber, + grantResults: IntArray, + ) { + if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return + + if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + store.dispatch(WalletConnectAction.NotifyCameraPermissionIsRequired) + store.dispatch(NavigationAction.PopBackTo()) + } + } + + private fun permissionIsGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val cameraPermission = + ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + cameraPermission == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + + private fun requestPermission() { + requestPermissions(arrayOf(Manifest.permission.CAMERA), CameraView.PERMISSION_REQUEST_CODE) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt new file mode 100644 index 0000000000..c1028c874d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt @@ -0,0 +1,55 @@ +package com.tangem.tap.features.details.ui.walletconnect + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_wallet_connect_session.view.* + +class WalletConnectSessionsAdapter + : ListAdapter( + DiffUtilCallback +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionsViewHolder { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_wallet_connect_session, parent, false) + return SessionsViewHolder(layout) + } + + override fun onBindViewHolder(holder: SessionsViewHolder, position: Int) { + holder.bind(currentList[position]) + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: WalletConnectSession, newItem: WalletConnectSession, + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: WalletConnectSession, newItem: WalletConnectSession, + ) = oldItem == newItem + } + + class SessionsViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + + fun bind(session: WalletConnectSession) { + view.tv_card_id.text = view.context.getString( + R.string.wallet_connect_card_number, session.wallet.cardId + ) + view.tv_d_app_name.text = session.peerMeta.name + + view.btn_disconnect.setOnClickListener { + store.dispatch(WalletConnectAction.DisconnectSession(session.session)) + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt new file mode 100644 index 0000000000..1a55a976c7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt @@ -0,0 +1,87 @@ +package com.tangem.tap.features.details.ui.walletconnect + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionInflater +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_wallet_connect_sessions.* +import org.rekotlin.StoreSubscriber + + +class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_sessions), + StoreSubscriber { + + private lateinit var walletConnectSessionsAdapter: WalletConnectSessionsAdapter + private var dialog: Dialog? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activity?.onBackPressedDispatcher?.addCallback( + this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.walletConnectState == newState.walletConnectState + }.select { it.walletConnectState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + toolbar.setNavigationOnClickListener { + store.dispatch(NavigationAction.PopBackTo()) + } + setOnClickListeners() + setupTransactionsRecyclerView() + } + + private fun setOnClickListeners() { + fab_open_session.setOnClickListener { + store.dispatch(WalletConnectAction.StartWalletConnect(activity = requireActivity())) + } + } + + private fun setupTransactionsRecyclerView() { + walletConnectSessionsAdapter = WalletConnectSessionsAdapter() + rv_wallet_connect_sessions.layoutManager = LinearLayoutManager(requireContext()) + rv_wallet_connect_sessions.adapter = walletConnectSessionsAdapter + walletConnectSessionsAdapter.submitList(store.state.walletConnectState.sessions) + } + + override fun newState(state: WalletConnectState) { + if (activity == null) return + + fab_open_session.show(!state.loading) + pb_wallet_connect.show(state.loading) + + walletConnectSessionsAdapter.submitList(state.sessions) + rv_wallet_connect_sessions.show(state.sessions.isNotEmpty()) + tv_no_sessions.show(state.sessions.isEmpty()) + tv_no_sessions_title.show(state.sessions.isEmpty()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt new file mode 100644 index 0000000000..0b6a55899b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import com.tangem.tap.store +import com.tangem.wallet.R + +class ApproveWcSessionDialog { + companion object { + fun create(session: WalletConnectSession, context: Context): AlertDialog { + val message = context.getString( + R.string.wallet_connect_request_session_start, + session.wallet.cardId, + session.peerMeta.name, + session.peerMeta.url + ) + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.wallet_connect)) + setMessage(message) + setPositiveButton(context.getText(R.string.common_start)) { _, _ -> + store.dispatch(WalletConnectAction.ApproveSession( + session.session + )) + } + setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt new file mode 100644 index 0000000000..5fb2d1507a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.store +import com.tangem.wallet.R + +class ClipboardOrScanQrDialog { + companion object { + fun create(wcUri: String, context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.wallet_connect)) + setMessage(context.getText(R.string.wallet_connect_clipboard_alert)) + setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ -> + store.dispatch(WalletConnectAction.ScanCard(wcUri)) + } + setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ -> + store.dispatch(NavigationAction.NavigateTo(AppScreen.QrScan)) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt new file mode 100644 index 0000000000..789384c758 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt @@ -0,0 +1,45 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.store +import com.tangem.wallet.R +import com.trustwallet.walletconnect.models.session.WCSession + +class PersonalSignDialog { + companion object { + fun create( + data: PersonalSignDialogData, + context: Context, + ): AlertDialog { + val message = + context.getString(R.string.wallet_connect_alert_sign_message, data.cardId) + + context.getString(R.string.wallet_connect_personal_sign_message, + data.dAppName, + data.message) + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.wallet_connect)) + setMessage(message) + setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> + store.dispatch(WalletConnectAction.SignMessage(data.session)) + } + setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> + store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id)) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} + +data class PersonalSignDialogData( + val cardId: String, + val dAppName: String, + val message: String, + val session: WCSession, + val id: Long, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt new file mode 100644 index 0000000000..192fc3d9e1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt @@ -0,0 +1,67 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType +import com.tangem.tap.store +import com.tangem.wallet.R +import com.trustwallet.walletconnect.models.session.WCSession + +class TransactionDialog { + companion object { + fun create( + data: TransactionRequestDialogData, + context: Context, + ): AlertDialog { + val message = context.getString( + R.string.wallet_connect_create_tx_message, + data.cardId, + data.dAppName, + data.dAppUrl, + data.amount, + data.gasAmount, + data.totalAmount, + data.balance + ) + + val positiveButtonTitle = when (data.type) { + WcTransactionType.EthSignTransaction -> context.getText(R.string.common_sign) + WcTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send) + } + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.wallet_connect)) + setMessage(message) + setPositiveButton(positiveButtonTitle) { _, _ -> + if (data.isEnoughFundsToSend) { + store.dispatch(WalletConnectAction.SendTransaction(data.session)) + } else { + store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id)) + store.dispatch(WalletConnectAction.NotEnoughFunds) + } + } + setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> + store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id)) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} + +data class TransactionRequestDialogData( + val cardId: String, + val dAppName: String, + val dAppUrl: String, + val amount: String, + val gasAmount: String, + val totalAmount: String, + val balance: String, + val isEnoughFundsToSend: Boolean, + val session: WCSession, + val id: Long, + val type: WcTransactionType, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index 080a4838b3..c3ff7b8de0 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -12,10 +12,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import com.tangem.Log import com.tangem.TangemSdkLogger -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* import com.tangem.commands.common.card.Card import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.domain.TapWorkarounds @@ -138,6 +135,7 @@ class AdditionalEmailInfo { var blockchain: Blockchain = Blockchain.Unknown, var address: String = "", var explorerLink: String = "", + var host: String = "", // var outputsCount: String = "" // var transactionHex: String = "" ) @@ -178,14 +176,28 @@ class AdditionalEmailInfo { signedHashesCount = card.signedHashesCount().toString() } - fun setWalletsInfo(wallets: List) { + fun setWalletsInfo(walletManagers: List) { walletsInfo.clear() - wallets.forEach { walletsInfo.add(EmailWalletInfo(it.blockchain, getAddress(it), getExploreUri(it))) } + walletManagers.forEach { manager -> + walletsInfo.add( + EmailWalletInfo( + blockchain = manager.wallet.blockchain, + address = getAddress(manager.wallet), + explorerLink = getExploreUri(manager.wallet), + host = manager.currentHost + ) + ) + } } - fun updateOnSendError(wallet: Wallet, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) { + fun updateOnSendError(wallet: Wallet, host: String, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) { val amountState = store.state.sendState.amountState - onSendErrorWalletInfo = EmailWalletInfo(wallet.blockchain, getAddress(wallet), getExploreUri(wallet)) + onSendErrorWalletInfo = EmailWalletInfo( + blockchain = wallet.blockchain, + address = getAddress(wallet), + explorerLink = getExploreUri(wallet), + host = host + ) this.destinationAddress = destinationAddress amount = amountToSend.value?.stripZeroPlainString() ?: "0" @@ -267,6 +279,7 @@ class SendTransactionFailedEmail(private val error: String) : EmailData { appendKeyValue("Error", error) appendKeyValue("Card ID", infoHolder.cardId) appendKeyValue("Blockchain", walletInfo.blockchain.fullName) + appendKeyValue("Host", walletInfo.host) appendKeyValue("Token", infoHolder.token) appendKeyValue("Source address", walletInfo.address) appendKeyValue("Destination address", infoHolder.destinationAddress) @@ -291,6 +304,7 @@ class FeedbackEmail : EmailData { infoHolder.walletsInfo.forEach { builder.appendKeyValue("Blockchain", it.blockchain.fullName) + builder.appendKeyValue("Host", it.host) builder.appendKeyValue("Wallet address", it.address) builder.appendKeyValue("Explorer link", it.explorerLink) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt index 19bb270b16..dcf3bf5a43 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt @@ -83,15 +83,13 @@ class AmountMiddleware { val sendState = appState?.sendState ?: return dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false)) - dispatch(AmountActionUi.CheckAmountToSend) - - if (SendState.isReadyToRequestFee()) { - dispatch(FeeAction.RequestFee) - if (!sendState.amountState.isCoinAmount()) return - + if (sendState.amountState.isCoinAmount()) { dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true)) dispatch(FeeActionUi.ChangeIncludeFee(true)) } + + dispatch(AmountActionUi.CheckAmountToSend) + if (SendState.isReadyToRequestFee()) dispatch(FeeAction.RequestFee) } private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 6ad9802785..3d2b5e9c37 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -4,9 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.Signer import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.commands.SignResponse import com.tangem.commands.common.card.Card import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler @@ -162,7 +160,13 @@ private fun sendTransaction( when { message == null -> { dispatch(SendAction.SendError(TapError.UnknownError)) - infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress) + infoHolder?.updateOnSendError( + wallet = walletManager.wallet, + host = walletManager.currentHost, + amountToSend = amountToSend, + feeAmount = feeAmount, + destinationAddress = destinationAddress + ) dispatch(SendAction.Dialog.SendTransactionFails("unknown error")) } message.contains("50002") -> { @@ -177,7 +181,13 @@ private fun sendTransaction( Timber.e(throwable) FirebaseCrashlytics.getInstance().recordException(throwable) dispatch(SendAction.SendError(TapError.CustomError(message))) - infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress) + infoHolder?.updateOnSendError( + wallet = walletManager.wallet, + host = walletManager.currentHost, + amountToSend = amountToSend, + feeAmount = feeAmount, + destinationAddress = destinationAddress + ) dispatch(SendAction.Dialog.SendTransactionFails(message)) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 5be3d75998..2b2b682294 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -251,7 +251,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { val mainCurrency = sp.getString("mainCurrency", TapCurrency.DEFAULT_FIAT_CURRENCY) val foundType = MainCurrencyType.values() .firstOrNull { it.name.toLowerCase() == mainCurrency!!.toLowerCase() } - ?: MainCurrencyType.FIAT + ?: MainCurrencyType.CRYPTO return foundType } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt index be4a64b61e..66a7dd928b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -21,8 +21,6 @@ private fun internalReduce(action: Action, state: AppState): TokensState { tokensState.copy(currencies = action.currencies) } is TokensAction.SetAddedCurrencies -> { - - tokensState.copy(addedCurrencies = action.wallets.toCardCurrencies()) } is TokensAction.LoadCardTokens.Success -> { @@ -35,7 +33,7 @@ private fun internalReduce(action: Action, state: AppState): TokensState { } private fun List.toCardCurrencies(): CardCurrencies { - val tokens = mapNotNull { (it.currency as? Currency.Token)?.token } - val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain } + val tokens = mapNotNull { (it.currency as? Currency.Token)?.token }.toSet() + val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain }.toSet() return CardCurrencies(tokens = tokens, blockchains = blockchains) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt index 1905ae9d96..7767d74b32 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -7,10 +7,8 @@ import androidx.annotation.StringRes import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.squareup.picasso.Callback import com.squareup.picasso.Picasso import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchain.common.Token import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.global.CryptoCurrencyName @@ -20,7 +18,6 @@ import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.item_currency_subtitle.view.* import kotlinx.android.synthetic.main.item_popular_token.view.* -import java.lang.Exception import java.util.* class CurrenciesAdapter : ListAdapter(DiffUtilCallback) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 1b2a1aa4b0..35b301fd50 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -44,14 +44,17 @@ data class WalletState( val blockchains: List get() = walletManagers.map { it.wallet.blockchain } + val currencies: List + get() = wallets.mapNotNull { it.currency } + fun getWalletManager(token: Token?): WalletManager? { if (token == null) return null val ethereumWalletManager = walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum } - return if (ethereumWalletManager?.presetTokens?.find { it == token } != null) { + return if (ethereumWalletManager?.presetTokens?.contains(token) == true) { ethereumWalletManager } else { val primaryWalletManager = walletManagers.find { it.wallet.blockchain == primaryBlockchain } - if (primaryWalletManager?.presetTokens?.find { it == token } != null) { + if (primaryWalletManager?.presetTokens?.contains(token) == true) { primaryWalletManager } else { ethereumWalletManager @@ -132,6 +135,11 @@ data class WalletState( return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets } + fun addWalletManagers(newWalletManagers: List): WalletState { + val updatedWalletManagers = this.walletManagers + + newWalletManagers.filterNot { this.blockchains.contains(it.wallet.blockchain) } + return copy(walletManagers = updatedWalletManagers) + } } sealed class WalletDialog: StateDialog { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 5487eec050..d35eea4fd3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -1,11 +1,9 @@ package com.tangem.tap.features.wallet.redux.middlewares -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.TokenFinder +import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction @@ -28,8 +26,8 @@ class MultiWalletMiddleware { ) { when (action) { is WalletAction.MultiWallet.AddWalletManagers -> { - val wallets = action.walletManagers.map { it.wallet } - store.state.globalState.feedbackManager?.infoHolder?.setWalletsInfo(wallets) + store.state.globalState.feedbackManager?.infoHolder + ?.setWalletsInfo(action.walletManagers) } is WalletAction.MultiWallet.SelectWallet -> { if (action.walletData != null) { @@ -43,9 +41,11 @@ class MultiWalletMiddleware { addToken(action.token, walletState, globalState) } is WalletAction.MultiWallet.AddTokens -> { - action.tokens.map { addToken(it, walletState, globalState) } + addTokens(action.tokens, walletState, globalState) } is WalletAction.MultiWallet.AddBlockchain -> { + if (walletState?.blockchains?.contains(action.blockchain) == true) return + globalState?.scanNoteResponse?.card?.let { card -> currenciesRepository.saveAddedBlockchain(card.cardId, action.blockchain) globalState.tapWalletManager.walletManagerFactory @@ -163,13 +163,44 @@ class MultiWalletMiddleware { ) scope.launch { - val result = walletManager?.addToken(token) - withContext(Dispatchers.Main) { - when (result) { + when (val result = walletManager?.addToken(token)) { + is Result.Success -> { + store.dispatchOnMain( + WalletAction.MultiWallet.TokenLoaded(amount = result.data, token = token) + ) + } + } + } + } + + private fun addTokens( + tokens: List, + walletState: WalletState?, + globalState: GlobalState?, + ) { + val card = globalState?.scanNoteResponse?.card ?: return + val tokensWithManagers = tokens.map { token -> + val walletManager = walletState?.getWalletManager(token) + ?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp( + card = card, + blockchain = Blockchain.Ethereum + )?.also { walletManager -> + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) + store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain)) + } + store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Token( + token = token, + blockchain = walletManager?.wallet?.blockchain ?: Blockchain.Ethereum + ))) + TokenWithManager(token, walletManager) + } + scope.launch { + tokensWithManagers.forEach { + when (val result = it.walletManager?.addToken(it.token)) { is Result.Success -> { - store.dispatch( + store.dispatchOnMain( WalletAction.MultiWallet.TokenLoaded( - amount = result.data, token = token + amount = result.data, token = it.token ) ) } @@ -177,4 +208,6 @@ class MultiWalletMiddleware { } } } + + private data class TokenWithManager(val token: Token, val walletManager: WalletManager?) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index 8c45a9b8c3..2f90f29e0f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFormattedCurrencyString @@ -18,9 +19,7 @@ class MultiWalletReducer { fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState { return when (action) { is WalletAction.MultiWallet.AddWalletManagers -> { - state.copy( - walletManagers = state.walletManagers + action.walletManagers - ) + state.addWalletManagers(action.walletManagers) } is WalletAction.MultiWallet.AddBlockchains -> { val wallets = action.blockchains.map { blockchain -> @@ -75,47 +74,12 @@ class MultiWalletReducer { } } is WalletAction.MultiWallet.AddTokens -> { - if (!state.isMultiwalletAllowed) return state - val wallets = action.tokens.map { token -> - val walletManager = state.getWalletManager(token)?.wallet - WalletData( - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - currency = token.name, - currencySymbol = token.symbol - ), - walletAddresses = createAddressList(walletManager), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(allowed = false), - currency = Currency.Token( - token = token, - blockchain = walletManager?.blockchain ?: Blockchain.Ethereum - ) - ) - } + val wallets = action.tokens.mapNotNull { token -> token.toWallet(state) } state.copy(wallets = state.replaceSomeWallets(wallets)) } is WalletAction.MultiWallet.AddToken -> { - if (!state.isMultiwalletAllowed) return state - val walletManager = state.getWalletManager(action.token)?.wallet - val walletAddresses = createAddressList(walletManager) - - val wallet = WalletData( - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - currency = action.token.name, - currencySymbol = action.token.symbol - ), - walletAddresses = walletAddresses, - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(allowed = false), - currency = Currency.Token( - token = action.token, - blockchain = walletManager?.blockchain ?: Blockchain.Ethereum - ) - ) - val wallets = state.replaceWalletInWallets(wallet) - state.copy(wallets = wallets) + val walletData = action.token.toWallet(state) ?: return state + state.copy(wallets = state.replaceWalletInWallets(walletData)) } is WalletAction.MultiWallet.TokenLoaded -> { val pendingTransactions = state.getWalletManager(action.token) @@ -182,4 +146,29 @@ class MultiWalletReducer { is WalletAction.MultiWallet.SaveCurrencies -> state } } +} + +fun Token.toWallet(state: WalletState): WalletData? { + if (!state.isMultiwalletAllowed) return null + if (state.currencies.any { it is Currency.Token && it.token == this }) { + return null + } + + val walletManager = state.getWalletManager(this)?.wallet + val walletAddresses = createAddressList(walletManager) + + return WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = this.name, + currencySymbol = this.symbol + ), + walletAddresses = walletAddresses, + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + currency = Currency.Token( + token = this, + blockchain = walletManager?.blockchain ?: Blockchain.Ethereum + ) + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index d69a984733..5daef27db2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -15,6 +15,7 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import org.rekotlin.Action import java.math.BigDecimal +import java.math.RoundingMode class WalletReducer { companion object { @@ -291,7 +292,7 @@ private fun setNewFiatRate( appCurrency: FiatCurrencyName, state: WalletState ): WalletState { val rate = fiatRate.second ?: return state - val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency) + val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency, RoundingMode.HALF_UP) val currency = fiatRate.first return if (!state.isMultiwalletAllowed) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 340238f2de..16f8f2b17b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -10,8 +10,7 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import com.squareup.picasso.Picasso -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.MainActivity +import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.global.StateDialog import com.tangem.tap.common.redux.navigation.NavigationAction @@ -25,8 +24,6 @@ import kotlinx.android.synthetic.main.fragment_details_twin_cards.* import kotlinx.android.synthetic.main.fragment_wallet_details.* import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar import kotlinx.android.synthetic.main.item_currency_wallet.view.* -import kotlinx.android.synthetic.main.item_currency_wallet.view.iv_currency -import kotlinx.android.synthetic.main.item_currency_wallet.view.tv_token_letter import kotlinx.android.synthetic.main.item_popular_token.view.* import kotlinx.android.synthetic.main.layout_balance_error.* import kotlinx.android.synthetic.main.layout_balance_wallet_details.* @@ -184,13 +181,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS if (state.state == ProgressState.Error) { if (state.error == ErrorType.NoInternetConnection) { srl_wallet_details?.isRefreshing = false - (activity as? MainActivity)?.showSnackbar( + (activity as? SnackbarHandler)?.showSnackbar( text = R.string.wallet_notification_no_internet, buttonTitle = R.string.common_retry ) { store.dispatch(WalletAction.LoadData) } } } else { - (activity as? MainActivity)?.dismissSnackbar() + (activity as? SnackbarHandler)?.dismissSnackbar() } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 8aa058881c..f5f61139b1 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -2,13 +2,11 @@ package com.tangem.tap.features.wallet.ui import android.os.Bundle import android.view.Menu -import android.view.Menu.NONE import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.get import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -17,6 +15,7 @@ import com.squareup.picasso.Picasso import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.tap.MainActivity import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.configurable.warningMessage.WarningMessage @@ -156,6 +155,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { + store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> store.dispatch(DetailsAction.PrepareScreen( scanNoteResponse.card, scanNoteResponse, diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index fc33c31d7d..70c51c9eb3 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -106,10 +106,10 @@ android:id="@+id/tv_signed_hashes" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/darkGray1" - android:textSize="16sp" android:paddingTop="7dp" android:paddingBottom="7dp" + android:textColor="@color/darkGray1" + android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_issuer" tools:text="48 hashes" /> @@ -120,7 +120,7 @@ android:layout_height="wrap_content" android:drawablePadding="15dp" android:paddingTop="7dp" - android:paddingBottom="10dp" + android:paddingBottom="7dp" android:text="@string/disclaimer_title" android:textColor="@color/darkGray6" android:textSize="16sp" @@ -132,16 +132,16 @@ android:id="@+id/tv_card_tou" android:layout_width="match_parent" android:layout_height="wrap_content" + android:drawablePadding="15dp" android:paddingTop="7dp" android:paddingBottom="10dp" - android:drawablePadding="15dp" + android:text="@string/details_row_title_card_tou" android:textColor="@color/darkGray6" android:textSize="16sp" android:visibility="gone" app:drawableEndCompat="@drawable/ic_angle_bracket_right" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" - android:text="@string/details_row_title_card_tou" /> + app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" /> + + + + + app:layout_constraintTop_toBottomOf="@id/barrier" /> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_wallet_connect_session.xml b/app/src/main/res/layout/item_wallet_connect_session.xml new file mode 100644 index 0000000000..b7ef47ef46 --- /dev/null +++ b/app/src/main/res/layout/item_wallet_connect_session.xml @@ -0,0 +1,57 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_details.xml b/app/src/main/res/layout/layout_wallet_details.xml index f8ac54763e..63bbb5f031 100644 --- a/app/src/main/res/layout/layout_wallet_details.xml +++ b/app/src/main/res/layout/layout_wallet_details.xml @@ -101,13 +101,13 @@ android:layout_marginTop="10dp" android:layout_marginEnd="14dp" android:ellipsize="middle" - android:maxLines="2" android:textColor="@color/darkGray1" android:textSize="13sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/iv_qr_code" app:layout_constraintTop_toTopOf="@id/iv_qr_code" - tools:text="139mrsJgyWnJjkljlkhiuojlkljkjljljlkjlkljlkjkljlkjlkjkljky9BV" /> + tools:text="139mrsJgyWnJjkljlkhiuojlkljkjljljlkjlkfsdkdsjflsdkjflsdkjfffffffljlkjkljlkjlkjkljky9BV" + android:singleLine="true" /> I understand There are only %s signatures available on this card. You must withdraw all of your funds. + + //Wallet Connect + Wallet Connect + Wallet Connect Sessions + WalletConnect session opened with %s + Ooops. No Sessions. + No opened WalletConnect sessions + Open session + Card: %s + Tap card to bind to wallet connect + + Card: %1s + Request to create transaction for %2s + \n%3s + + \n\nAmount: %4s + \nFee: %5s + \nTotal: %6s + \nBalance: %7s + + Can\'t send transaction. Not enough funds. + + Request to start a session for card with ID %1s\n + for %2s\n\n + + URL: %3s + + + Requesting to sign a message\nwith card %s\n\n + Message for %s:\n%s + + + Disconnect + Sign + Sign and send + Reject + + Clipboard contain WalletConnect code. Use copied value or scan QR-code + Paste from clipboard + Scan new code diff --git a/dependencies.gradle b/dependencies.gradle index b28e81e8b9..32b0ea5280 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,4 +1,4 @@ ext.versions = [ - kotlin : '1.5.0', + kotlin : '1.5.10', build_gradle: '4.2.1', ]