Updated on 2026-08-14
This commit is contained in:
commit
078ddad6fc
64 changed files with 2058 additions and 207 deletions
|
|
@ -65,6 +65,8 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.aar'], dir: 'libs')
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
|
|
@ -75,9 +77,9 @@ dependencies {
|
|||
implementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'com.tangem:blockchain:develop-11'
|
||||
implementation 'com.tangem:core:develop-15'
|
||||
implementation 'com.tangem:sdk:develop-15'
|
||||
implementation 'com.tangem:blockchain:develop-17'
|
||||
implementation 'com.tangem:core:develop-42'
|
||||
implementation 'com.tangem:sdk:develop-42'
|
||||
|
||||
// WebView
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
|
|
@ -106,6 +108,20 @@ dependencies {
|
|||
implementation 'com.google.firebase:firebase-config-ktx'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
|
||||
// WalletConnect
|
||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// Network and Json
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
|
||||
implementation 'com.squareup.moshi:moshi:1.9.2'
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||
//
|
||||
// //Crypto
|
||||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.google.truth:truth:1.0.1"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
|
|
|
|||
BIN
app/libs/walletconnect.aar
Normal file
BIN
app/libs/walletconnect.aar
Normal file
Binary file not shown.
|
|
@ -78,6 +78,14 @@
|
|||
<meta-data
|
||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="wc"/>
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
|
||||
|
|
|
|||
|
|
@ -882,6 +882,12 @@
|
|||
"name" : "WAX",
|
||||
"contractAddress" : "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022"
|
||||
},
|
||||
{
|
||||
"decimalCount" : 18,
|
||||
"symbol" : "WETH",
|
||||
"name" : "Wrapped Ether",
|
||||
"contractAddress" : "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
},
|
||||
{
|
||||
"decimalCount" : 18,
|
||||
"symbol" : "WPR",
|
||||
|
|
|
|||
|
|
@ -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<Tag>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
55
app/src/main/java/com/tangem/tap/common/DialogManager.kt
Normal file
55
app/src/main/java/com/tangem/tap/common/DialogManager.kt
Normal file
|
|
@ -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<GlobalState> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/com/tangem/tap/common/IntentHandler.kt
Normal file
41
app/src/main/java/com/tangem/tap/common/IntentHandler.kt
Normal file
|
|
@ -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<Tag>(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()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
15
app/src/main/java/com/tangem/tap/common/extensions/Store.kt
Normal file
15
app/src/main/java/com/tangem/tap/common/extensions/Store.kt
Normal file
|
|
@ -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<AppState>.dispatchOnMain(action: Action) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(action)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
|
|
@ -38,4 +39,8 @@ sealed class GlobalAction : Action {
|
|||
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
|
||||
|
||||
data class SendFeedback(val emailData: EmailData) : GlobalAction()
|
||||
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
|
||||
|
||||
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
|
||||
object HideDialog : GlobalAction()
|
||||
}
|
||||
|
|
@ -56,6 +56,10 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
|
|||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
|
||||
}
|
||||
is GlobalAction.UpdateFeedbackInfo -> {
|
||||
store.state.globalState.feedbackManager?.infoHolder
|
||||
?.setWalletsInfo(action.walletManagers)
|
||||
}
|
||||
}
|
||||
nextDispatch(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -33,10 +33,14 @@ class TangemSdkManager(val activity: ComponentActivity) {
|
|||
activity, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
|
||||
)
|
||||
|
||||
suspend fun scanNote(analyticsHandler: AnalyticsHandler): CompletionResult<ScanNoteResponse> {
|
||||
suspend fun scanNote(
|
||||
analyticsHandler: AnalyticsHandler, messageRes: Int? = null
|
||||
): CompletionResult<ScanNoteResponse> {
|
||||
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<Card> {
|
||||
|
|
|
|||
|
|
@ -184,23 +184,32 @@ class TapWalletManager {
|
|||
private fun loadMultiWalletData(
|
||||
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
|
||||
) {
|
||||
val presetTokens = primaryWalletManager?.presetTokens?.toList() ?: emptyList()
|
||||
val presetTokens = primaryWalletManager?.presetTokens ?: emptySet()
|
||||
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
|
||||
|
||||
if (savedCurrencies == null) {
|
||||
if (primaryBlockchain != null && primaryWalletManager != null) {
|
||||
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
|
||||
CardCurrencies(
|
||||
blockchains = listOf(primaryBlockchain), tokens = presetTokens
|
||||
blockchains = setOf(primaryBlockchain), tokens = presetTokens
|
||||
)))
|
||||
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
|
||||
store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens))
|
||||
store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens.toList()))
|
||||
} else {
|
||||
val blockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
|
||||
CardCurrencies(blockchains = blockchains, tokens = emptySet())
|
||||
))
|
||||
val walletManagers =
|
||||
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
|
||||
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList()))
|
||||
}
|
||||
store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse(card, walletManagerFactory))
|
||||
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
|
||||
} else {
|
||||
val blockchains = savedCurrencies.blockchains
|
||||
val blockchains = savedCurrencies.blockchains.toList()
|
||||
val walletManagers = if (
|
||||
presetTokens.isNotEmpty() &&
|
||||
primaryWalletManager != null && primaryBlockchain != null
|
||||
|
|
@ -214,8 +223,7 @@ class TapWalletManager {
|
|||
|
||||
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains))
|
||||
store.dispatch(WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens))
|
||||
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
|
||||
store.dispatch(WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import org.spongycastle.util.encoders.Base64
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
|
@ -37,7 +37,8 @@ class TopUpHelper {
|
|||
val sha256Hmac = Mac.getInstance("HmacSHA256")
|
||||
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
|
||||
sha256Hmac.init(secretKey)
|
||||
return Base64.toBase64String(sha256Hmac.doFinal(data.toByteArray()))
|
||||
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
|
||||
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,16 +3,13 @@ package com.tangem.tap.domain.extensions
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.commands.common.card.EllipticCurve
|
||||
import org.stellar.sdk.requests.ErrorResponse
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
||||
fun Blockchain.isNoAccountError(exception: Throwable): Boolean {
|
||||
return when (this) {
|
||||
Blockchain.Stellar -> {
|
||||
(exception is ErrorResponse) && (exception.code == 404)
|
||||
}
|
||||
Blockchain.XRP -> exception.message?.contains("Account not found") == true
|
||||
Blockchain.Stellar, Blockchain.XRP ->
|
||||
exception.message?.contains("Account not found") == true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ fun Card.getToken(): Token? {
|
|||
val symbol = cardData?.tokenSymbol ?: return null
|
||||
val contractAddress = cardData?.tokenContractAddress ?: return null
|
||||
val decimals = cardData?.tokenDecimal ?: return null
|
||||
if (symbol.isBlank() || contractAddress.isBlank()) return null
|
||||
return Token(symbol, contractAddress, decimals)
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ fun Card.getSingleWallet(): CardWallet? {
|
|||
fun Card.getStatus(): CardStatus {
|
||||
if (firmwareVersion < FirmwareConstraints.AvailabilityVersions.walletData) return status!!
|
||||
|
||||
return if (getWallets().any { it.status == WalletStatus.Loaded }) {
|
||||
return if (wallets.any { it.status == WalletStatus.Loaded }) {
|
||||
CardStatus.Loaded
|
||||
} else {
|
||||
CardStatus.Empty
|
||||
|
|
@ -36,11 +37,11 @@ fun Card.getStatus(): CardStatus {
|
|||
}
|
||||
|
||||
fun Card.hasSignedHashes(): Boolean {
|
||||
return getWallets().any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 }
|
||||
return wallets.any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 }
|
||||
}
|
||||
|
||||
fun Card.signedHashesCount(): Int {
|
||||
return getWallets().map { it.signedHashes ?: 0 }.sum()
|
||||
return wallets.map { it.signedHashes ?: 0 }.sum()
|
||||
}
|
||||
|
||||
val Card.remainingSignatures: Int?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
|
|||
is CompletionResult.Success -> {
|
||||
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<ScanNoteRespons
|
|||
return
|
||||
}
|
||||
|
||||
val curvesPresent = card.getWallets().map { it.curve }
|
||||
val curvesPresent = card.wallets.map { it.curve }
|
||||
val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
|
||||
|
||||
if (curvesToCreate.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import com.tangem.tap.network.createMoshi
|
|||
|
||||
class CurrenciesRepository(val context: Application) {
|
||||
private val moshi = createMoshi()
|
||||
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, TokenDao::class.java)
|
||||
private val tokensAdapter: JsonAdapter<Set<TokenDao>> = moshi.adapter(
|
||||
Types.newParameterizedType(Set::class.java, TokenDao::class.java)
|
||||
)
|
||||
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, Blockchain::class.java)
|
||||
private val blockchainsAdapter: JsonAdapter<Set<Blockchain>> = moshi.adapter(
|
||||
Types.newParameterizedType(Set::class.java, Blockchain::class.java)
|
||||
)
|
||||
|
||||
fun loadCardCurrencies(cardId: String): CardCurrencies? {
|
||||
|
|
@ -43,45 +43,45 @@ class CurrenciesRepository(val context: Application) {
|
|||
}
|
||||
|
||||
fun removeToken(cardId: String, token: Token) {
|
||||
val tokens = loadSavedTokens(cardId).filterNot { it == token }
|
||||
val tokens = loadSavedTokens(cardId).filterNot { it == token }.toSet()
|
||||
saveTokens(cardId, tokens)
|
||||
}
|
||||
|
||||
fun removeBlockchain(cardId: String, blockchain: Blockchain) {
|
||||
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }
|
||||
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }.toSet()
|
||||
saveBlockchains(cardId, blockchains)
|
||||
}
|
||||
|
||||
private fun loadSavedTokens(cardId: String): List<Token> {
|
||||
private fun loadSavedTokens(cardId: String): Set<Token> {
|
||||
return try {
|
||||
val json = context.readFileText(getFileNameForTokens(cardId))
|
||||
tokensAdapter.fromJson(json)!!.map { it.toToken() }
|
||||
tokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTokens(cardId: String, tokens: List<Token>) {
|
||||
val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) })
|
||||
private fun saveTokens(cardId: String, tokens: Set<Token>) {
|
||||
val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }.toSet())
|
||||
context.rewriteFile(json, getFileNameForTokens(cardId))
|
||||
}
|
||||
|
||||
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
|
||||
private fun loadSavedBlockchains(cardId: String): Set<Blockchain> {
|
||||
return try {
|
||||
val json = context.readFileText(getFileNameForBlockchains(cardId))
|
||||
blockchainsAdapter.fromJson(json) ?: emptyList()
|
||||
blockchainsAdapter.fromJson(json) ?: emptySet()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBlockchains(cardId: String, blockchains: List<Blockchain>) {
|
||||
private fun saveBlockchains(cardId: String, blockchains: Set<Blockchain>) {
|
||||
val json = blockchainsAdapter.toJson(blockchains)
|
||||
context.rewriteFile(json, getFileNameForBlockchains(cardId))
|
||||
}
|
||||
|
||||
private fun Context.readFileText(fileName: String): String =
|
||||
this.openFileInput(fileName).bufferedReader().readText()
|
||||
this.openFileInput(fileName).bufferedReader().readText()
|
||||
|
||||
private fun Context.rewriteFile(content: String, fileName: String) {
|
||||
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
|
||||
|
|
@ -108,12 +108,13 @@ class CurrenciesRepository(val context: Application) {
|
|||
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
|
||||
|
||||
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
|
||||
fun getFileNameForBlockchains(cardId: String): String = "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
|
||||
fun getFileNameForBlockchains(cardId: String): String =
|
||||
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
|
||||
|
||||
private val secp256k1Blochcains = listOf(
|
||||
Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin,
|
||||
Blockchain.XRP, Blockchain.Tezos,
|
||||
Blockchain.Ethereum, Blockchain.RSK)
|
||||
Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin,
|
||||
Blockchain.XRP, Blockchain.Tezos,
|
||||
Blockchain.Ethereum, Blockchain.RSK)
|
||||
private val ed25519Blockchains = listOf(Blockchain.CardanoShelley, Blockchain.Stellar)
|
||||
}
|
||||
}
|
||||
|
|
@ -121,18 +122,24 @@ class CurrenciesRepository(val context: Application) {
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenDao(
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val contractAddress: String,
|
||||
val decimalCount: Int
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val contractAddress: String,
|
||||
val decimalCount: Int,
|
||||
) {
|
||||
fun toToken(): Token {
|
||||
return Token(name = name, symbol = symbol, contractAddress = contractAddress, decimals = decimalCount)
|
||||
return Token(name = name,
|
||||
symbol = symbol,
|
||||
contractAddress = contractAddress,
|
||||
decimals = decimalCount)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromToken(token: Token): TokenDao {
|
||||
return TokenDao(name = token.name, symbol = token.symbol, contractAddress = token.contractAddress, decimalCount = token.decimals)
|
||||
return TokenDao(name = token.name,
|
||||
symbol = token.symbol,
|
||||
contractAddress = token.contractAddress,
|
||||
decimalCount = token.decimals)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -140,21 +147,25 @@ data class TokenDao(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardCurrenciesDao(
|
||||
val tokens: List<TokenDao>,
|
||||
val blockchains: List<Blockchain>
|
||||
val tokens: Set<TokenDao>,
|
||||
val blockchains: Set<Blockchain>,
|
||||
) {
|
||||
fun toCardCurrencies(): CardCurrencies {
|
||||
return CardCurrencies(tokens = tokens.map { it.toToken() }, blockchains = blockchains)
|
||||
return CardCurrencies(tokens = tokens.map { it.toToken() }.toSet(),
|
||||
blockchains = blockchains)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao {
|
||||
return CardCurrenciesDao(tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }, blockchains = cardCurrencies.blockchains)
|
||||
return CardCurrenciesDao(
|
||||
tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.toSet(),
|
||||
blockchains = cardCurrencies.blockchains
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CardCurrencies(
|
||||
val tokens: List<Token>,
|
||||
val blockchains: List<Blockchain>
|
||||
val tokens: Set<Token>,
|
||||
val blockchains: Set<Blockchain>,
|
||||
)
|
||||
|
|
@ -86,13 +86,11 @@ fun Card.getTwinCardIdForUser(): String {
|
|||
}
|
||||
|
||||
fun Card.changeStatusToLoaded(): Card {
|
||||
val wallets = getWallets().map { it.copy(status = WalletStatus.Loaded) }
|
||||
return copy(status = CardStatus.Loaded)
|
||||
.also { it.setWallets(wallets) }
|
||||
val wallets = wallets.map { it.copy(status = WalletStatus.Loaded) }
|
||||
return copy(status = CardStatus.Loaded, wallets = wallets)
|
||||
}
|
||||
|
||||
fun Card.changeStatusToEmpty(): Card {
|
||||
val wallets = getWallets().map { it.copy(status = WalletStatus.Empty) }
|
||||
return copy(status = CardStatus.Empty)
|
||||
.also { it.setWallets(wallets) }
|
||||
val wallets = wallets.map { it.copy(status = WalletStatus.Empty) }
|
||||
return copy(status = CardStatus.Empty, wallets = wallets)
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
package com.tangem.tap.domain.walletconnect
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.*
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.walletConnectRepository
|
||||
import com.trustwallet.walletconnect.WCClient
|
||||
import com.trustwallet.walletconnect.models.WCPeerMeta
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
|
||||
import com.trustwallet.walletconnect.models.session.WCSession
|
||||
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WalletConnectManager {
|
||||
|
||||
private val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(interceptor)
|
||||
.addInterceptor(RetryInterceptor())
|
||||
.build()
|
||||
}
|
||||
private val interceptor by lazy {
|
||||
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
|
||||
}
|
||||
|
||||
private var sessions: MutableMap<WCSession, WalletConnectActiveData> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<SessionDao>> = 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<WalletConnectSession> {
|
||||
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<WalletConnectSession>) {
|
||||
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
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package com.tangem.tap.domain.walletconnect
|
||||
|
||||
import com.tangem.Message
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.extensions.*
|
||||
import com.tangem.commands.SignCommand
|
||||
import com.tangem.commands.wallet.WalletIndex
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.tap.common.extensions.toFormattedString
|
||||
import com.tangem.tap.features.details.redux.walletconnect.*
|
||||
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
|
||||
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdk
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WalletConnectSdkHelper {
|
||||
|
||||
suspend fun prepareTransactionData(
|
||||
transaction: WCEthereumTransaction,
|
||||
session: WalletConnectSession,
|
||||
id: Long,
|
||||
type: WcTransactionType,
|
||||
): WcTransactionData? {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val walletManager = factory.makeEthereumWalletManager(
|
||||
session.wallet.cardId,
|
||||
session.wallet.walletPublicKey.hexToBytes(),
|
||||
emptyList(),
|
||||
isTestNet = session.wallet.isTestNet
|
||||
) ?: return null
|
||||
|
||||
|
||||
try {
|
||||
walletManager.update()
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception)
|
||||
return null
|
||||
}
|
||||
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
|
||||
val balance =
|
||||
walletManager.wallet.amounts[AmountType.Coin]?.value ?: return null
|
||||
|
||||
val gas = transaction.gas?.hexToBigDecimal()
|
||||
?: transaction.gasLimit?.hexToBigDecimal() ?: return null
|
||||
|
||||
val decimals = blockchain.decimals()
|
||||
|
||||
val value = transaction.value?.hexToBigDecimal()
|
||||
?.movePointLeft(decimals) ?: return null
|
||||
|
||||
val gasPrice = transaction.gasPrice?.hexToBigDecimal()
|
||||
?: when (val result =
|
||||
(walletManager as? EthereumGasLoader)?.getGasPrice()) {
|
||||
is Result.Success -> result.data.toBigDecimal()
|
||||
is Result.Failure -> {
|
||||
Timber.e(result.error)
|
||||
return null
|
||||
}
|
||||
null -> return null
|
||||
}
|
||||
|
||||
val fee = (gas * gasPrice).movePointLeft(decimals)
|
||||
val total = value + fee
|
||||
|
||||
val transactionData = TransactionData(
|
||||
amount = Amount(value, blockchain),
|
||||
fee = Amount(fee, blockchain),
|
||||
sourceAddress = transaction.from,
|
||||
destinationAddress = transaction.to!!,
|
||||
extras = EthereumTransactionExtras(
|
||||
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
|
||||
gasLimit = gas.toBigInteger(),
|
||||
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger()
|
||||
)
|
||||
)
|
||||
val dialogData = TransactionRequestDialogData(
|
||||
cardId = session.wallet.cardId,
|
||||
dAppName = session.peerMeta.name,
|
||||
dAppUrl = session.peerMeta.url,
|
||||
amount = value.toFormattedString(decimals),
|
||||
gasAmount = fee.toFormattedString(decimals),
|
||||
totalAmount = total.toFormattedString(decimals),
|
||||
balance = balance.toFormattedString(decimals),
|
||||
isEnoughFundsToSend = (balance - total) >= BigDecimal.ZERO,
|
||||
session = session.session,
|
||||
id = id,
|
||||
type = type
|
||||
)
|
||||
return WcTransactionData(
|
||||
type = type,
|
||||
transaction = transactionData,
|
||||
session = session,
|
||||
id = id,
|
||||
walletManager = walletManager,
|
||||
dialogData = dialogData
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun completeTransaction(data: WcTransactionData): String? {
|
||||
return when (data.type) {
|
||||
WcTransactionType.EthSendTransaction -> sendTransaction(data)
|
||||
WcTransactionType.EthSignTransaction -> signTransaction(data)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendTransaction(data: WcTransactionData): String? {
|
||||
val result = (data.walletManager as TransactionSender).send(
|
||||
transactionData = data.transaction,
|
||||
signer = Signer(tangemSdk)
|
||||
)
|
||||
return when (result) {
|
||||
SimpleResult.Success -> {
|
||||
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
|
||||
}
|
||||
is SimpleResult.Failure -> {
|
||||
Timber.e(result.error)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signTransaction(data: WcTransactionData): String? {
|
||||
val dataToSign = EthereumUtils.buildTransactionToSign(
|
||||
transactionData = data.transaction,
|
||||
nonce = null,
|
||||
blockchain = data.walletManager.wallet.blockchain,
|
||||
gasLimit = null
|
||||
) ?: return null
|
||||
|
||||
val command = SignCommand(
|
||||
hashes = arrayOf(dataToSign.hash),
|
||||
walletIndex = WalletIndex.PublicKey(data.walletManager.wallet.publicKey)
|
||||
)
|
||||
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message())
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
HEX_PREFIX + result.data
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e(result.error.customMessage)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareDataForPersonalSign(
|
||||
message: WCEthereumSignMessage,
|
||||
session: WalletConnectSession,
|
||||
id: Long,
|
||||
): WcPersonalSignData {
|
||||
val messageData = message.data.removePrefix(HEX_PREFIX).hexToBytes()
|
||||
val messageString = message.data.hexToAscii() ?: message.data
|
||||
|
||||
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
|
||||
val hashToSign = (prefixData + messageData).toKeccak()
|
||||
|
||||
|
||||
val dialogData = PersonalSignDialogData(
|
||||
cardId = session.wallet.cardId,
|
||||
dAppName = session.peerMeta.name,
|
||||
message = messageString,
|
||||
session = session.session,
|
||||
id = id
|
||||
)
|
||||
return WcPersonalSignData(
|
||||
hash = hashToSign,
|
||||
session = session,
|
||||
id = id,
|
||||
dialogData = dialogData
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.hexToAscii(): String? {
|
||||
return removePrefix(HEX_PREFIX).hexToBytes()
|
||||
.map {
|
||||
val char = it.toInt().toChar()
|
||||
if (char.isAscii()) char else return null
|
||||
}
|
||||
.joinToString("")
|
||||
}
|
||||
|
||||
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
|
||||
val publicKey = wallet.walletPublicKey.hexToBytes()
|
||||
val command = SignCommand(
|
||||
arrayOf(hashToSign),
|
||||
WalletIndex.PublicKey(publicKey))
|
||||
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
|
||||
is CompletionResult.Success -> {
|
||||
val hash = result.data.signatures.first()
|
||||
|
||||
return EthereumUtils.prepareSignedMessageData(
|
||||
hash, hashToSign, publicKey
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e(result.error.customMessage)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
|
||||
private const val HEX_PREFIX = "0x"
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import com.tangem.tap.domain.twins.TwinsHelper
|
|||
import com.tangem.tap.domain.twins.isTwinCard
|
||||
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletReducer
|
||||
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
|
||||
import com.tangem.tap.features.wallet.models.toPendingTransactions
|
||||
import org.rekotlin.Action
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -89,7 +90,8 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS
|
|||
|| state.card?.settingsMask?.contains(Settings.IsReusable) == false
|
||||
|| state.card?.isWalletDataSupported == true
|
||||
val notEmpty = state.wallets.any {
|
||||
!it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty()
|
||||
!it.recentTransactions.toPendingTransactions(it.address).isNullOrEmpty()
|
||||
|| it.amounts.toSendableAmounts().isNotEmpty()
|
||||
}
|
||||
val eraseWalletState = when {
|
||||
notAllowedByCard -> EraseWalletState.NotAllowedByCard
|
||||
|
|
|
|||
|
|
@ -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<WalletConnectSession>) :
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppState> = { 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletConnectSession> = 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,
|
||||
|
||||
)
|
||||
|
|
@ -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<Det
|
|||
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
|
||||
}
|
||||
|
||||
tv_wallet_connect.setOnClickListener {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
|
||||
}
|
||||
|
||||
tv_security_title.setOnClickListener {
|
||||
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.zxing.Result
|
||||
import com.otaliastudios.cameraview.CameraView
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.store
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
|
||||
class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
|
||||
|
||||
private var scannerView: ZXingScannerView? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
if (!permissionIsGranted()) requestPermission()
|
||||
|
||||
scannerView = ZXingScannerView(activity)
|
||||
return scannerView
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
scannerView?.stopCamera()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
scannerView?.setResultHandler(this)
|
||||
scannerView?.startCamera()
|
||||
}
|
||||
|
||||
override fun handleResult(result: Result) {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
|
||||
if (!result.text.isNullOrBlank()) {
|
||||
store.dispatch(WalletConnectAction.ScanCard(result.text))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<WalletConnectSession, WalletConnectSessionsAdapter.SessionsViewHolder>(
|
||||
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<WalletConnectSession>() {
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletConnectState> {
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -12,10 +12,7 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.content.FileProvider
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.commands.common.card.Card
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.domain.TapWorkarounds
|
||||
|
|
@ -138,6 +135,7 @@ class AdditionalEmailInfo {
|
|||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var address: String = "",
|
||||
var explorerLink: String = "",
|
||||
var host: String = "",
|
||||
// var outputsCount: String = ""
|
||||
// var transactionHex: String = ""
|
||||
)
|
||||
|
|
@ -178,14 +176,28 @@ class AdditionalEmailInfo {
|
|||
signedHashesCount = card.signedHashesCount().toString()
|
||||
}
|
||||
|
||||
fun setWalletsInfo(wallets: List<Wallet>) {
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
wallets.forEach { walletsInfo.add(EmailWalletInfo(it.blockchain, getAddress(it), getExploreUri(it))) }
|
||||
walletManagers.forEach { manager ->
|
||||
walletsInfo.add(
|
||||
EmailWalletInfo(
|
||||
blockchain = manager.wallet.blockchain,
|
||||
address = getAddress(manager.wallet),
|
||||
explorerLink = getExploreUri(manager.wallet),
|
||||
host = manager.currentHost
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateOnSendError(wallet: Wallet, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
|
||||
fun updateOnSendError(wallet: Wallet, host: String, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
|
||||
val amountState = store.state.sendState.amountState
|
||||
onSendErrorWalletInfo = EmailWalletInfo(wallet.blockchain, getAddress(wallet), getExploreUri(wallet))
|
||||
onSendErrorWalletInfo = EmailWalletInfo(
|
||||
blockchain = wallet.blockchain,
|
||||
address = getAddress(wallet),
|
||||
explorerLink = getExploreUri(wallet),
|
||||
host = host
|
||||
)
|
||||
|
||||
this.destinationAddress = destinationAddress
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
|
||||
|
|
@ -267,6 +279,7 @@ class SendTransactionFailedEmail(private val error: String) : EmailData {
|
|||
appendKeyValue("Error", error)
|
||||
appendKeyValue("Card ID", infoHolder.cardId)
|
||||
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
appendKeyValue("Host", walletInfo.host)
|
||||
appendKeyValue("Token", infoHolder.token)
|
||||
appendKeyValue("Source address", walletInfo.address)
|
||||
appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
|
|
@ -291,6 +304,7 @@ class FeedbackEmail : EmailData {
|
|||
|
||||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,15 +83,13 @@ class AmountMiddleware {
|
|||
val sendState = appState?.sendState ?: return
|
||||
|
||||
dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false))
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
|
||||
if (SendState.isReadyToRequestFee()) {
|
||||
dispatch(FeeAction.RequestFee)
|
||||
if (!sendState.amountState.isCoinAmount()) return
|
||||
|
||||
if (sendState.amountState.isCoinAmount()) {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true))
|
||||
dispatch(FeeActionUi.ChangeIncludeFee(true))
|
||||
}
|
||||
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
if (SendState.isReadyToRequestFee()) dispatch(FeeAction.RequestFee)
|
||||
}
|
||||
|
||||
private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Signer
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.commands.SignResponse
|
||||
import com.tangem.commands.common.card.Card
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
|
||||
|
|
@ -162,7 +160,13 @@ private fun sendTransaction(
|
|||
when {
|
||||
message == null -> {
|
||||
dispatch(SendAction.SendError(TapError.UnknownError))
|
||||
infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress)
|
||||
infoHolder?.updateOnSendError(
|
||||
wallet = walletManager.wallet,
|
||||
host = walletManager.currentHost,
|
||||
amountToSend = amountToSend,
|
||||
feeAmount = feeAmount,
|
||||
destinationAddress = destinationAddress
|
||||
)
|
||||
dispatch(SendAction.Dialog.SendTransactionFails("unknown error"))
|
||||
}
|
||||
message.contains("50002") -> {
|
||||
|
|
@ -177,7 +181,13 @@ private fun sendTransaction(
|
|||
Timber.e(throwable)
|
||||
FirebaseCrashlytics.getInstance().recordException(throwable)
|
||||
dispatch(SendAction.SendError(TapError.CustomError(message)))
|
||||
infoHolder?.updateOnSendError(walletManager.wallet, amountToSend, feeAmount, destinationAddress)
|
||||
infoHolder?.updateOnSendError(
|
||||
wallet = walletManager.wallet,
|
||||
host = walletManager.currentHost,
|
||||
amountToSend = amountToSend,
|
||||
feeAmount = feeAmount,
|
||||
destinationAddress = destinationAddress
|
||||
)
|
||||
dispatch(SendAction.Dialog.SendTransactionFails(message))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
val mainCurrency = sp.getString("mainCurrency", TapCurrency.DEFAULT_FIAT_CURRENCY)
|
||||
val foundType = MainCurrencyType.values()
|
||||
.firstOrNull { it.name.toLowerCase() == mainCurrency!!.toLowerCase() }
|
||||
?: MainCurrencyType.FIAT
|
||||
?: MainCurrencyType.CRYPTO
|
||||
return foundType
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
|
|||
tokensState.copy(currencies = action.currencies)
|
||||
}
|
||||
is TokensAction.SetAddedCurrencies -> {
|
||||
|
||||
|
||||
tokensState.copy(addedCurrencies = action.wallets.toCardCurrencies())
|
||||
}
|
||||
is TokensAction.LoadCardTokens.Success -> {
|
||||
|
|
@ -35,7 +33,7 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
|
|||
}
|
||||
|
||||
private fun List<WalletData>.toCardCurrencies(): CardCurrencies {
|
||||
val tokens = mapNotNull { (it.currency as? Currency.Token)?.token }
|
||||
val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain }
|
||||
val tokens = mapNotNull { (it.currency as? Currency.Token)?.token }.toSet()
|
||||
val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain }.toSet()
|
||||
return CardCurrencies(tokens = tokens, blockchains = blockchains)
|
||||
}
|
||||
|
|
@ -7,10 +7,8 @@ import androidx.annotation.StringRes
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.squareup.picasso.Callback
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.IconsUtil
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
|
|
@ -20,7 +18,6 @@ import com.tangem.tap.store
|
|||
import com.tangem.wallet.R
|
||||
import kotlinx.android.synthetic.main.item_currency_subtitle.view.*
|
||||
import kotlinx.android.synthetic.main.item_popular_token.view.*
|
||||
import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>(DiffUtilCallback) {
|
||||
|
|
|
|||
|
|
@ -44,14 +44,17 @@ data class WalletState(
|
|||
val blockchains: List<Blockchain>
|
||||
get() = walletManagers.map { it.wallet.blockchain }
|
||||
|
||||
val currencies: List<Currency>
|
||||
get() = wallets.mapNotNull { it.currency }
|
||||
|
||||
fun getWalletManager(token: Token?): WalletManager? {
|
||||
if (token == null) return null
|
||||
val ethereumWalletManager = walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }
|
||||
return if (ethereumWalletManager?.presetTokens?.find { it == token } != null) {
|
||||
return if (ethereumWalletManager?.presetTokens?.contains(token) == true) {
|
||||
ethereumWalletManager
|
||||
} else {
|
||||
val primaryWalletManager = walletManagers.find { it.wallet.blockchain == primaryBlockchain }
|
||||
if (primaryWalletManager?.presetTokens?.find { it == token } != null) {
|
||||
if (primaryWalletManager?.presetTokens?.contains(token) == true) {
|
||||
primaryWalletManager
|
||||
} else {
|
||||
ethereumWalletManager
|
||||
|
|
@ -132,6 +135,11 @@ data class WalletState(
|
|||
return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets
|
||||
}
|
||||
|
||||
fun addWalletManagers(newWalletManagers: List<WalletManager>): WalletState {
|
||||
val updatedWalletManagers = this.walletManagers +
|
||||
newWalletManagers.filterNot { this.blockchains.contains(it.wallet.blockchain) }
|
||||
return copy(walletManagers = updatedWalletManagers)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class WalletDialog: StateDialog {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package com.tangem.tap.features.wallet.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.TokenFinder
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -28,8 +26,8 @@ class MultiWalletMiddleware {
|
|||
) {
|
||||
when (action) {
|
||||
is WalletAction.MultiWallet.AddWalletManagers -> {
|
||||
val wallets = action.walletManagers.map { it.wallet }
|
||||
store.state.globalState.feedbackManager?.infoHolder?.setWalletsInfo(wallets)
|
||||
store.state.globalState.feedbackManager?.infoHolder
|
||||
?.setWalletsInfo(action.walletManagers)
|
||||
}
|
||||
is WalletAction.MultiWallet.SelectWallet -> {
|
||||
if (action.walletData != null) {
|
||||
|
|
@ -43,9 +41,11 @@ class MultiWalletMiddleware {
|
|||
addToken(action.token, walletState, globalState)
|
||||
}
|
||||
is WalletAction.MultiWallet.AddTokens -> {
|
||||
action.tokens.map { addToken(it, walletState, globalState) }
|
||||
addTokens(action.tokens, walletState, globalState)
|
||||
}
|
||||
is WalletAction.MultiWallet.AddBlockchain -> {
|
||||
if (walletState?.blockchains?.contains(action.blockchain) == true) return
|
||||
|
||||
globalState?.scanNoteResponse?.card?.let { card ->
|
||||
currenciesRepository.saveAddedBlockchain(card.cardId, action.blockchain)
|
||||
globalState.tapWalletManager.walletManagerFactory
|
||||
|
|
@ -163,13 +163,44 @@ class MultiWalletMiddleware {
|
|||
)
|
||||
|
||||
scope.launch {
|
||||
val result = walletManager?.addToken(token)
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
when (val result = walletManager?.addToken(token)) {
|
||||
is Result.Success -> {
|
||||
store.dispatchOnMain(
|
||||
WalletAction.MultiWallet.TokenLoaded(amount = result.data, token = token)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTokens(
|
||||
tokens: List<Token>,
|
||||
walletState: WalletState?,
|
||||
globalState: GlobalState?,
|
||||
) {
|
||||
val card = globalState?.scanNoteResponse?.card ?: return
|
||||
val tokensWithManagers = tokens.map { token ->
|
||||
val walletManager = walletState?.getWalletManager(token)
|
||||
?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp(
|
||||
card = card,
|
||||
blockchain = Blockchain.Ethereum
|
||||
)?.also { walletManager ->
|
||||
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
|
||||
}
|
||||
store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Token(
|
||||
token = token,
|
||||
blockchain = walletManager?.wallet?.blockchain ?: Blockchain.Ethereum
|
||||
)))
|
||||
TokenWithManager(token, walletManager)
|
||||
}
|
||||
scope.launch {
|
||||
tokensWithManagers.forEach {
|
||||
when (val result = it.walletManager?.addToken(it.token)) {
|
||||
is Result.Success -> {
|
||||
store.dispatch(
|
||||
store.dispatchOnMain(
|
||||
WalletAction.MultiWallet.TokenLoaded(
|
||||
amount = result.data, token = token
|
||||
amount = result.data, token = it.token
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -177,4 +208,6 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TokenWithManager(val token: Token, val walletManager: WalletManager?)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.reducers
|
|||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.extensions.toFiatString
|
||||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
|
|
@ -18,9 +19,7 @@ class MultiWalletReducer {
|
|||
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
|
||||
return when (action) {
|
||||
is WalletAction.MultiWallet.AddWalletManagers -> {
|
||||
state.copy(
|
||||
walletManagers = state.walletManagers + action.walletManagers
|
||||
)
|
||||
state.addWalletManagers(action.walletManagers)
|
||||
}
|
||||
is WalletAction.MultiWallet.AddBlockchains -> {
|
||||
val wallets = action.blockchains.map { blockchain ->
|
||||
|
|
@ -75,47 +74,12 @@ class MultiWalletReducer {
|
|||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.AddTokens -> {
|
||||
if (!state.isMultiwalletAllowed) return state
|
||||
val wallets = action.tokens.map { token ->
|
||||
val walletManager = state.getWalletManager(token)?.wallet
|
||||
WalletData(
|
||||
currencyData = BalanceWidgetData(
|
||||
BalanceStatus.Loading,
|
||||
currency = token.name,
|
||||
currencySymbol = token.symbol
|
||||
),
|
||||
walletAddresses = createAddressList(walletManager),
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
topUpState = TopUpState(allowed = false),
|
||||
currency = Currency.Token(
|
||||
token = token,
|
||||
blockchain = walletManager?.blockchain ?: Blockchain.Ethereum
|
||||
)
|
||||
)
|
||||
}
|
||||
val wallets = action.tokens.mapNotNull { token -> token.toWallet(state) }
|
||||
state.copy(wallets = state.replaceSomeWallets(wallets))
|
||||
}
|
||||
is WalletAction.MultiWallet.AddToken -> {
|
||||
if (!state.isMultiwalletAllowed) return state
|
||||
val walletManager = state.getWalletManager(action.token)?.wallet
|
||||
val walletAddresses = createAddressList(walletManager)
|
||||
|
||||
val wallet = WalletData(
|
||||
currencyData = BalanceWidgetData(
|
||||
BalanceStatus.Loading,
|
||||
currency = action.token.name,
|
||||
currencySymbol = action.token.symbol
|
||||
),
|
||||
walletAddresses = walletAddresses,
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
topUpState = TopUpState(allowed = false),
|
||||
currency = Currency.Token(
|
||||
token = action.token,
|
||||
blockchain = walletManager?.blockchain ?: Blockchain.Ethereum
|
||||
)
|
||||
)
|
||||
val wallets = state.replaceWalletInWallets(wallet)
|
||||
state.copy(wallets = wallets)
|
||||
val walletData = action.token.toWallet(state) ?: return state
|
||||
state.copy(wallets = state.replaceWalletInWallets(walletData))
|
||||
}
|
||||
is WalletAction.MultiWallet.TokenLoaded -> {
|
||||
val pendingTransactions = state.getWalletManager(action.token)
|
||||
|
|
@ -182,4 +146,29 @@ class MultiWalletReducer {
|
|||
is WalletAction.MultiWallet.SaveCurrencies -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Token.toWallet(state: WalletState): WalletData? {
|
||||
if (!state.isMultiwalletAllowed) return null
|
||||
if (state.currencies.any { it is Currency.Token && it.token == this }) {
|
||||
return null
|
||||
}
|
||||
|
||||
val walletManager = state.getWalletManager(this)?.wallet
|
||||
val walletAddresses = createAddressList(walletManager)
|
||||
|
||||
return WalletData(
|
||||
currencyData = BalanceWidgetData(
|
||||
BalanceStatus.Loading,
|
||||
currency = this.name,
|
||||
currencySymbol = this.symbol
|
||||
),
|
||||
walletAddresses = walletAddresses,
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
topUpState = TopUpState(allowed = false),
|
||||
currency = Currency.Token(
|
||||
token = this,
|
||||
blockchain = walletManager?.blockchain ?: Blockchain.Ethereum
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ package com.tangem.tap.features.wallet.ui
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.Menu.NONE
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.get
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
|
@ -17,6 +15,7 @@ import com.squareup.picasso.Picasso
|
|||
import com.tangem.tangem_sdk_new.extensions.dpToPx
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
|
|
@ -156,6 +155,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.details_menu -> {
|
||||
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
|
||||
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
|
||||
store.dispatch(DetailsAction.PrepareScreen(
|
||||
scanNoteResponse.card, scanNoteResponse,
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@
|
|||
android:id="@+id/tv_signed_hashes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="16sp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_issuer"
|
||||
tools:text="48 hashes" />
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="15dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/disclaimer_title"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
|
|
@ -132,16 +132,16 @@
|
|||
android:id="@+id/tv_card_tou"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="15dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:drawablePadding="15dp"
|
||||
android:text="@string/details_row_title_card_tou"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer"
|
||||
android:text="@string/details_row_title_card_tou" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_settings_title"
|
||||
|
|
@ -179,8 +179,8 @@
|
|||
android:id="@+id/tv_app_currency_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/details_row_title_currency"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
|
|
@ -191,8 +191,8 @@
|
|||
android:id="@+id/tv_send_feedback"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/details_row_title_send_feedback"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
|
|
@ -200,6 +200,26 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_wallet_connect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/wallet_connect"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="tv_wallet_connect,tv_send_feedback" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_card_title"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -211,7 +231,7 @@
|
|||
android:textColor="@color/colorSecondary"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -237,7 +257,7 @@
|
|||
android:id="@+id/tv_security_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/details_row_title_manage_security"
|
||||
android:textColor="@color/darkGray6"
|
||||
|
|
|
|||
92
app/src/main/res/layout/fragment_wallet_connect_sessions.xml
Normal file
92
app/src/main/res/layout/fragment_wallet_connect_sessions.xml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/coordinator_details_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/backgroundLightGray"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
style="@style/Widget.MaterialComponents.Toolbar.Surface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/backgroundLightGray"
|
||||
android:fitsSystemWindows="true"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
|
||||
app:title="@string/wallet_connect_sessions_title" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_details_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/pb_wallet_connect"
|
||||
android:indeterminate="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_no_sessions_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/wallet_connect_no_sessions_title"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="32sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_no_sessions"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_no_sessions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/wallet_connect_no_sessions_message"
|
||||
android:textAlignment="center"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_no_sessions_title" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_wallet_connect_sessions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_open_session"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="33dp"
|
||||
android:contentDescription="@string/wallet_connect_open_session"
|
||||
android:src="@drawable/ic_add"
|
||||
app:backgroundTint="@color/accent"
|
||||
app:borderWidth="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
57
app/src/main/res/layout/item_wallet_connect_session.xml
Normal file
57
app/src/main/res/layout/item_wallet_connect_session.xml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_d_app_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_card_id"
|
||||
app:layout_constraintEnd_toStartOf="@id/btn_disconnect"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="WalletConnect Example fsdljfsdlkjfsdlkjflsdkjflsdjf slfjdskfjskldjflskd" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_card_id"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btn_disconnect"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_d_app_name"
|
||||
tools:text="Card: CB241024012410" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/btn_disconnect"
|
||||
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||
android:layout_width="102dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:src="@drawable/ic_inactive"
|
||||
android:text="@string/common_disconnect"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/white"
|
||||
app:chipBackgroundColor="@color/accent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -101,13 +101,13 @@
|
|||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:ellipsize="middle"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_qr_code"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_qr_code"
|
||||
tools:text="139mrsJgyWnJjkljlkhiuojlkljkjljljlkjlkljlkjkljlkjlkjkljky9BV" />
|
||||
tools:text="139mrsJgyWnJjkljlkhiuojlkljkjljljlkjlkfsdkdsjflsdkjflsdkjfffffffljlkjkljlkjlkjkljky9BV"
|
||||
android:singleLine="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_explore"
|
||||
|
|
|
|||
|
|
@ -113,4 +113,44 @@ this wallet.
|
|||
<string name="alert_button_i_understand" translatable="false">I understand</string>
|
||||
|
||||
<string name="warning_low_signatures_format" translatable="false">There are only %s signatures available on this card. You must withdraw all of your funds.</string>
|
||||
|
||||
//Wallet Connect
|
||||
<string name="wallet_connect" translatable="false">Wallet Connect</string>
|
||||
<string name="wallet_connect_sessions_title" translatable="false">Wallet Connect Sessions</string>
|
||||
<string name="wallet_connect_session_opened" translatable="false">WalletConnect session opened with %s</string>
|
||||
<string name="wallet_connect_no_sessions_title" translatable="false">Ooops. No Sessions.</string>
|
||||
<string name="wallet_connect_no_sessions_message" translatable="false">No opened WalletConnect sessions</string>
|
||||
<string name="wallet_connect_open_session" translatable="false">Open session</string>
|
||||
<string name="wallet_connect_card_number" translatable="false">Card: %s</string>
|
||||
<string name="wallet_connect_scan_card_message" translatable="false">Tap card to bind to wallet connect</string>
|
||||
|
||||
<string name="wallet_connect_create_tx_message" translatable="false">Card: %1s
|
||||
Request to create transaction for %2s
|
||||
\n%3s
|
||||
|
||||
\n\nAmount: %4s
|
||||
\nFee: %5s
|
||||
\nTotal: %6s
|
||||
\nBalance: %7s</string>
|
||||
|
||||
<string name="wallet_connect_create_tx_not_enough_funds" translatable="false">Can\'t send transaction. Not enough funds.</string>
|
||||
|
||||
<string name="wallet_connect_request_session_start" translatable="false">Request to start a session for card with ID %1s\n
|
||||
for %2s\n\n
|
||||
|
||||
URL: %3s</string>
|
||||
|
||||
|
||||
<string name="wallet_connect_alert_sign_message" translatable="false">Requesting to sign a message\nwith card %s\n\n</string>
|
||||
<string name="wallet_connect_personal_sign_message" translatable="false">Message for %s:\n%s</string>
|
||||
|
||||
|
||||
<string name="common_disconnect" translatable="false">Disconnect</string>
|
||||
<string name="common_sign" translatable="false">Sign</string>
|
||||
<string name="common_sign_and_send" translatable="false">Sign and send</string>
|
||||
<string name="common_reject" translatable="false">Reject</string>
|
||||
|
||||
<string name="wallet_connect_clipboard_alert" translatable="false">Clipboard contain WalletConnect code. Use copied value or scan QR-code</string>
|
||||
<string name="wallet_connect_paste_from_clipboard" translatable="false">Paste from clipboard</string>
|
||||
<string name="wallet_connect_scan_new_code" translatable="false">Scan new code</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
ext.versions = [
|
||||
kotlin : '1.5.0',
|
||||
kotlin : '1.5.10',
|
||||
build_gradle: '4.2.1',
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue