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