diff --git a/app/build.gradle b/app/build.gradle index 0c5338992b..54f56fcd1c 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,10 @@ 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-11' //TODO: set correct one + implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210607.120025-19' + implementation 'com.tangem:core:develop-36' + implementation 'com.tangem:sdk:develop-36' // WebView implementation "androidx.browser:browser:1.3.0" @@ -106,6 +109,9 @@ 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' + 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..b0328ee0c2 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 @@ -38,4 +38,7 @@ sealed class GlobalAction : Action { data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() data class SendFeedback(val emailData: EmailData) : 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/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/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/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 99e2cba747..00e6acb92f 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 @@ -28,7 +28,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 +36,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 = 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..e0d37ec8e0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -0,0 +1,240 @@ +package com.tangem.tap.domain.walletconnect + +import com.google.common.base.CharMatcher +import com.tangem.Message +import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader +import com.tangem.blockchain.blockchains.ethereum.EthereumHelper +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +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.Result +import com.tangem.blockchain.extensions.Signer +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.extensions.hexToBigDecimal +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 org.kethereum.crypto.api.ec.ECDSASignature +import org.kethereum.crypto.determineRecId +import org.kethereum.crypto.impl.ec.canonicalise +import org.kethereum.keccakshortcut.keccak +import org.kethereum.model.PublicKey +import timber.log.Timber +import java.math.BigDecimal +import java.math.BigInteger + +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 = EthereumHelper.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).keccak() + + + 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("") + } + + private fun Char.isAscii(): Boolean = CharMatcher.ascii().matches(this) + + 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() + + val r = BigInteger(1, hash.copyOfRange(0, 32)) + val s = BigInteger(1, hash.copyOfRange(32, 64)) + + val ecdsaSignature = ECDSASignature(r, s).canonicalise() + + val recId = ecdsaSignature.determineRecId(hashToSign, + PublicKey(publicKey.sliceArray(1..64))) + val v = (recId + 27).toBigInteger() + + return HEX_PREFIX + ecdsaSignature.r.toString(16) + ecdsaSignature.s.toString(16) + + v.toString(16) + } + 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/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/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/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index fc33c31d7d..35ff627980 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" /> @@ -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/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 339ccae0b2..d84ec691c4 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -113,4 +113,44 @@ this wallet. 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