Updated on 2026-08-14
This commit is contained in:
commit
23d9050035
340 changed files with 4881 additions and 7536 deletions
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
|
|
@ -205,4 +205,4 @@
|
|||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,17 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
//conflict of dependencies when adding WalletConnectV2.0 library
|
||||
configurations {
|
||||
all {
|
||||
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
|
||||
resolutionStrategy {
|
||||
force("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
}
|
||||
exclude(group = "com.github.komputing.kethereum")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(files("libs/walletconnect-1.5.6.aar"))
|
||||
implementation(project(":domain:legacy"))
|
||||
|
|
@ -42,6 +53,7 @@ dependencies {
|
|||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.androidx.core.splashScreen)
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.constraintLayout)
|
||||
|
|
@ -123,6 +135,8 @@ dependencies {
|
|||
implementation(deps.armadillo)
|
||||
implementation(deps.mviCore.watcher)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.walletConnectCore)
|
||||
implementation(deps.walletConnectWeb3)
|
||||
|
||||
/** Testing libraries */
|
||||
testImplementation(deps.test.junit)
|
||||
|
|
|
|||
|
|
@ -27,17 +27,18 @@
|
|||
</queries>
|
||||
|
||||
<application
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:label="@string/tangem_app_name"
|
||||
android:name="com.tangem.tap.TapApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:hardwareAccelerated="true"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:allowBackup">
|
||||
tools:replace="android:allowBackup, android:fullBackupContent">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
@ -58,40 +60,25 @@
|
|||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data
|
||||
android:pathPrefix="/ndef"
|
||||
android:host="www.tangem.com"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:pathPrefix="/ndef"
|
||||
android:host="www.tangem.com"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPrefix="/ndef"
|
||||
android:host="tangem.com"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:pathPrefix="/ndef"
|
||||
android:host="tangem.com"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPrefix="/ndef"
|
||||
android:host="app.tangem.com"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<data android:scheme="http" />
|
||||
<data android:host="www.tangem.com" />
|
||||
<data android:pathPrefix="/ndef" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.TECH_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<data android:scheme="https" />
|
||||
<data android:host="www.tangem.com" />
|
||||
<data android:pathPrefix="/ndef" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />
|
||||
<data android:scheme="http" />
|
||||
<data android:host="tangem.com" />
|
||||
<data android:pathPrefix="/ndef" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.TAG_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="tangem.com" />
|
||||
<data android:pathPrefix="/ndef" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="app.tangem.com" />
|
||||
<data android:pathPrefix="/ndef" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c714678d9c46dee1b15ef9e8d27aa23afa1dfd8d
|
||||
Subproject commit fad890b2a0b552be60d124949ca0a3a4d672dac1
|
||||
|
|
@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
|
|
@ -31,6 +32,7 @@ import com.tangem.tap.domain.TangemSdkManager
|
|||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
|
||||
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
|
||||
import com.tangem.tap.features.shop.redux.ShopAction
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
||||
|
|
@ -90,6 +92,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var walletRouter: WalletRouter
|
||||
|
||||
@Inject
|
||||
lateinit var walletConnectInteractor: WalletConnectInteractor
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
private val dialogManager = DialogManager()
|
||||
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
|
||||
|
|
@ -97,6 +102,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
systemActions()
|
||||
|
|
@ -116,12 +123,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
GooglePayService(createPaymentsClient(this), this),
|
||||
),
|
||||
)
|
||||
|
||||
store.dispatch(
|
||||
DaggerGraphAction.SetActivityDependencies(
|
||||
testerRouter = testerRouter,
|
||||
scanCardUseCase = scanCardUseCase,
|
||||
walletRouter = walletRouter,
|
||||
walletConnectInteractor = walletConnectInteractor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,60 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.tangem.Log
|
||||
import com.tangem.LogFormat
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.config.FeaturesLocalLoader
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import com.tangem.tap.common.IntentHandler
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.topup.TopUpController
|
||||
import com.tangem.tap.common.chat.ChatManager
|
||||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.shop.TangemShopService
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
|
||||
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.*
|
||||
import coil.*
|
||||
import com.tangem.*
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.network.*
|
||||
import com.tangem.core.analytics.*
|
||||
import com.tangem.core.featuretoggle.manager.*
|
||||
import com.tangem.data.source.preferences.*
|
||||
import com.tangem.datasource.api.common.*
|
||||
import com.tangem.datasource.asset.*
|
||||
import com.tangem.datasource.config.*
|
||||
import com.tangem.datasource.config.models.*
|
||||
import com.tangem.datasource.connection.*
|
||||
import com.tangem.domain.*
|
||||
import com.tangem.domain.common.*
|
||||
import com.tangem.features.wallet.featuretoggles.*
|
||||
import com.tangem.tap.common.*
|
||||
import com.tangem.tap.common.analytics.*
|
||||
import com.tangem.tap.common.analytics.api.*
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.*
|
||||
import com.tangem.tap.common.analytics.handlers.appsFlyer.*
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.*
|
||||
import com.tangem.tap.common.analytics.topup.*
|
||||
import com.tangem.tap.common.chat.*
|
||||
import com.tangem.tap.common.feedback.*
|
||||
import com.tangem.tap.common.images.*
|
||||
import com.tangem.tap.common.log.*
|
||||
import com.tangem.tap.common.redux.*
|
||||
import com.tangem.tap.common.redux.global.*
|
||||
import com.tangem.tap.common.shop.*
|
||||
import com.tangem.tap.domain.configurable.warningMessage.*
|
||||
import com.tangem.tap.domain.tokens.*
|
||||
import com.tangem.tap.domain.totalBalance.*
|
||||
import com.tangem.tap.domain.totalBalance.di.*
|
||||
import com.tangem.tap.domain.walletCurrencies.*
|
||||
import com.tangem.tap.domain.walletCurrencies.di.*
|
||||
import com.tangem.tap.domain.walletStores.*
|
||||
import com.tangem.tap.domain.walletStores.di.*
|
||||
import com.tangem.tap.domain.walletStores.repository.*
|
||||
import com.tangem.tap.domain.walletStores.repository.di.*
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.domain.walletconnect2.domain.*
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.*
|
||||
import com.tangem.tap.proxy.*
|
||||
import com.tangem.tap.proxy.redux.*
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import dagger.hilt.android.*
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.logging.*
|
||||
import org.rekotlin.*
|
||||
import timber.log.*
|
||||
import javax.inject.*
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository
|
||||
|
||||
lateinit var store: Store<AppState>
|
||||
|
||||
|
|
@ -137,6 +131,12 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var walletFeatureToggles: WalletFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var walletConnect2Repository: WalletConnect2Repository
|
||||
|
||||
@Inject
|
||||
lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
@ -151,6 +151,8 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
networkConnectionManager = networkConnectionManager,
|
||||
customTokenFeatureToggles = customTokenFeatureToggles,
|
||||
walletFeatureToggles = walletFeatureToggles,
|
||||
walletConnectRepository = walletConnect2Repository,
|
||||
walletConnectSessionsRepository = walletConnectSessionsRepository,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -193,6 +195,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
}
|
||||
|
||||
initTopUpController()
|
||||
walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId)
|
||||
}
|
||||
|
||||
private fun initTopUpController() {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
|
|||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.*
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.*
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.features.wallet.ui.dialogs.*
|
||||
|
|
@ -94,14 +93,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context)
|
||||
is WalletConnectDialog.ClipboardOrScanQr ->
|
||||
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
|
||||
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.dialogData, context)
|
||||
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context)
|
||||
is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context)
|
||||
is WalletConnectDialog.BnbTransactionDialog ->
|
||||
BnbTransactionDialog.create(
|
||||
data = state.dialog.data,
|
||||
session = state.dialog.session,
|
||||
sessionId = state.dialog.sessionId,
|
||||
dAppName = state.dialog.dAppName,
|
||||
preparedData = state.dialog.data,
|
||||
context = context,
|
||||
)
|
||||
is WalletConnectDialog.UnsupportedNetwork ->
|
||||
|
|
@ -110,6 +106,15 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
|
||||
context = context,
|
||||
)
|
||||
is WalletConnectDialog.SessionProposalDialog -> {
|
||||
SessionProposalDialog.create(
|
||||
sessionProposal = state.dialog.sessionProposal,
|
||||
networks = state.dialog.networks,
|
||||
context = context,
|
||||
onApprove = state.dialog.onApprove,
|
||||
onReject = state.dialog.onReject,
|
||||
)
|
||||
}
|
||||
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
|
||||
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
|
||||
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
|
||||
|
|
@ -118,9 +123,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
context = context,
|
||||
cardId = state.dialog.cardId,
|
||||
)
|
||||
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
|
||||
is SaltPayDialog.Activation.PutVisaCard -> PutVisaCardDialog.create(context)
|
||||
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)
|
||||
is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context)
|
||||
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context, state.dialog)
|
||||
is WalletDialog.SelectAmountToSendDialog -> AmountToSendBottomSheetDialog(context, state.dialog)
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.tap.common.extensions.readFile
|
||||
import com.tangem.tap.common.extensions.rewriteFile
|
||||
|
||||
interface FileReader {
|
||||
fun readFile(fileName: String): String
|
||||
fun rewriteFile(content: String, fileName: String)
|
||||
}
|
||||
|
||||
class AndroidFileReader(private val context: Context) : FileReader {
|
||||
override fun readFile(fileName: String): String {
|
||||
return context.readFile(fileName)
|
||||
}
|
||||
|
||||
override fun rewriteFile(content: String, fileName: String) {
|
||||
context.rewriteFile(content, fileName)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.common.analytics.converters
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.Converter
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
||||
/**
|
||||
|
|
@ -17,7 +16,6 @@ class ParamCardCurrencyConverter : Converter<CardTypesResolver, AnalyticsParam.C
|
|||
val type = when {
|
||||
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
|
||||
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
|
||||
value.isSaltPay() -> AnalyticsParam.CurrencyType.Token(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay))
|
||||
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
|
||||
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
|
||||
else -> null
|
||||
|
|
|
|||
|
|
@ -87,12 +87,54 @@ sealed class AnalyticsParam {
|
|||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
object Send : TxSentFrom("Send")
|
||||
object Swap : TxSentFrom("Swap")
|
||||
data class Send(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Send"), TxData
|
||||
|
||||
data class Swap(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Swap"), TxData
|
||||
|
||||
data class Approve(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
val permissionType: String,
|
||||
) : TxSentFrom("Approve"), TxData
|
||||
|
||||
object WalletConnect : TxSentFrom("WalletConnect")
|
||||
object Sell : TxSentFrom("Sell")
|
||||
}
|
||||
|
||||
sealed interface TxData {
|
||||
val blockchain: String
|
||||
val token: String
|
||||
val feeType: FeeType
|
||||
}
|
||||
|
||||
sealed class FeeType(val value: String) {
|
||||
object Fixed : FeeType("Fixed")
|
||||
object Min : FeeType("Min")
|
||||
object Normal : FeeType("Normal")
|
||||
object Max : FeeType("Max")
|
||||
|
||||
companion object {
|
||||
fun fromString(feeType: String): FeeType {
|
||||
return when (feeType) {
|
||||
Min.value -> Min
|
||||
Normal.value -> Normal
|
||||
Max.value -> Max
|
||||
Fixed.value -> Fixed
|
||||
else -> Fixed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class WalletCreationType(val value: String) {
|
||||
object PrivateKey : WalletCreationType("Private key")
|
||||
object NewSeed : WalletCreationType("New seed")
|
||||
|
|
@ -100,9 +142,13 @@ sealed class AnalyticsParam {
|
|||
}
|
||||
|
||||
companion object Key {
|
||||
const val BLOCKCHAIN = "blockchain"
|
||||
const val TOKEN = "Token"
|
||||
const val SOURCE = "Source"
|
||||
const val BALANCE = "Balance"
|
||||
const val BATCH = "Batch"
|
||||
const val FEE_TYPE = "Fee Type"
|
||||
const val PERMISSION_TYPE = "Permission Type"
|
||||
const val PRODUCT_TYPE = "Product Type"
|
||||
const val FIRMWARE = "Firmware"
|
||||
const val CURRENCY = "Currency"
|
||||
|
|
|
|||
|
|
@ -49,10 +49,25 @@ sealed class Basic(
|
|||
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
|
||||
)
|
||||
|
||||
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom) : Basic(
|
||||
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) : Basic(
|
||||
event = "Transaction sent",
|
||||
params = mapOf(AnalyticsParam.SOURCE to sentFrom.value),
|
||||
)
|
||||
params = buildMap {
|
||||
this[AnalyticsParam.SOURCE] = sentFrom.value
|
||||
if (sentFrom is AnalyticsParam.TxData) {
|
||||
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
|
||||
this[AnalyticsParam.TOKEN] = sentFrom.token
|
||||
this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value
|
||||
}
|
||||
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
|
||||
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType
|
||||
}
|
||||
this["Memo"] = memoType.name
|
||||
},
|
||||
) {
|
||||
enum class MemoType {
|
||||
Empty, Full, Null
|
||||
}
|
||||
}
|
||||
|
||||
class ScanError(error: Throwable) : Basic(
|
||||
event = "Scan",
|
||||
|
|
|
|||
|
|
@ -73,16 +73,36 @@ sealed class Token(
|
|||
error: Throwable? = null,
|
||||
) : Token("Token / Send", event, params, error) {
|
||||
|
||||
class ScreenOpened : Send("Send Screen Opened")
|
||||
class ButtonPaste : Send("Button - Paste")
|
||||
class ButtonQRCode : Send("Button - QR Code")
|
||||
class ButtonSwapCurrency : Send("Button - Swap Currency")
|
||||
class ScreenOpened : Send(event = "Send Screen Opened")
|
||||
class ButtonPaste : Send(event = "Button - Paste")
|
||||
class ButtonQRCode : Send(event = "Button - QR Code")
|
||||
class ButtonSwapCurrency : Send(event = "Button - Swap Currency")
|
||||
|
||||
class TransactionSent(type: CurrencyType, error: Throwable? = null) : Send(
|
||||
event = "Transaction Sent",
|
||||
params = mapOf("Token" to type.value),
|
||||
error = error,
|
||||
)
|
||||
class AddressEntered(sourceType: SourceType, validationResult: ValidationResult) : Send(
|
||||
event = "Address Entered",
|
||||
params = mapOf(
|
||||
"Source" to sourceType.name,
|
||||
"Validation" to validationResult.name,
|
||||
),
|
||||
) {
|
||||
enum class SourceType {
|
||||
QRCode, PasteButton, PastePopup
|
||||
}
|
||||
|
||||
enum class ValidationResult {
|
||||
Success, Fail
|
||||
}
|
||||
}
|
||||
|
||||
class SelectedCurrency(currency: CurrencyType) : Send(
|
||||
event = "Selected Currency",
|
||||
params = mapOf("Type" to currency.value),
|
||||
) {
|
||||
|
||||
enum class CurrencyType(val value: String) {
|
||||
Token(value = "Token"), AppCurrency(value = "App Currency")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Topup(
|
||||
|
|
|
|||
|
|
@ -45,11 +45,6 @@ class CardContextInterceptor(
|
|||
ProductType.Note -> "Note"
|
||||
ProductType.Twins -> "Twin"
|
||||
ProductType.Wallet -> "Wallet"
|
||||
ProductType.SaltPay -> if (scanResponse.cardTypesResolver.isSaltPayVisa()) {
|
||||
"Visa"
|
||||
} else {
|
||||
"Visa Backup"
|
||||
}
|
||||
ProductType.Start2Coin -> "Start2Coin"
|
||||
else -> if (DemoHelper.isDemoCard(scanResponse)) {
|
||||
if (DemoHelper.isTestDemoCard(scanResponse)) {
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.operations.backup.BackupService
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun BackupService.primaryCardIsSaltPayVisa(): Boolean {
|
||||
return primaryCardId?.slice(0..3)?.let(SaltPayWorkaround::isVisaBatchId) ?: false
|
||||
}
|
||||
|
|
@ -1,20 +1,11 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AnyRes
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
fun Context.readFile(fileName: String): String = this.openFileInput(fileName).bufferedReader().readText()
|
||||
|
||||
fun Context.rewriteFile(content: String, fileName: String) {
|
||||
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
|
||||
it.write(content.toByteArray(), 0, content.length)
|
||||
}
|
||||
}
|
||||
import android.content.*
|
||||
import android.content.pm.*
|
||||
import android.content.res.*
|
||||
import android.net.*
|
||||
import androidx.annotation.*
|
||||
import androidx.core.content.*
|
||||
|
||||
fun Context.isPermissionGranted(permission: String): Boolean {
|
||||
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||
|
|
|
|||
|
|
@ -1,32 +1,10 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.widget.ImageView
|
||||
import coil.ImageLoader
|
||||
import coil.imageLoader
|
||||
import coil.load
|
||||
import coil.request.Disposable
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun ImageRequest.Builder.cardImageData(any: Any?): ImageRequest.Builder = apply {
|
||||
data(any?.parseData())
|
||||
}
|
||||
|
||||
fun ImageView.loadCardImage(
|
||||
data: Any?,
|
||||
imageLoader: ImageLoader = context.imageLoader,
|
||||
builder: ImageRequest.Builder.() -> Unit = {},
|
||||
): Disposable = this.load(
|
||||
data = data?.parseData(),
|
||||
imageLoader = imageLoader,
|
||||
builder = builder,
|
||||
)
|
||||
|
||||
private fun Any?.parseData(): Any? = when {
|
||||
this is String && this == Artwork.SALT_PAY_URL -> R.drawable.img_salt_pay_visa
|
||||
else -> this
|
||||
data(any)
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@ package com.tangem.tap.common.extensions
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionHistoryProvider
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.services.Result
|
||||
|
|
@ -38,9 +35,6 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
Result.Success(wallet)
|
||||
} else {
|
||||
update()
|
||||
if (wallet.blockchain == Blockchain.SaltPay && this is TransactionHistoryProvider) {
|
||||
this.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens())
|
||||
}
|
||||
Result.Success(wallet)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
|
|
@ -87,26 +81,10 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
|
|||
return if (addressDataList.isEmpty()) null else addressDataList[0]
|
||||
}
|
||||
|
||||
fun WalletManager.getBlockchainTxHistory(): List<TransactionData> = wallet.getBlockchainTxHistory()
|
||||
|
||||
fun WalletManager.getTokenTxHistory(token: Token): List<TransactionData> = wallet.getTokenTxHistory(token)
|
||||
|
||||
fun <T> WalletManager.Companion.stub(): T {
|
||||
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf())
|
||||
return object : WalletManager(wallet) {
|
||||
override val currentHost: String = ""
|
||||
override suspend fun update() {}
|
||||
} as T
|
||||
}
|
||||
|
||||
fun Wallet.getBlockchainTxHistory(): List<TransactionData> {
|
||||
return historyTransactions.filter {
|
||||
it.contractAddress.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun Wallet.getTokenTxHistory(token: Token): List<TransactionData> {
|
||||
return historyTransactions.filter {
|
||||
it.contractAddress == token.contractAddress
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
|||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
|
||||
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
|
||||
|
|
@ -94,7 +93,6 @@ data class AppState(
|
|||
HomeMiddleware.handler,
|
||||
OnboardingNoteMiddleware.handler,
|
||||
OnboardingWalletMiddleware.handler,
|
||||
OnboardingSaltPayMiddleware.handler,
|
||||
OnboardingOtherCardsMiddleware.handler,
|
||||
WalletMiddleware().walletMiddleware,
|
||||
TwinCardsMiddleware.handler,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ sealed class AppDialog : StateDialog {
|
|||
val onOk: VoidCallback? = null,
|
||||
) : AppDialog()
|
||||
|
||||
data class OkCancelDialogRes(
|
||||
val headerId: Int,
|
||||
val messageId: Int,
|
||||
val okButton: DialogButton,
|
||||
val cancelButton: DialogButton,
|
||||
) : AppDialog()
|
||||
|
||||
data class DialogButton(
|
||||
val title: Int,
|
||||
val action: VoidCallback? = null,
|
||||
)
|
||||
|
||||
object ScanFailsDialog : AppDialog()
|
||||
|
||||
data class AddressInfoDialog(
|
||||
|
|
|
|||
|
|
@ -38,19 +38,16 @@ sealed class GlobalAction : Action {
|
|||
sealed class Onboarding : GlobalAction() {
|
||||
/**
|
||||
* Initiate an onboarding process.
|
||||
* For resuming unfinished backup of standard Wallet and SaltPay cards see
|
||||
* For resuming unfinished backup of standard Wallet see
|
||||
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
|
||||
*/
|
||||
data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding()
|
||||
|
||||
/**
|
||||
* Initiate resuming of unfinished backup for standard Wallet and SaltPay cards.
|
||||
* Initiate resuming of unfinished backup for standard Wallet.
|
||||
* See more BackupAction.CheckForUnfinishedBackup
|
||||
*/
|
||||
data class StartForUnfinishedBackup(
|
||||
val addedBackupCardsCount: Int,
|
||||
val isSaltPayVisa: Boolean,
|
||||
) : Onboarding()
|
||||
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
|
||||
|
||||
object Stop : Onboarding()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import com.tangem.common.extensions.guard
|
|||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
|
|
@ -26,16 +24,12 @@ import com.tangem.tap.network.exchangeServices.ExchangeService
|
|||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment
|
||||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
|
||||
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
|
||||
import com.tangem.tap.network.exchangeServices.utorg.UtorgAuthProvider
|
||||
import com.tangem.tap.network.exchangeServices.utorg.UtorgEnvironment
|
||||
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import com.tangem.tap.walletCurrenciesManager
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
|
@ -109,13 +103,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
return
|
||||
}
|
||||
|
||||
val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse
|
||||
|
||||
// if config not set -> try to get it based on a scanResponse.productType
|
||||
val unsafeChatConfig = action.chatConfig ?: when {
|
||||
scanResponse?.cardTypesResolver?.isSaltPay() == true -> config.saltPayConfig?.sprinklr
|
||||
else -> config.zendesk
|
||||
}
|
||||
val unsafeChatConfig = action.chatConfig ?: config.zendesk
|
||||
|
||||
val chatConfig = unsafeChatConfig.guard {
|
||||
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
|
||||
|
|
@ -139,11 +128,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
store.state.globalState.scanResponse
|
||||
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
|
||||
}
|
||||
val productTypeProvider: () -> ProductType? = { scanResponseProvider.invoke()?.productType }
|
||||
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
|
||||
|
||||
val exchangeManager = CurrencyExchangeManager(
|
||||
buyService = makeBuyExchangeService(config, productTypeProvider),
|
||||
buyService = makeBuyExchangeService(config),
|
||||
sellService = makeSellExchangeService(config),
|
||||
primaryRules = CardExchangeRules(cardProvider),
|
||||
)
|
||||
|
|
@ -213,28 +201,13 @@ private fun makeSellExchangeService(config: Config): ExchangeService {
|
|||
)
|
||||
}
|
||||
|
||||
private fun makeBuyExchangeService(config: Config, productTypeProvider: () -> ProductType?): ExchangeService {
|
||||
private fun makeBuyExchangeService(config: Config): ExchangeService {
|
||||
return BuyExchangeService(
|
||||
productTypeProvider = productTypeProvider,
|
||||
mercuryoService = makeMercuryoExchangeService(config),
|
||||
utorgService = makeUtorgExchangeService(config),
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeMercuryoExchangeService(config: Config): MercuryoService {
|
||||
val mercuryoEnvironment = MercuryoEnvironment.prod(config.mercuryoWidgetId, config.mercuryoSecret)
|
||||
return MercuryoService(mercuryoEnvironment)
|
||||
}
|
||||
|
||||
private fun makeUtorgExchangeService(config: Config): UtorgExchangeService {
|
||||
val saltPayConfig = requireNotNull(config.saltPayConfig)
|
||||
|
||||
val utorgAuthProvider = UtorgAuthProvider(saltPayConfig.kycProvider.sidValue)
|
||||
val utorgEnvironment = if (BuildConfig.DEBUG) {
|
||||
UtorgEnvironment.stage(utorgAuthProvider, LogConfig.network.utorgService)
|
||||
// UtorgEnvironment.mock()
|
||||
} else {
|
||||
UtorgEnvironment.prod(utorgAuthProvider)
|
||||
}
|
||||
return UtorgExchangeService(utorgEnvironment)
|
||||
}
|
||||
|
|
@ -229,7 +229,6 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
tangemSdk.config.cardIdDisplayFormat = when {
|
||||
scanResponse == null -> CardIdDisplayFormat.Full
|
||||
scanResponse.cardTypesResolver.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4)
|
||||
scanResponse.cardTypesResolver.isSaltPay() -> CardIdDisplayFormat.None
|
||||
else -> CardIdDisplayFormat.Full
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ sealed class TapError(
|
|||
|
||||
object UnknownError : TapError(R.string.send_error_unknown)
|
||||
open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
|
||||
data class UnsupportedState(
|
||||
val stateError: String,
|
||||
) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError"))
|
||||
|
||||
object ScanCardError : TapError(R.string.scan_card_error)
|
||||
object UnknownBlockchain : TapError(R.string.wallet_error_unsupported_blockchain_subtitle)
|
||||
object NoInternetConnection : TapError(R.string.wallet_notification_no_internet)
|
||||
|
|
@ -38,11 +42,6 @@ sealed class TapError(
|
|||
data class DustAmount(override val args: List<Any>) : TapError(R.string.send_error_dust_amount_format)
|
||||
object DustChange : TapError(R.string.send_error_dust_change)
|
||||
|
||||
data class UnsupportedState(
|
||||
val stateError: String,
|
||||
val customMessage: String = "Unsupported state:",
|
||||
) : TapError(R.string.common_custom_string, listOf("$customMessage $stateError"))
|
||||
|
||||
sealed class WalletManager {
|
||||
object CreationError : CustomError("Can't create wallet manager")
|
||||
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
|
@ -13,6 +9,7 @@ import com.tangem.domain.common.extensions.withMainContext
|
|||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
|
|
@ -25,10 +22,6 @@ import com.tangem.tap.features.disclaimer.createDisclaimer
|
|||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -85,10 +78,24 @@ class TapWalletManager(
|
|||
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
|
||||
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
|
||||
}
|
||||
|
||||
setupWalletConnectV2(userWallet)
|
||||
loadData(userWallet, refresh)
|
||||
}
|
||||
|
||||
private fun setupWalletConnectV2(userWallet: UserWallet) {
|
||||
val cardId = if (userWallet.cardsInWallet.size == 1) {
|
||||
userWallet.cardId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
scope.launch {
|
||||
store.state.daggerGraphState.walletConnectInteractor?.startListening(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
cardId = cardId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
|
||||
walletStoresManager.fetch(userWallet, refresh)
|
||||
.doOnSuccess {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.common.card.FirmwareVersion
|
|||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.getTwinCardNumber
|
||||
import com.tangem.operations.attestation.CardVerifyAndGetInfo
|
||||
|
|
@ -21,7 +20,7 @@ val CardDTO.isFirmwareMultiwalletAllowed: Boolean
|
|||
get() = firmwareVersion >= FirmwareVersion.MultiWalletAvailable && settings.maxWalletsCount > 1
|
||||
|
||||
val CardDTO.isHdWalletAllowedByApp: Boolean
|
||||
get() = settings.isHDWalletAllowed && !isSaltPay
|
||||
get() = settings.isHDWalletAllowed
|
||||
|
||||
@Suppress("UnnecessaryParentheses")
|
||||
fun CardDTO.hasSignedHashes(): Boolean {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.domain.model
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.tap.domain.model.WalletDataModel.AddressData
|
||||
import com.tangem.tap.domain.model.WalletDataModel.Status
|
||||
|
|
@ -28,7 +27,6 @@ data class WalletDataModel(
|
|||
val fiatRate: BigDecimal?,
|
||||
val isCardSingleToken: Boolean,
|
||||
val isCustom: Boolean,
|
||||
val historyTransactions: List<TransactionData>?,
|
||||
) {
|
||||
|
||||
data class WalletAddresses(
|
||||
|
|
@ -79,6 +77,7 @@ data class WalletDataModel(
|
|||
|
||||
data class Unreachable(
|
||||
override val errorMessage: String?,
|
||||
override val amount: BigDecimal = BigDecimal.ZERO,
|
||||
) : Status() {
|
||||
override val isErrorStatus: Boolean = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ class UserWalletBuilder(
|
|||
get() = when (productType) {
|
||||
ProductType.Note -> "Note"
|
||||
ProductType.Twins -> "Twin"
|
||||
ProductType.SaltPay -> "SaltPay"
|
||||
ProductType.Start2Coin -> "Start2Coin"
|
||||
ProductType.Wallet -> when {
|
||||
card.isBackupNotAllowed -> "Tangem card"
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ class UserWalletIdBuilder private constructor(
|
|||
ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
|
||||
ProductType.Note,
|
||||
ProductType.Wallet,
|
||||
ProductType.SaltPay,
|
||||
ProductType.Start2Coin,
|
||||
-> null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import com.tangem.blockchain.common.WalletManager
|
|||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.tap.common.extensions.getBlockchainTxHistory
|
||||
import com.tangem.tap.common.extensions.getTokenTxHistory
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
|
|
@ -114,7 +112,6 @@ private fun BlockchainNetwork.getBlockchainWalletData(
|
|||
fiatRate = null,
|
||||
isCardSingleToken = false,
|
||||
isCustom = currency.isCustomCurrency(cardDerivationStyle),
|
||||
historyTransactions = walletManager?.getBlockchainTxHistory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +135,6 @@ private fun BlockchainNetwork.getTokensWalletsData(
|
|||
fiatRate = null,
|
||||
isCardSingleToken = token == primaryToken,
|
||||
isCustom = currency.isCustomCurrency(cardDerivationStyle),
|
||||
historyTransactions = walletManager?.getTokenTxHistory(token),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +152,6 @@ private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): Wal
|
|||
fiatRate = null,
|
||||
isCardSingleToken = false,
|
||||
isCustom = false,
|
||||
historyTransactions = walletManager.getBlockchainTxHistory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +169,6 @@ private fun Token.toTokenWalletData(walletManager: WalletManager, primaryToken:
|
|||
fiatRate = null,
|
||||
isCardSingleToken = this == primaryToken,
|
||||
isCustom = false,
|
||||
historyTransactions = walletManager.getTokenTxHistory(this),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import com.tangem.common.module.ModuleMessage
|
|||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
import com.tangem.tap.domain.moduleMessage.domain.DomainMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayMessageConverter
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage
|
||||
|
||||
class ModuleMessageConverter(
|
||||
private val context: Context,
|
||||
|
|
@ -15,7 +13,6 @@ class ModuleMessageConverter(
|
|||
override fun convert(message: ModuleMessage): ConvertedMessage {
|
||||
val convertedMessage = when (message) {
|
||||
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
|
||||
is SaltPayModuleMessage -> SaltPayMessageConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
return convertedMessage ?: convertUnknownMessage(message)
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.tap.domain.moduleMessage.saltPay
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedMessage
|
||||
import com.tangem.tap.domain.moduleMessage.saltPay.converter.SaltPayActivationErrorConverter
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SaltPayMessageConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<SaltPayModuleMessage, ConvertedMessage?> {
|
||||
|
||||
override fun convert(message: SaltPayModuleMessage): ConvertedMessage? {
|
||||
return when (message) {
|
||||
is SaltPayModuleError -> SaltPayErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SaltPayErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<SaltPayModuleError, ConvertedMessage?> {
|
||||
|
||||
override fun convert(message: SaltPayModuleError): ConvertedMessage? = when (message) {
|
||||
is SaltPayActivationError -> SaltPayActivationErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package com.tangem.tap.domain.moduleMessage.saltPay.converter
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SaltPayActivationErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<SaltPayActivationError, ConvertedDialogMessage?> {
|
||||
|
||||
override fun convert(message: SaltPayActivationError): ConvertedDialogMessage? {
|
||||
val saltPayError = message as? SaltPayActivationError ?: throw UnsupportedOperationException()
|
||||
|
||||
val dialogMessage = when (saltPayError) {
|
||||
SaltPayActivationError.NoGas -> {
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.saltpay_error_no_gas_title),
|
||||
message = context.getString(R.string.saltpay_error_no_gas_message),
|
||||
)
|
||||
}
|
||||
SaltPayActivationError.EmptyBackupCardScanned -> {
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.saltpay_error_empty_backup_title),
|
||||
message = context.getString(R.string.saltpay_error_empty_backup_message),
|
||||
)
|
||||
}
|
||||
SaltPayActivationError.WeakPin -> {
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.saltpay_error_pin_weak_title),
|
||||
message = context.getString(R.string.saltpay_error_pin_weak_message),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val errorMessage = if (saltPayError.message == SaltPayActivationError.EMPTY_MESSAGE) {
|
||||
saltPayError::class.java.simpleName
|
||||
} else {
|
||||
saltPayError.message
|
||||
}
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.common_error),
|
||||
message = errorMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return dialogMessage
|
||||
}
|
||||
}
|
||||
|
|
@ -5,20 +5,14 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.twinsIsTwinned
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||
import com.tangem.tap.common.extensions.addContext
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -26,17 +20,11 @@ import com.tangem.tap.features.disclaimer.createDisclaimer
|
|||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
|
||||
internal object LegacyScanProcessor {
|
||||
|
||||
|
|
@ -85,24 +73,16 @@ internal object LegacyScanProcessor {
|
|||
onScanStateChange(false)
|
||||
sendAnalytics(analyticsEvent, scanResponse)
|
||||
|
||||
checkForUnfinishedBackupForSaltPay(
|
||||
backupService = backupService,
|
||||
showDisclaimerIfNeed(
|
||||
scanResponse = scanResponse,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onFailure = onFailure,
|
||||
nextHandler = { scanResponse1 ->
|
||||
showDisclaimerIfNeed(
|
||||
scanResponse = scanResponse1,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onFailure = onFailure,
|
||||
nextHandler = { scanResponse2 ->
|
||||
onScanSuccess(
|
||||
scanResponse = scanResponse2,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onSuccess = onSuccess,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
onFailure = onFailure,
|
||||
)
|
||||
},
|
||||
nextHandler = { scanResponse2 ->
|
||||
onScanSuccess(
|
||||
scanResponse = scanResponse2,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onSuccess = onSuccess,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -121,35 +101,6 @@ internal object LegacyScanProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
|
||||
* see BackupAction.CheckForUnfinishedBackup
|
||||
* If user touches card other than Visa SaltPay - show dialog and block next processing
|
||||
*/
|
||||
private suspend inline fun checkForUnfinishedBackupForSaltPay(
|
||||
backupService: BackupService,
|
||||
scanResponse: ScanResponse,
|
||||
nextHandler: (ScanResponse) -> Unit,
|
||||
onFailure: suspend (error: TangemError) -> Unit,
|
||||
) {
|
||||
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
|
||||
nextHandler(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
val isTheSamePrimaryCard = backupService.primaryCardId
|
||||
?.let { it == scanResponse.card.cardId }
|
||||
?: false
|
||||
|
||||
if (scanResponse.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) {
|
||||
val error = SaltPayActivationError.PutVisaCard
|
||||
SaltPayExceptionHandler.handle(error)
|
||||
onFailure(TangemSdkError.ExceptionError(error))
|
||||
} else {
|
||||
nextHandler(scanResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun showDisclaimerIfNeed(
|
||||
scanResponse: ScanResponse,
|
||||
crossinline disclaimerWillShow: () -> Unit = {},
|
||||
|
|
@ -165,7 +116,7 @@ internal object LegacyScanProcessor {
|
|||
scope.launch {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
disclaimerWillShow()
|
||||
arrayOf<Action>(
|
||||
store.dispatchWithMain(
|
||||
DisclaimerAction.Show(
|
||||
fromScreen = AppScreen.Home,
|
||||
callback = DisclaimerCallback(
|
||||
|
|
@ -192,7 +143,6 @@ internal object LegacyScanProcessor {
|
|||
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
crossinline onWalletNotCreated: suspend () -> Unit,
|
||||
crossinline onSuccess: suspend (ScanResponse) -> Unit,
|
||||
crossinline onFailure: suspend (error: TangemError) -> Unit,
|
||||
) {
|
||||
val globalState = store.state.globalState
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
|
|
@ -200,66 +150,21 @@ internal object LegacyScanProcessor {
|
|||
|
||||
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
|
||||
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||
if (cardTypesResolver.isSaltPay()) {
|
||||
if (cardTypesResolver.isSaltPayVisa()) {
|
||||
val manager = SaltPayActivationManagerFactory(
|
||||
blockchain = scanResponse.cardTypesResolver.getBlockchain(),
|
||||
card = scanResponse.card,
|
||||
).create()
|
||||
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
|
||||
delay(500)
|
||||
withMainContext {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Analytics.addContext(scanResponse)
|
||||
val isOnboardingCase = result.data
|
||||
if (isOnboardingCase) {
|
||||
onWalletNotCreated()
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
|
||||
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
|
||||
store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false))
|
||||
navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
|
||||
} else {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
onSuccess(scanResponse)
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
SaltPayExceptionHandler.handle(result.error)
|
||||
onFailure(TangemSdkError.ExceptionError(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
Analytics.addContext(scanResponse)
|
||||
onWalletNotCreated()
|
||||
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
|
||||
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
navigateTo(appScreen) { onProgressStateChange(it) }
|
||||
} else {
|
||||
Analytics.setContext(scanResponse)
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
onWalletNotCreated()
|
||||
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
|
||||
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
|
||||
} else {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
if (scanResponse.card.backupStatus?.isActive == false) {
|
||||
val error = SaltPayActivationError.PutVisaCard
|
||||
SaltPayExceptionHandler.handle(error)
|
||||
onFailure(TangemSdkError.ExceptionError(error))
|
||||
} else {
|
||||
Analytics.setContext(scanResponse)
|
||||
onSuccess(scanResponse)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
Analytics.addContext(scanResponse)
|
||||
onWalletNotCreated()
|
||||
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
|
||||
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
navigateTo(appScreen) { onProgressStateChange(it) }
|
||||
} else {
|
||||
Analytics.setContext(scanResponse)
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
onWalletNotCreated()
|
||||
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
|
||||
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
|
||||
} else {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
onSuccess(scanResponse)
|
||||
}
|
||||
onSuccess(scanResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.tangem.core.analytics.AnalyticsEvent
|
|||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -52,7 +51,6 @@ internal object UseCaseScanProcessor {
|
|||
if (analyticsEvent != null) {
|
||||
add(AnalyticsChain(analyticsEvent))
|
||||
}
|
||||
add(CheckForUnfinishedSaltPayBackupChain(backupService))
|
||||
add(DisclaimerChain(store, disclaimerWillShow))
|
||||
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager, preferencesStorage))
|
||||
}
|
||||
|
|
@ -91,9 +89,7 @@ internal object UseCaseScanProcessor {
|
|||
navigateTo(exception.onboardingRoute)
|
||||
onWalletNotCreated()
|
||||
}
|
||||
is ScanChainException.PutSaltPayVisaCard,
|
||||
is ScanChainException.DisclaimerWasCanceled,
|
||||
is ScanChainException.CheckForSaltPayOnboardingCaseException,
|
||||
-> onFailure(scanCardExceptionConverter.convertBack(exception))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@ package com.tangem.tap.domain.scanCard.chains
|
|||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.twinsIsTwinned
|
||||
import com.tangem.domain.core.chain.Chain
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -21,13 +18,8 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
|||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import kotlinx.coroutines.delay
|
||||
import org.rekotlin.Store
|
||||
|
||||
|
|
@ -35,9 +27,7 @@ import org.rekotlin.Store
|
|||
* Handles the verification process to determine if the scanned card requires onboarding.
|
||||
*
|
||||
* Returns:
|
||||
* - [ScanChainException.OnboardingNeeded] if onboarding required for none SaltPay cards.
|
||||
* - [ScanChainException.CheckForSaltPayOnboardingCaseException] if unable to check for SaltPay onboarding case.
|
||||
* - [ScanChainException.PutSaltPayVisaCard] if scanned SaltPay card is not activated and onboarding required.
|
||||
* - [ScanChainException.OnboardingNeeded] if onboarding required.
|
||||
*
|
||||
* @param store the [Store] that holds the state of the app.
|
||||
* @param tapWalletManager manager responsible for handling operations related to the Wallet.
|
||||
|
|
@ -54,54 +44,10 @@ class CheckForOnboardingChain(
|
|||
override suspend fun invoke(
|
||||
previousChainResult: ScanResponse,
|
||||
): Either<ScanCardException.ChainException, ScanResponse> {
|
||||
val cardTypesResolver = previousChainResult.cardTypesResolver
|
||||
|
||||
tapWalletManager.updateConfigManager(previousChainResult)
|
||||
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
|
||||
|
||||
return when {
|
||||
cardTypesResolver.isSaltPayVisa() -> {
|
||||
val manager = SaltPayActivationManagerFactory(
|
||||
blockchain = previousChainResult.cardTypesResolver.getBlockchain(),
|
||||
card = previousChainResult.card,
|
||||
).create()
|
||||
val result = OnboardingSaltPayHelper.isOnboardingCase(previousChainResult, manager)
|
||||
delay(timeMillis = 500)
|
||||
withMainContext {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Analytics.addContext(previousChainResult)
|
||||
val isOnboardingCase = result.data
|
||||
if (isOnboardingCase) {
|
||||
store.dispatch(
|
||||
GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = false),
|
||||
)
|
||||
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
|
||||
store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false))
|
||||
ScanChainException.OnboardingNeeded(AppScreen.OnboardingWallet).left()
|
||||
} else {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
previousChainResult.right()
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
SaltPayExceptionHandler.handle(result.error)
|
||||
ScanChainException.CheckForSaltPayOnboardingCaseException(result.error).left()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cardTypesResolver.isSaltPayWallet() -> {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
if (previousChainResult.card.backupStatus?.isActive == false) {
|
||||
SaltPayExceptionHandler.handle(SaltPayActivationError.PutVisaCard)
|
||||
ScanChainException.PutSaltPayVisaCard.left()
|
||||
} else {
|
||||
Analytics.setContext(previousChainResult)
|
||||
previousChainResult.right()
|
||||
}
|
||||
}
|
||||
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
|
||||
Analytics.addContext(previousChainResult)
|
||||
store.dispatchOnMain(GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = true))
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.tap.domain.scanCard.chains
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.core.chain.Chain
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
|
||||
|
||||
/**
|
||||
* Handles the verification for unfinished SaltPay backup processes after the card scanning operation.
|
||||
*
|
||||
* Returns [ScanChainException.PutSaltPayVisaCard] if backup is not finished.
|
||||
*
|
||||
* @param backupService a service responsible for managing backup operations, used here to check for unfinished backups
|
||||
* and verify the primary card type.
|
||||
*
|
||||
* @see Chain for more information about the Chain interface.
|
||||
*/
|
||||
internal class CheckForUnfinishedSaltPayBackupChain(
|
||||
private val backupService: BackupService,
|
||||
) : Chain<ScanCardException.ChainException, ScanResponse> {
|
||||
|
||||
override suspend fun invoke(
|
||||
previousChainResult: ScanResponse,
|
||||
): Either<ScanCardException.ChainException, ScanResponse> {
|
||||
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
|
||||
return previousChainResult.right()
|
||||
}
|
||||
|
||||
val isTheSamePrimaryCard = backupService.primaryCardId
|
||||
?.let { it == previousChainResult.card.cardId }
|
||||
?: false
|
||||
|
||||
return if (previousChainResult.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) {
|
||||
ScanChainException.PutSaltPayVisaCard.left()
|
||||
} else {
|
||||
previousChainResult.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,6 @@ import com.tangem.tap.common.redux.navigation.AppScreen
|
|||
|
||||
sealed class ScanChainException : ScanCardException.ChainException() {
|
||||
|
||||
/**
|
||||
* May be returned from [CheckForOnboardingChain] and [CheckForUnfinishedSaltPayBackupChain]
|
||||
* */
|
||||
object PutSaltPayVisaCard : ScanChainException()
|
||||
|
||||
/**
|
||||
* May be returned from [DisclaimerChain]
|
||||
* */
|
||||
|
|
@ -21,11 +16,4 @@ sealed class ScanChainException : ScanCardException.ChainException() {
|
|||
* @param onboardingRoute route where to navigate
|
||||
* */
|
||||
data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException()
|
||||
|
||||
/**
|
||||
* May be returned from [CheckForOnboardingChain]
|
||||
*
|
||||
* @param cause cause of exception
|
||||
* */
|
||||
data class CheckForSaltPayOnboardingCaseException(override val cause: Throwable?) : ScanChainException()
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.tap.domain.scanCard.chains.ScanChainException
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCardException> {
|
||||
|
|
@ -35,10 +34,6 @@ internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCar
|
|||
private fun concertScanChainException(value: ScanCardException.ChainException): TangemError {
|
||||
return when (val e = value as? ScanChainException) {
|
||||
is ScanChainException.DisclaimerWasCanceled -> TangemSdkError.UserCancelled()
|
||||
is ScanChainException.PutSaltPayVisaCard -> TangemSdkError.ExceptionError(
|
||||
SaltPayActivationError.PutVisaCard,
|
||||
)
|
||||
is ScanChainException.CheckForSaltPayOnboardingCaseException,
|
||||
is ScanChainException.OnboardingNeeded,
|
||||
null,
|
||||
-> TangemSdkError.ExceptionError(e?.cause)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import com.tangem.crypto.CryptoUtils
|
|||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.TapWorkarounds.isExcluded
|
||||
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
|
|
@ -189,9 +188,7 @@ private class ScanWalletProcessor(
|
|||
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() &&
|
||||
(activationInProgress || card.isSaltPay)
|
||||
) {
|
||||
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() && activationInProgress) {
|
||||
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
|
||||
when (linkingResult) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
@ -213,10 +210,7 @@ private class ScanWalletProcessor(
|
|||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
val productType = when (card.isSaltPay) {
|
||||
true -> ProductType.SaltPay
|
||||
else -> ProductType.Wallet
|
||||
}
|
||||
val productType = ProductType.Wallet
|
||||
scope.launch {
|
||||
val derivations = collectDerivations(card)
|
||||
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechService
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.datasource.files.AndroidFileReader
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.tap.common.AndroidFileReader
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.domain.tokens.converters.CurrencyConverter
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.squareup.moshi.JsonAdapter
|
|||
import com.tangem.Log
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.tap.common.FileReader
|
||||
import com.tangem.datasource.files.FileReader
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
||||
class UserTokensStorageService(private val fileReader: FileReader) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.domain.userWalletList
|
|||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
|
|
@ -43,7 +42,6 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV
|
|||
return when {
|
||||
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
|
||||
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
|
||||
SaltPayWorkaround.isSaltPayCardId(cardId) -> Artwork.SALT_PAY_URL
|
||||
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
|
||||
TwinCardNumber.First -> Artwork.TWIN_CARD_1
|
||||
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ package com.tangem.tap.domain.walletStores.repository.implementation
|
|||
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.TransactionHistoryProvider
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result.Failure
|
||||
|
|
@ -35,7 +33,6 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update
|
|||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithTxHistories
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
|
||||
|
|
@ -248,11 +245,6 @@ internal class DefaultWalletAmountsRepository(
|
|||
TestActions.testAmountInjectionForWalletManagerEnabled = false
|
||||
} else {
|
||||
walletManager.update()
|
||||
|
||||
val wallet = walletManager.wallet
|
||||
if (wallet.blockchain == Blockchain.SaltPay && walletManager is TransactionHistoryProvider) {
|
||||
walletManager.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +341,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
it.updateWithDemoAmounts(wallet = updatedWallet)
|
||||
} else {
|
||||
it.updateWithAmounts(wallet = updatedWallet)
|
||||
}.updateWithTxHistories(wallet = updatedWallet)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import com.tangem.blockchain.common.BlockchainSdkError
|
|||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.tap.common.extensions.getBlockchainTxHistory
|
||||
import com.tangem.tap.common.extensions.getTokenTxHistory
|
||||
import com.tangem.tap.domain.extensions.amountToCreateAccount
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
|
|
@ -28,21 +26,6 @@ internal fun List<WalletDataModel>.updateWithFiatRates(fiatRates: Map<String, Do
|
|||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithTxHistory(wallet: Wallet): WalletDataModel {
|
||||
return this.copy(
|
||||
historyTransactions = when (currency) {
|
||||
is Currency.Blockchain -> wallet.getBlockchainTxHistory()
|
||||
is Currency.Token -> wallet.getTokenTxHistory(currency.token)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithTxHistories(wallet: Wallet): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithTxHistory(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithAmount(wallet)
|
||||
|
|
@ -170,7 +153,10 @@ internal fun List<WalletDataModel>.updateWithMissedDerivation(): List<WalletData
|
|||
internal fun List<WalletDataModel>.updateWithUnreachable(): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.copy(
|
||||
status = WalletDataModel.Unreachable(errorMessage = null),
|
||||
status = WalletDataModel.Unreachable(
|
||||
errorMessage = null,
|
||||
amount = walletData.status.amount,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,6 @@ internal fun WalletStoreModel.updateWithError(wallet: Wallet, error: TangemError
|
|||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithTxHistories(wallet: Wallet): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithTxHistories(wallet = wallet),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithAmounts(wallet: Wallet): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithAmounts(wallet = wallet),
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import com.google.gson.GsonBuilder
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer
|
||||
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.TradeData
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
|
||||
import com.trustwallet.walletconnect.models.binance.tradeOrderSerializer
|
||||
import timber.log.Timber
|
||||
|
||||
object BnbHelper {
|
||||
|
|
@ -39,6 +41,50 @@ object BnbHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun WcBinanceTradeOrder.toWCBinanceTradeOrder(): WCBinanceTradeOrder {
|
||||
return WCBinanceTradeOrder(
|
||||
account_number = accountNumber,
|
||||
chain_id = chainId,
|
||||
data = data,
|
||||
memo = memo,
|
||||
sequence = sequence,
|
||||
source = source,
|
||||
msgs = msgs.map { it.toWCBinanceTradeOrderMessage() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun WcBinanceTradeOrder.Message.toWCBinanceTradeOrderMessage(): WCBinanceTradeOrder.Message {
|
||||
return WCBinanceTradeOrder.Message(id, orderType, price, quantity, sender, side, symbol, timeInforce)
|
||||
}
|
||||
|
||||
fun WcBinanceTransferOrder.toWCBinanceTransferOrder(): WCBinanceTransferOrder {
|
||||
return WCBinanceTransferOrder(
|
||||
account_number = accountNumber,
|
||||
chain_id = chainId,
|
||||
data = data,
|
||||
memo = memo,
|
||||
sequence = sequence,
|
||||
source = source,
|
||||
msgs = msgs.map { it.toWCBinanceTransferOrderMessage() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun WcBinanceTransferOrder.Message.toWCBinanceTransferOrderMessage(): WCBinanceTransferOrder.Message {
|
||||
return WCBinanceTransferOrder.Message(
|
||||
inputs.map { it.toWCBinanceItem() },
|
||||
outputs.map { it.toWCBinanceItem() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun WcBinanceTransferOrder.Message.Item.toWCBinanceItem(): WCBinanceTransferOrder.Message.Item {
|
||||
return WCBinanceTransferOrder.Message.Item(
|
||||
address,
|
||||
coins.map {
|
||||
WCBinanceTransferOrder.Message.Item.Coin(it.amount, it.denom)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun createMessageData(order: WCBinanceTradeOrder): BinanceMessageData.Trade {
|
||||
val address = order.msgs.first().sender
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,18 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.analytics.events.WalletConnect
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.walletconnect.extensions.isDappSupported
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
|
||||
import com.tangem.tap.domain.walletconnect.extensions.toWcEthereumSignMessage
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.*
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.walletConnectRepository
|
||||
|
|
@ -61,7 +60,7 @@ class WalletConnectManager {
|
|||
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
|
||||
}
|
||||
|
||||
private var sessions: MutableMap<WCSession, WalletConnectActiveData> = mutableMapOf()
|
||||
private var sessions: MutableMap<Topic, WalletConnectActiveData> = mutableMapOf()
|
||||
|
||||
fun connect(wcUri: String) {
|
||||
val session = WCSession.from(wcUri).guard {
|
||||
|
|
@ -73,7 +72,7 @@ class WalletConnectManager {
|
|||
)
|
||||
return
|
||||
}
|
||||
if (sessions[session] != null) {
|
||||
if (sessions[session.topic] != null) {
|
||||
store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession)
|
||||
return
|
||||
}
|
||||
|
|
@ -93,7 +92,7 @@ class WalletConnectManager {
|
|||
return
|
||||
}
|
||||
|
||||
sessions[session] = WalletConnectActiveData(
|
||||
sessions[session.topic] = WalletConnectActiveData(
|
||||
peerId = peerId,
|
||||
remotePeerId = null,
|
||||
session = session,
|
||||
|
|
@ -104,26 +103,26 @@ class WalletConnectManager {
|
|||
}
|
||||
|
||||
fun updateSession(session: WalletConnectSession) {
|
||||
val updatedSession = sessions[session.session]?.copy(
|
||||
val updatedSession = sessions[session.session.topic]?.copy(
|
||||
wallet = session.wallet,
|
||||
)
|
||||
if (updatedSession != null) {
|
||||
sessions[session.session] = updatedSession
|
||||
sessions[session.session.topic] = updatedSession
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBlockchain(session: WalletConnectSession) {
|
||||
sessions[session.session]?.client?.updateSession(
|
||||
sessions[session.session.topic]?.client?.updateSession(
|
||||
accounts = listOfNotNull(session.getAddress()),
|
||||
chainId = session.wallet.blockchain?.getChainId(),
|
||||
approved = true,
|
||||
)
|
||||
|
||||
val updatedSession = sessions[session.session]?.copy(
|
||||
val updatedSession = sessions[session.session.topic]?.copy(
|
||||
wallet = session.wallet,
|
||||
)
|
||||
if (updatedSession != null) {
|
||||
sessions[session.session] = updatedSession
|
||||
sessions[session.session.topic] = updatedSession
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +130,7 @@ class WalletConnectManager {
|
|||
private fun setupConnectionTimeoutCheck(session: WCSession) {
|
||||
scope.launch {
|
||||
delay(20_000)
|
||||
val data = sessions[session]
|
||||
val data = sessions[session.topic]
|
||||
if (data != null && data.peerMeta == null) {
|
||||
disconnect(session)
|
||||
store.dispatchOnMain(WalletConnectAction.OpeningSessionTimeout(session))
|
||||
|
|
@ -160,13 +159,13 @@ class WalletConnectManager {
|
|||
setListeners(it.client)
|
||||
it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId)
|
||||
}
|
||||
}.associateBy { it.session }.toMutableMap()
|
||||
}.associateBy { it.session.topic }.toMutableMap()
|
||||
|
||||
store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions))
|
||||
}
|
||||
|
||||
fun approve(session: WCSession) {
|
||||
val activeData = sessions[session] ?: return
|
||||
val activeData = sessions[session.topic] ?: return
|
||||
removeSimilarSessions(activeData)
|
||||
|
||||
val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey ?: return
|
||||
|
|
@ -203,18 +202,18 @@ class WalletConnectManager {
|
|||
sessionsToRemove.forEach { disconnect(it.value.session) }
|
||||
}
|
||||
|
||||
fun rejectRequest(session: WCSession, id: Long) {
|
||||
val activeData = sessions[session] ?: return
|
||||
fun rejectRequest(topic: String, id: Long) {
|
||||
val activeData = sessions[topic] ?: return
|
||||
activeData.client.rejectRequest(id)
|
||||
}
|
||||
|
||||
fun acceptRequest(session: WCSession, id: Long, data: String) {
|
||||
val activeData = sessions[session] ?: return
|
||||
fun acceptRequest(topic: String, id: Long, data: String) {
|
||||
val activeData = sessions[topic] ?: return
|
||||
activeData.client.approveRequest(id, data)
|
||||
}
|
||||
|
||||
fun disconnect(session: WCSession) {
|
||||
val activeData = sessions[session] ?: return
|
||||
val activeData = sessions[session.topic] ?: return
|
||||
val disconnected = if (activeData.client.isConnected) {
|
||||
activeData.client.killSession()
|
||||
} else {
|
||||
|
|
@ -228,118 +227,145 @@ class WalletConnectManager {
|
|||
|
||||
private fun onSessionClosed(session: WCSession) {
|
||||
Analytics.send(WalletConnect.SessionDisconnected())
|
||||
sessions.remove(session)
|
||||
sessions.remove(session.topic)
|
||||
walletConnectRepository.removeSession(session)
|
||||
store.dispatchOnMain(WalletConnectAction.RemoveSession(session))
|
||||
}
|
||||
|
||||
fun handleTransactionRequest(
|
||||
transaction: WCEthereumTransaction,
|
||||
transaction: WcEthereumTransaction,
|
||||
session: WalletConnectSession,
|
||||
id: Long,
|
||||
type: WcTransactionType,
|
||||
type: WcEthTransactionType,
|
||||
) {
|
||||
val activeData = sessions[session.session] ?: return
|
||||
val activeData = sessions[session.session.topic] ?: return
|
||||
scope.launch {
|
||||
val data = WalletConnectSdkHelper().prepareTransactionData(
|
||||
transaction = transaction,
|
||||
session = session,
|
||||
id = id,
|
||||
type = type,
|
||||
EthTransactionData(
|
||||
transaction = transaction,
|
||||
networkId = session.wallet.blockchain?.toNetworkId() ?: "",
|
||||
rawDerivationPath = session.wallet.derivationPath?.rawPath,
|
||||
id = id,
|
||||
topic = session.session.topic,
|
||||
type = type,
|
||||
metaName = session.peerMeta.name,
|
||||
metaUrl = session.peerMeta.url,
|
||||
),
|
||||
).guard {
|
||||
sessions[session.session] = activeData.copy(transactionData = null)
|
||||
sessions[session.session.topic] = activeData.copy(transactionData = null)
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
session.session,
|
||||
session.session.topic,
|
||||
id,
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
sessions[session.session] = activeData.copy(transactionData = data)
|
||||
sessions[session.session.topic] = activeData.copy(transactionData = data)
|
||||
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.RequestTransaction(
|
||||
data.dialogData,
|
||||
WcPreparedRequest.EthTransaction(
|
||||
preparedRequestData = data,
|
||||
topic = session.session.topic,
|
||||
requestId = id,
|
||||
derivationPath = data.walletManager.wallet.publicKey.derivationPath?.rawPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun completeTransaction(session: WCSession) {
|
||||
val activeData = sessions[session]
|
||||
fun completeTransaction(topic: Topic) {
|
||||
val activeData = sessions[topic]
|
||||
val data = activeData?.transactionData ?: return
|
||||
scope.launch {
|
||||
val hash = WalletConnectSdkHelper().completeTransaction(data, cardId).guard {
|
||||
sessions[data.session.session] = activeData.copy(transactionData = null)
|
||||
sessions[topic] = activeData.copy(transactionData = null)
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
data.session.session,
|
||||
topic,
|
||||
data.id,
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
acceptRequest(session, data.id, hash)
|
||||
sessions[session] = activeData.copy(transactionData = null)
|
||||
acceptRequest(topic, data.id, hash)
|
||||
sessions[topic] = activeData.copy(transactionData = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun signBnb(id: Long, data: ByteArray, sessionData: WCSession) {
|
||||
val activeData = sessions[sessionData] ?: return
|
||||
fun signBnb(id: Long, data: ByteArray, topic: Topic) {
|
||||
val activeData = sessions[topic] ?: return
|
||||
scope.launch {
|
||||
val hash = WalletConnectSdkHelper().signBnbTransaction(data, activeData, cardId).guard {
|
||||
val hash = WalletConnectSdkHelper().signBnbTransaction(
|
||||
data = data,
|
||||
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
|
||||
derivationPath = activeData.wallet.derivationPath?.rawPath,
|
||||
cardId = cardId,
|
||||
).guard {
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
sessionData,
|
||||
topic,
|
||||
id,
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
acceptRequest(sessionData, id, hash)
|
||||
acceptRequest(topic, id, hash)
|
||||
}
|
||||
}
|
||||
|
||||
fun handlePersonalSignRequest(message: WCEthereumSignMessage, session: WalletConnectSession, id: Long) {
|
||||
val activeData = sessions[session.session] ?: return
|
||||
val activeData = sessions[session.session.topic] ?: return
|
||||
scope.launch {
|
||||
val data = WalletConnectSdkHelper().prepareDataForPersonalSign(
|
||||
message = message,
|
||||
session = session,
|
||||
message = message.toWcEthereumSignMessage(),
|
||||
topic = session.session.topic,
|
||||
id = id,
|
||||
metaName = session.peerMeta.name,
|
||||
)
|
||||
sessions[session.session] = activeData.copy(personalSignData = data)
|
||||
sessions[session.session.topic] = activeData.copy(personalSignData = data)
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.PersonalSign(
|
||||
data.dialogData,
|
||||
WcPreparedRequest.EthSign(
|
||||
preparedRequestData = data,
|
||||
topic = session.session.topic,
|
||||
requestId = id,
|
||||
derivationPath = session.wallet.derivationPath?.rawPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendSignedMessage(session: WCSession) {
|
||||
val activeData = sessions[session]
|
||||
fun sendSignedMessage(topic: Topic) {
|
||||
val activeData = sessions[topic]
|
||||
val data = activeData?.personalSignData ?: return
|
||||
scope.launch {
|
||||
val hash = WalletConnectSdkHelper().signPersonalMessage(data.hash, activeData.wallet, cardId)
|
||||
val hash = WalletConnectSdkHelper().signPersonalMessage(
|
||||
hashToSign = data.hash,
|
||||
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
|
||||
derivationPath = activeData.wallet.derivationPath?.rawPath,
|
||||
cardId = cardId,
|
||||
)
|
||||
.guard {
|
||||
sessions[data.session.session] = activeData.copy(transactionData = null)
|
||||
sessions[topic] = activeData.copy(transactionData = null)
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
data.session.session,
|
||||
topic,
|
||||
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)
|
||||
sessions[topic] = activeData.copy(personalSignData = data)
|
||||
acceptRequest(topic, data.id, hash)
|
||||
sessions[topic] = activeData.copy(transactionData = null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +374,7 @@ class WalletConnectManager {
|
|||
client.onSessionRequest = { id: Long, peer: WCPeerMeta ->
|
||||
Timber.d("OnSessionRequest: $peer")
|
||||
val session = client.session
|
||||
val data = sessions[session]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId)
|
||||
val data = sessions[session?.topic]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId)
|
||||
if (data != null && session != null) {
|
||||
if (!peer.isDappSupported()) {
|
||||
store.dispatchOnMain(
|
||||
|
|
@ -358,7 +384,7 @@ class WalletConnectManager {
|
|||
),
|
||||
)
|
||||
} else {
|
||||
sessions[session] = data
|
||||
sessions[session.topic] = data
|
||||
val sessionData = data.toWalletConnectSession()
|
||||
sessionData?.let {
|
||||
store.dispatchOnMain(
|
||||
|
|
@ -384,13 +410,13 @@ class WalletConnectManager {
|
|||
// AnalyticsAnOld.WcAction.SendTransaction
|
||||
// )
|
||||
// )
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.HandleTransactionRequest(
|
||||
transaction = transaction,
|
||||
session = sessionData,
|
||||
id = id,
|
||||
type = WcTransactionType.EthSendTransaction,
|
||||
type = WcEthTransactionType.EthSendTransaction,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -402,13 +428,13 @@ class WalletConnectManager {
|
|||
// AnalyticsAnOld.WcAction.SignTransaction
|
||||
// )
|
||||
// )
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.HandleTransactionRequest(
|
||||
transaction = transaction,
|
||||
session = sessionData,
|
||||
id = id,
|
||||
type = WcTransactionType.EthSignTransaction,
|
||||
type = WcEthTransactionType.EthSignTransaction,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -420,7 +446,7 @@ class WalletConnectManager {
|
|||
// AnalyticsAnOld.WcAction.PersonalSign
|
||||
// )
|
||||
// )
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.HandlePersonalSignRequest(
|
||||
message,
|
||||
|
|
@ -434,7 +460,7 @@ class WalletConnectManager {
|
|||
client.onBnbCancel = { id: Long, order: WCBinanceCancelOrder ->
|
||||
}
|
||||
client.onBnbTrade = { id: Long, order: WCBinanceTradeOrder ->
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.BinanceTransaction.Trade(
|
||||
id = id,
|
||||
|
|
@ -445,7 +471,7 @@ class WalletConnectManager {
|
|||
}
|
||||
}
|
||||
client.onBnbTransfer = { id: Long, order: WCBinanceTransferOrder ->
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.BinanceTransaction.Transfer(
|
||||
id = id,
|
||||
|
|
@ -484,7 +510,7 @@ class WalletConnectManager {
|
|||
private fun switchChain(chainId: Int, client: WCClient) {
|
||||
val blockchain = Blockchain.fromChainId(chainId)
|
||||
Timber.d("WC switch chainID\nNew Blockchain: $blockchain")
|
||||
val session = sessions[client.session]?.toWalletConnectSession()
|
||||
val session = sessions[client.session?.topic]?.toWalletConnectSession()
|
||||
if (session != null) {
|
||||
store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session))
|
||||
}
|
||||
|
|
@ -499,6 +525,8 @@ class WalletConnectManager {
|
|||
}
|
||||
}
|
||||
|
||||
typealias Topic = String
|
||||
|
||||
data class WalletConnectActiveData(
|
||||
val peerId: String,
|
||||
val remotePeerId: String?,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,7 @@ 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.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.CommonSigner
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
|
|
@ -24,41 +17,40 @@ import com.tangem.common.extensions.hexToBytes
|
|||
import com.tangem.common.extensions.toDecompressedPublicKey
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.operations.sign.SignHashCommand
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType
|
||||
import com.tangem.tap.common.analytics.events.WalletConnect
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.extensions.toFormattedString
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder
|
||||
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
|
||||
import com.tangem.tap.features.details.redux.walletconnect.*
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
|
||||
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.WCEthereumSignMessage.WCSignType.MESSAGE
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.TYPED_MESSAGE
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.*
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WalletConnectSdkHelper {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
suspend fun prepareTransactionData(
|
||||
transaction: WCEthereumTransaction,
|
||||
session: WalletConnectSession,
|
||||
id: Long,
|
||||
type: WcTransactionType,
|
||||
): WcTransactionData? {
|
||||
val walletManager = getWalletManager(session) ?: return null
|
||||
suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData? {
|
||||
val transaction = data.transaction
|
||||
val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null
|
||||
val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null
|
||||
|
||||
walletManager.safeUpdate()
|
||||
val wallet = walletManager.wallet
|
||||
|
|
@ -95,53 +87,49 @@ class WalletConnectSdkHelper {
|
|||
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
|
||||
),
|
||||
)
|
||||
|
||||
val dialogData = TransactionRequestDialogData(
|
||||
dAppName = session.peerMeta.name,
|
||||
dAppUrl = session.peerMeta.url,
|
||||
dAppName = data.metaName,
|
||||
dAppUrl = data.metaUrl,
|
||||
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,
|
||||
topic = data.topic,
|
||||
id = data.id,
|
||||
type = data.type,
|
||||
)
|
||||
return WcTransactionData(
|
||||
type = type,
|
||||
type = data.type,
|
||||
transaction = transactionData,
|
||||
session = session,
|
||||
id = id,
|
||||
topic = data.topic,
|
||||
id = data.id,
|
||||
walletManager = walletManager,
|
||||
dialogData = dialogData,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getWalletManager(session: WalletConnectSession): WalletManager? {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val publicKey = Wallet.PublicKey(
|
||||
session.wallet.walletPublicKey ?: return null,
|
||||
session.wallet.derivedPublicKey,
|
||||
session.wallet.derivationPath,
|
||||
)
|
||||
val blockchain = session.wallet.getBlockchainForSession()
|
||||
return factory.makeWalletManager(
|
||||
private fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = blockchain,
|
||||
publicKey = publicKey,
|
||||
derivationPath = derivationPath,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
return store.state.walletState.getWalletManager(blockchainNetwork)
|
||||
}
|
||||
|
||||
suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? {
|
||||
return when (data.type) {
|
||||
WcTransactionType.EthSendTransaction -> sendTransaction(data, cardId)
|
||||
WcTransactionType.EthSignTransaction -> signTransaction(data, cardId)
|
||||
WcEthTransactionType.EthSendTransaction -> sendTransaction(data, cardId)
|
||||
WcEthTransactionType.EthSignTransaction -> signTransaction(data, cardId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGasLimitFromTx(
|
||||
value: BigDecimal,
|
||||
walletManager: WalletManager,
|
||||
transaction: WCEthereumTransaction,
|
||||
transaction: WcEthereumTransaction,
|
||||
): BigDecimal {
|
||||
return transaction.gas?.hexToBigDecimal()
|
||||
?: transaction.gasLimit?.hexToBigDecimal()
|
||||
|
|
@ -155,7 +143,7 @@ class WalletConnectSdkHelper {
|
|||
private suspend fun getGaLimitFromBlockchain(
|
||||
value: BigDecimal,
|
||||
walletManager: WalletManager,
|
||||
transaction: WCEthereumTransaction,
|
||||
transaction: WcEthereumTransaction,
|
||||
): BigDecimal {
|
||||
val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit(
|
||||
amount = Amount(value, walletManager.wallet.blockchain),
|
||||
|
|
@ -180,7 +168,7 @@ class WalletConnectSdkHelper {
|
|||
return when (result) {
|
||||
SimpleResult.Success -> {
|
||||
val sentFrom = AnalyticsParam.TxSentFrom.WalletConnect
|
||||
Analytics.send(Basic.TransactionSent(sentFrom))
|
||||
Analytics.send(Basic.TransactionSent(sentFrom = sentFrom, memoType = MemoType.Null))
|
||||
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
|
||||
}
|
||||
is SimpleResult.Failure -> {
|
||||
|
|
@ -216,17 +204,32 @@ class WalletConnectSdkHelper {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun signBnbTransaction(data: ByteArray, session: WalletConnectActiveData, cardId: String?): String? {
|
||||
fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade {
|
||||
return BnbHelper.createMessageData(data.toWCBinanceTradeOrder())
|
||||
}
|
||||
|
||||
fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer {
|
||||
return BnbHelper.createMessageData(data.toWCBinanceTransferOrder())
|
||||
}
|
||||
|
||||
suspend fun signBnbTransaction(
|
||||
data: ByteArray,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
cardId: String?,
|
||||
): String? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
|
||||
val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
|
||||
|
||||
val command = SignHashCommand(
|
||||
hash = data,
|
||||
walletPublicKey = session.wallet.walletPublicKey ?: return null,
|
||||
derivationPath = session.wallet.derivationPath,
|
||||
walletPublicKey = wallet.publicKey.seedKey,
|
||||
derivationPath = wallet.publicKey.derivationPath,
|
||||
)
|
||||
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val key = session.wallet.derivedPublicKey?.toDecompressedPublicKey()
|
||||
?: session.wallet.walletPublicKey.toDecompressedPublicKey()
|
||||
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
|
||||
getBnbResultString(
|
||||
key.toHexString(),
|
||||
result.data.signature.toHexString(),
|
||||
|
|
@ -241,13 +244,16 @@ class WalletConnectSdkHelper {
|
|||
}
|
||||
|
||||
fun prepareDataForPersonalSign(
|
||||
message: WCEthereumSignMessage,
|
||||
session: WalletConnectSession,
|
||||
message: WcEthereumSignMessage,
|
||||
topic: String,
|
||||
metaName: String,
|
||||
id: Long,
|
||||
): WcPersonalSignData {
|
||||
val messageData = when (message.type) {
|
||||
MESSAGE, PERSONAL_MESSAGE -> createMessageData(message)
|
||||
TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
|
||||
WcEthereumSignMessage.WCSignType.MESSAGE,
|
||||
WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
|
||||
-> createMessageData(message)
|
||||
WcEthereumSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
|
||||
}
|
||||
|
||||
val messageString = message.data.hexToAscii()
|
||||
|
|
@ -255,20 +261,21 @@ class WalletConnectSdkHelper {
|
|||
?: message.data
|
||||
|
||||
val dialogData = PersonalSignDialogData(
|
||||
dAppName = session.peerMeta.name,
|
||||
dAppName = metaName,
|
||||
message = messageString,
|
||||
session = session.session,
|
||||
topic = topic,
|
||||
id = id,
|
||||
)
|
||||
return WcPersonalSignData(
|
||||
hash = messageData,
|
||||
session = session,
|
||||
topic = topic,
|
||||
id = id,
|
||||
dialogData = dialogData,
|
||||
type = message.type,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMessageData(message: WCEthereumSignMessage): ByteArray {
|
||||
private fun createMessageData(message: WcEthereumSignMessage): ByteArray {
|
||||
val messageData = try {
|
||||
message.data.removePrefix(HEX_PREFIX).hexToBytes()
|
||||
} catch (exception: Exception) {
|
||||
|
|
@ -299,16 +306,27 @@ class WalletConnectSdkHelper {
|
|||
}.joinToString("")
|
||||
}
|
||||
|
||||
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession, cardId: String?): String? {
|
||||
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey
|
||||
val command = SignHashCommand(hashToSign, wallet.walletPublicKey!!, wallet.derivationPath)
|
||||
suspend fun signPersonalMessage(
|
||||
hashToSign: ByteArray,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
cardId: String?,
|
||||
): String? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
|
||||
val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
|
||||
|
||||
val command = SignHashCommand(
|
||||
hash = hashToSign,
|
||||
walletPublicKey = wallet.publicKey.seedKey,
|
||||
derivationPath = wallet.publicKey.derivationPath,
|
||||
)
|
||||
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
|
||||
is CompletionResult.Success -> {
|
||||
val hash = result.data.signature
|
||||
return EthereumUtils.prepareSignedMessageData(
|
||||
signedHash = hash,
|
||||
hashToSign = hashToSign,
|
||||
publicKey = CryptoUtils.decompressPublicKey(key!!),
|
||||
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,39 @@
|
|||
package com.tangem.tap.domain.walletconnect.extensions
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
|
||||
import com.trustwallet.walletconnect.models.WCPeerMeta
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
|
||||
|
||||
fun WCPeerMeta.isDappSupported(): Boolean {
|
||||
return !unsupportedDappsList.any { this.url.contains(it) }
|
||||
}
|
||||
|
||||
private val unsupportedDappsList: List<String> = listOf("dydx.exchange")
|
||||
private val unsupportedDappsList: List<String> = listOf("dydx.exchange")
|
||||
|
||||
fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
|
||||
return WcEthereumTransaction(
|
||||
from = from,
|
||||
to = to,
|
||||
nonce = nonce,
|
||||
gasPrice = gasPrice,
|
||||
maxFeePerGas = maxFeePerGas,
|
||||
maxPriorityFeePerGas = maxPriorityFeePerGas,
|
||||
gas = gas,
|
||||
gasLimit = gasLimit,
|
||||
value = value,
|
||||
data = data,
|
||||
)
|
||||
}
|
||||
|
||||
fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcEthereumSignMessage {
|
||||
return WcEthereumSignMessage(
|
||||
raw = raw,
|
||||
type = when (type) {
|
||||
WCEthereumSignMessage.WCSignType.MESSAGE -> WcEthereumSignMessage.WCSignType.MESSAGE
|
||||
WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE
|
||||
WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcEthereumSignMessage.WCSignType.TYPED_MESSAGE
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.tap.domain.walletconnect2.app
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.domain.common.extensions.*
|
||||
import com.tangem.tap.domain.walletconnect2.domain.*
|
||||
|
||||
class TangemWcBlockchainHelper : WcBlockchainHelper {
|
||||
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
|
||||
val parsed = chainId.split(CHAIN_SEPARATOR)
|
||||
if (parsed.size != 2) return null
|
||||
when {
|
||||
parsed.first() == EVM_NAMESPACE -> {
|
||||
val chainIdInt = parsed[1].toIntOrNull() ?: return null
|
||||
return Blockchain.fromChainId(chainIdInt)?.toNetworkId()
|
||||
}
|
||||
else -> {
|
||||
val blockchain = Blockchain.fromNetworkId(parsed.first())
|
||||
return if (supportedNonEvmBlockchains.contains(blockchain)) blockchain?.toNetworkId() else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun networkIdToChainIdOrNull(networkId: String): String? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
val namespace = blockchain?.getCaip2Namespace() ?: return null
|
||||
val chainId = blockchain.getCaip2ChainId() ?: return null
|
||||
return "$namespace$CHAIN_SEPARATOR$chainId"
|
||||
}
|
||||
|
||||
override fun getNamespaceFromFullChainIdOrNull(chainId: String): String? {
|
||||
val parsed = chainId.split(CHAIN_SEPARATOR)
|
||||
return parsed.firstOrNull()
|
||||
}
|
||||
|
||||
override fun chainIdToFullNameOrNull(chainId: String): String? {
|
||||
val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null
|
||||
return Blockchain.fromNetworkId(networkId)?.fullName
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCaip2ChainId(): String? {
|
||||
if (this.isEvm()) return this.getChainId()?.toString()
|
||||
|
||||
return when (this) {
|
||||
Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
|
||||
Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
|
||||
Blockchain.Tron -> "0x2b6653dc"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCaip2Namespace(): String? {
|
||||
return when {
|
||||
this.isEvm() -> EVM_NAMESPACE
|
||||
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val supportedNonEvmBlockchains = emptySet<Blockchain>() // TODO: add supported networks
|
||||
|
||||
private const val EVM_NAMESPACE = "eip155"
|
||||
private const val CHAIN_SEPARATOR = ":"
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.tap.domain.walletconnect2.app
|
||||
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectEventsHandler
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
|
||||
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
|
||||
override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.SessionProposalDialog(
|
||||
sessionProposal = proposal,
|
||||
networks = networksFormatted,
|
||||
onApprove = { store.dispatchOnMain(WalletConnectAction.ApproveProposal) },
|
||||
onReject = { store.dispatchOnMain(WalletConnectAction.RejectProposal) },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSessionEstablished() {
|
||||
store.dispatchOnMain(WalletConnectAction.SessionEstablished)
|
||||
}
|
||||
|
||||
override fun onSessionRejected(error: WalletConnectError) {
|
||||
store.dispatchOnMain(WalletConnectAction.SessionRejected(error))
|
||||
}
|
||||
|
||||
override fun onListOfSessionsUpdated(sessions: List<WcSessionForScreen>) {
|
||||
Timber.d("WC2: List of sessions updated. Sessions: $sessions")
|
||||
store.dispatchOnMain(WalletConnectAction.SessionListUpdated(sessions))
|
||||
}
|
||||
|
||||
override fun onSessionRequest(request: WcPreparedRequest) {
|
||||
store.dispatchOnMain(WalletConnectAction.ShowSessionRequest(request))
|
||||
}
|
||||
|
||||
override fun onUnsupportedRequest() {
|
||||
store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
package com.tangem.tap.domain.walletconnect2.data
|
||||
|
||||
import android.app.*
|
||||
import com.tangem.tap.domain.walletconnect2.domain.*
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.*
|
||||
import com.walletconnect.android.*
|
||||
import com.walletconnect.android.relay.*
|
||||
import com.walletconnect.web3.wallet.client.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.*
|
||||
import javax.inject.*
|
||||
|
||||
class WalletConnectRepositoryImpl @Inject constructor(
|
||||
private val application: Application,
|
||||
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
|
||||
) : WalletConnectRepository {
|
||||
|
||||
private var sessionProposal: Wallet.Model.SessionProposal? = null
|
||||
private var userNamespaces: Map<NetworkNamespace, List<Account>>? = null
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val _events: MutableSharedFlow<WalletConnectEvents> = MutableSharedFlow()
|
||||
override val events: Flow<WalletConnectEvents> = _events
|
||||
|
||||
private val _activeSessions: MutableSharedFlow<List<WalletConnectSession>> = MutableSharedFlow()
|
||||
override val activeSessions: Flow<List<WalletConnectSession>> = _activeSessions
|
||||
|
||||
/**
|
||||
* @param projectId Project ID at https://cloud.walletconnect.com/
|
||||
*/
|
||||
override fun init(projectId: String) {
|
||||
val relayUrl = "relay.walletconnect.com"
|
||||
val serverUrl = "wss://$relayUrl?projectId=$projectId"
|
||||
val connectionType = ConnectionType.AUTOMATIC
|
||||
|
||||
val appMetaData = Core.Model.AppMetaData(
|
||||
name = "Tangem",
|
||||
description = "Tangem Wallet",
|
||||
url = "tangem.com",
|
||||
icons = listOf(
|
||||
"https://user-images.githubusercontent.com/24321494/124071202-72a00900-da58-11eb-935a-dcdab21de52b.png",
|
||||
),
|
||||
redirect = "kotlin-wallet-wc:/request", // Custom Redirect URI
|
||||
)
|
||||
|
||||
CoreClient.initialize(
|
||||
relayServerUrl = serverUrl,
|
||||
connectionType = connectionType,
|
||||
application = application,
|
||||
metaData = appMetaData,
|
||||
) { error ->
|
||||
Timber.d("Error while initializing client: $error")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
WalletConnectError.ExternalApprovalError(error.throwable.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Web3Wallet.initialize(Wallet.Params.Init(core = CoreClient)) { error ->
|
||||
Timber.d("Error while initializing Web3Wallet: $error")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
WalletConnectError.ExternalApprovalError(error.throwable.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val walletDelegate = defineWalletDelegate()
|
||||
Web3Wallet.setWalletDelegate(walletDelegate)
|
||||
}
|
||||
|
||||
private fun defineWalletDelegate(): Web3Wallet.WalletDelegate {
|
||||
return object : Web3Wallet.WalletDelegate {
|
||||
override fun onSessionProposal(sessionProposal: Wallet.Model.SessionProposal) {
|
||||
// Triggered when wallet receives the session proposal sent by a Dapp
|
||||
Timber.d("sessionProposal: $sessionProposal")
|
||||
this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal
|
||||
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionProposal(
|
||||
sessionProposal.name,
|
||||
sessionProposal.description,
|
||||
sessionProposal.url,
|
||||
sessionProposal.icons,
|
||||
sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSessionRequest(sessionRequest: Wallet.Model.SessionRequest) {
|
||||
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
|
||||
Timber.d("sessionRequest: $sessionRequest")
|
||||
val request = wcRequestDeserializer.deserialize(
|
||||
method = sessionRequest.request.method,
|
||||
params = sessionRequest.request.params,
|
||||
)
|
||||
Timber.d("sessionRequestParsed: $request")
|
||||
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionRequest(
|
||||
request = request,
|
||||
chainId = sessionRequest.chainId,
|
||||
topic = sessionRequest.topic,
|
||||
id = sessionRequest.request.id,
|
||||
metaUrl = sessionRequest.peerMetaData?.url ?: "",
|
||||
metaName = sessionRequest.peerMetaData?.name ?: "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthRequest(authRequest: Wallet.Model.AuthRequest) {
|
||||
// Triggered when Dapp / Requester makes an authorization request
|
||||
}
|
||||
|
||||
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
|
||||
// Triggered when the session is deleted by the peer
|
||||
if (sessionDelete is Wallet.Model.SessionDelete.Success) {
|
||||
scope.launch {
|
||||
_events.emit(WalletConnectEvents.SessionDeleted(sessionDelete.topic))
|
||||
}
|
||||
updateSessions()
|
||||
}
|
||||
Timber.d("onSessionDelete: $sessionDelete")
|
||||
}
|
||||
|
||||
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
|
||||
// Triggered when wallet receives the session settlement response from Dapp
|
||||
Timber.d("onSessionSettleResponse: $settleSessionResponse")
|
||||
if (settleSessionResponse is Wallet.Model.SettledSessionResponse.Result) {
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalSuccess(
|
||||
topic = settleSessionResponse.session.topic,
|
||||
accounts = userNamespaces?.flatMap { it.value } ?: emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateSessions()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
|
||||
// Triggered when wallet receives the session update response from Dapp
|
||||
Timber.d("onSessionUpdateResponse: $sessionUpdateResponse")
|
||||
updateSessions()
|
||||
}
|
||||
|
||||
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
|
||||
// Triggered whenever the connection state is changed
|
||||
Timber.d("onConnectionStateChange: $state")
|
||||
if (state.isAvailable) updateSessions()
|
||||
}
|
||||
|
||||
override fun onError(error: Wallet.Model.Error) {
|
||||
// Triggered whenever there is an issue inside the SDK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pair(uri: String) {
|
||||
Web3Wallet.pair(Wallet.Params.Pair(uri))
|
||||
}
|
||||
|
||||
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>) {
|
||||
this.userNamespaces = userNamespaces
|
||||
|
||||
val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal)
|
||||
|
||||
val missingNetworks = findMissingNetworks(
|
||||
namespaces = sessionProposal.requiredNamespaces,
|
||||
userNamespaces = userNamespaces,
|
||||
)
|
||||
if (missingNetworks.isNotEmpty()) {
|
||||
Timber.e("Unsupported blockchains: $missingNetworks")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()),
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val userChains = userNamespaces.flatMap { namespace ->
|
||||
namespace.value.map { it.chainId to "${it.chainId}:${it.walletAddress}" }
|
||||
}.groupBy { pair -> pair.first }
|
||||
.mapValues { entry -> entry.value.map { pair -> pair.second }.toSet() }
|
||||
|
||||
val preparedNamespaces = sessionProposal.requiredNamespaces.map { requiredNamespace ->
|
||||
val accounts = requiredNamespace.value.chains?.mapNotNull { userChains[it] }?.flatten() ?: emptyList()
|
||||
|
||||
requiredNamespace.key to Wallet.Model.Namespace.Session(
|
||||
accounts = accounts,
|
||||
methods = requiredNamespace.value.methods,
|
||||
events = requiredNamespace.value.events,
|
||||
)
|
||||
}.toMap()
|
||||
|
||||
val sessionApproval = Wallet.Params.SessionApprove(sessionProposal.proposerPublicKey, preparedNamespaces)
|
||||
|
||||
Timber.d("Session approval is prepared for sending: $sessionApproval")
|
||||
|
||||
Web3Wallet.approveSession(
|
||||
params = sessionApproval,
|
||||
onSuccess = {
|
||||
Timber.d("Approved successfully: $it")
|
||||
},
|
||||
onError = {
|
||||
Timber.d("Error while approving: $it")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
WalletConnectError.ExternalApprovalError(it.throwable.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun sendRequest(topic: String, id: Long, result: String) {
|
||||
Web3Wallet.respondSessionRequest(
|
||||
params = Wallet.Params.SessionRequestResponse(
|
||||
sessionTopic = topic,
|
||||
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
|
||||
id = id,
|
||||
result = result,
|
||||
),
|
||||
),
|
||||
onSuccess = {},
|
||||
onError = {},
|
||||
)
|
||||
}
|
||||
|
||||
override fun rejectRequest(topic: String, id: Long) {
|
||||
Web3Wallet.respondSessionRequest(
|
||||
params = Wallet.Params.SessionRequestResponse(
|
||||
sessionTopic = topic,
|
||||
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
|
||||
id = id,
|
||||
code = 0,
|
||||
message = "",
|
||||
),
|
||||
),
|
||||
onSuccess = {},
|
||||
onError = {},
|
||||
)
|
||||
}
|
||||
|
||||
override fun reject() {
|
||||
Web3Wallet.rejectSession(
|
||||
params = Wallet.Params.SessionReject(
|
||||
sessionProposal?.proposerPublicKey ?: "",
|
||||
"",
|
||||
),
|
||||
onSuccess = {
|
||||
Timber.d("Rejected successfully: $it")
|
||||
},
|
||||
onError = {
|
||||
Timber.d("Error while rejecting: $it")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun disconnect(topic: String) {
|
||||
Web3Wallet.disconnectSession(
|
||||
params = Wallet.Params.SessionDisconnect(topic),
|
||||
onSuccess = {
|
||||
updateSessions()
|
||||
Timber.d("Disconnected successfully: $it")
|
||||
},
|
||||
onError = {
|
||||
Timber.d("Error while disconnecting: $it")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun send(topic: String, id: Long, data: String) {
|
||||
Web3Wallet.respondSessionRequest(
|
||||
params = Wallet.Params.SessionRequestResponse(
|
||||
sessionTopic = topic,
|
||||
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
|
||||
id = id,
|
||||
result = data,
|
||||
),
|
||||
),
|
||||
onError = {},
|
||||
onSuccess = {},
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateSessions() {
|
||||
scope.launch {
|
||||
val availableSessions = Web3Wallet.getListOfActiveSessions()
|
||||
.map {
|
||||
WalletConnectSession(
|
||||
topic = it.topic,
|
||||
icon = it.metaData?.icons?.firstOrNull(),
|
||||
name = it.metaData?.name,
|
||||
url = it.metaData?.url,
|
||||
)
|
||||
}
|
||||
Timber.d("Available sessions: $availableSessions")
|
||||
_activeSessions.emit(availableSessions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findMissingNetworks(
|
||||
namespaces: Map<String, Wallet.Model.Namespace.Proposal>,
|
||||
userNamespaces: Map<NetworkNamespace, List<Account>>,
|
||||
): Collection<String> {
|
||||
val requiredChains = namespaces.values.flatMap { it.chains ?: emptyList() }
|
||||
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
|
||||
return requiredChains.subtract(userChains.toSet())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.tap.domain.walletconnect2.data
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.datasource.di.SdkMoshi
|
||||
import com.tangem.datasource.files.FileReader
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.Session
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletConnectSessionsRepositoryImpl @Inject constructor(
|
||||
@SdkMoshi private val moshi: Moshi,
|
||||
private val fileReader: FileReader,
|
||||
) :
|
||||
WalletConnectSessionsRepository {
|
||||
|
||||
private val sessionsAdapter: JsonAdapter<List<Session>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, Session::class.java),
|
||||
)
|
||||
|
||||
override suspend fun loadSessions(userWallet: String): List<Session> {
|
||||
return try {
|
||||
val fileContent = fileReader.readFile(getFileNameForUserWallet(userWallet))
|
||||
sessionsAdapter.fromJson(fileContent) ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveSession(userWallet: String, session: Session) {
|
||||
val updatedList = loadSessions(userWallet).plus(session)
|
||||
writeSessionToFile(sessions = updatedList, userWallet = userWallet)
|
||||
}
|
||||
|
||||
override suspend fun removeSession(userWallet: String, topic: String) {
|
||||
val updatedList = loadSessions(userWallet).filterNot { it.topic == topic }
|
||||
writeSessionToFile(sessions = updatedList, userWallet = userWallet)
|
||||
}
|
||||
|
||||
private fun writeSessionToFile(sessions: List<Session>, userWallet: String) {
|
||||
val serialized = sessionsAdapter.toJson(sessions)
|
||||
try {
|
||||
fileReader.rewriteFile(serialized, getFileNameForUserWallet(userWallet))
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FILE_NAME_PREFIX = "wc_2"
|
||||
|
||||
private fun getFileNameForUserWallet(userWallet: String): String {
|
||||
return "$FILE_NAME_PREFIX-${userWallet.uppercase()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.tap.domain.walletconnect2.di
|
||||
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
|
||||
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
|
||||
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
|
||||
import com.tangem.tap.domain.walletconnect2.data.WalletConnectRepositoryImpl
|
||||
import com.tangem.tap.domain.walletconnect2.data.WalletConnectSessionsRepositoryImpl
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
class WalletConnectInteractorModule {
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideWalletConnectInteractor(
|
||||
wcRepository: WalletConnectRepository,
|
||||
wcSessionsRepository: WalletConnectSessionsRepository,
|
||||
): WalletConnectInteractor {
|
||||
return WalletConnectInteractor(
|
||||
handler = WalletConnectEventsHandlerImpl(),
|
||||
walletConnectRepository = wcRepository,
|
||||
sessionsRepository = wcSessionsRepository,
|
||||
sdkHelper = WalletConnectSdkHelper(),
|
||||
blockchainHelper = TangemWcBlockchainHelper(),
|
||||
dispatcher = AppCoroutineDispatcherProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface WalletConnectModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindWalletConnectRepository(repository: WalletConnectRepositoryImpl): WalletConnectRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindWalletConnectSessionsRepository(
|
||||
repository: WalletConnectSessionsRepositoryImpl,
|
||||
): WalletConnectSessionsRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
|
||||
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
|
||||
|
||||
interface WalletConnectEventsHandler {
|
||||
fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String)
|
||||
|
||||
fun onSessionEstablished()
|
||||
|
||||
fun onSessionRejected(error: WalletConnectError)
|
||||
|
||||
fun onListOfSessionsUpdated(sessions: List<WcSessionForScreen>)
|
||||
|
||||
fun onSessionRequest(request: WcPreparedRequest)
|
||||
|
||||
fun onUnsupportedRequest()
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.domain.walletconnect.*
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.*
|
||||
import com.tangem.tap.features.details.ui.walletconnect.*
|
||||
import com.tangem.utils.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.*
|
||||
|
||||
class WalletConnectInteractor(
|
||||
private val handler: WalletConnectEventsHandler,
|
||||
private val walletConnectRepository: WalletConnectRepository,
|
||||
private val sessionsRepository: WalletConnectSessionsRepository,
|
||||
private val sdkHelper: WalletConnectSdkHelper,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
val blockchainHelper: WcBlockchainHelper,
|
||||
) {
|
||||
|
||||
private val events = walletConnectRepository.events
|
||||
private val sessions = walletConnectRepository.activeSessions
|
||||
|
||||
private var userWalletId: String = ""
|
||||
private var cardId: String? = null
|
||||
|
||||
private var currentRequest: WalletConnectEvents.SessionRequest? = null
|
||||
private val sessionRequestConverter = WcSessionRequestConverter(
|
||||
blockchainHelper = blockchainHelper,
|
||||
sessionsRepository = sessionsRepository,
|
||||
sdkHelper = sdkHelper,
|
||||
)
|
||||
|
||||
suspend fun startListening(userWalletId: String, cardId: String?) {
|
||||
this.userWalletId = userWalletId
|
||||
this.cardId = cardId
|
||||
coroutineScope {
|
||||
launch { subscribeToEvents() }
|
||||
launch { subscribeToSessions() }
|
||||
}
|
||||
walletConnectRepository.updateSessions()
|
||||
}
|
||||
|
||||
private suspend fun subscribeToEvents() {
|
||||
events
|
||||
.onEach { wcEvent ->
|
||||
when (wcEvent) {
|
||||
is WalletConnectEvents.SessionProposal -> {
|
||||
Timber.d("WC session proposal event received")
|
||||
val networksFormatted = wcEvent.chainIds
|
||||
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
|
||||
.toString()
|
||||
handler.onProposalReceived(proposal = wcEvent, networksFormatted = networksFormatted)
|
||||
}
|
||||
is WalletConnectEvents.SessionApprovalError -> {
|
||||
val error = when (wcEvent.error) {
|
||||
is WalletConnectError.ApprovalErrorMissingNetworks -> {
|
||||
val missingNetworks = wcEvent.error.missingChains
|
||||
.map { blockchainHelper.chainIdToNetworkIdOrNull(it) }
|
||||
val containsUnsupportedNetworks = missingNetworks.any { it == null }
|
||||
if (containsUnsupportedNetworks) {
|
||||
WalletConnectError.ApprovalErrorUnsupportedNetwork
|
||||
} else {
|
||||
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
|
||||
}
|
||||
}
|
||||
else -> wcEvent.error
|
||||
}
|
||||
handler.onSessionRejected(error)
|
||||
}
|
||||
is WalletConnectEvents.SessionApprovalSuccess -> {
|
||||
sessionsRepository.saveSession(
|
||||
userWallet = userWalletId,
|
||||
session = Session.fromAccounts(
|
||||
accounts = wcEvent.accounts,
|
||||
topic = wcEvent.topic,
|
||||
),
|
||||
)
|
||||
handler.onSessionEstablished()
|
||||
}
|
||||
is WalletConnectEvents.SessionDeleted -> {
|
||||
sessionsRepository.removeSession(userWalletId, wcEvent.topic)
|
||||
}
|
||||
is WalletConnectEvents.SessionRequest -> {
|
||||
handleRequest(wcEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(dispatcher.io)
|
||||
.collect()
|
||||
}
|
||||
|
||||
private suspend fun subscribeToSessions() {
|
||||
sessions
|
||||
.onEach { listOfSessions ->
|
||||
val relevantTopics = getTopicsForUserWallet(userWalletId, sessionsRepository)
|
||||
val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics)
|
||||
handler.onListOfSessionsUpdated(filteredSessions)
|
||||
}
|
||||
.flowOn(dispatcher.io)
|
||||
.collect()
|
||||
}
|
||||
|
||||
private fun filterSessionsForUserWallet(
|
||||
availableSessions: List<WalletConnectSession>,
|
||||
relevantTopics: List<String>,
|
||||
): List<WcSessionForScreen> {
|
||||
return availableSessions.filter { relevantTopics.contains(it.topic) }
|
||||
.map {
|
||||
WcSessionForScreen(
|
||||
description = it.name ?: "",
|
||||
sessionId = it.topic,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTopicsForUserWallet(
|
||||
userWalletId: String,
|
||||
repository: WalletConnectSessionsRepository,
|
||||
): List<String> {
|
||||
return repository.loadSessions(userWalletId).map { it.topic }
|
||||
}
|
||||
|
||||
fun approveSessionProposal(accounts: List<Account>) {
|
||||
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
|
||||
.groupBy { account ->
|
||||
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
|
||||
?.let { NetworkNamespace(it) }
|
||||
}.filterNotNull()
|
||||
walletConnectRepository.approve(
|
||||
userNamespaces = userNamespaces,
|
||||
)
|
||||
}
|
||||
|
||||
fun rejectSessionProposal() {
|
||||
walletConnectRepository.reject()
|
||||
}
|
||||
|
||||
fun disconnectSession(topic: String) {
|
||||
walletConnectRepository.disconnect(topic)
|
||||
}
|
||||
|
||||
fun rejectRequest(topic: String, id: Long) {
|
||||
walletConnectRepository.rejectRequest(topic, id)
|
||||
}
|
||||
|
||||
private suspend fun handleRequest(sessionRequest: WalletConnectEvents.SessionRequest) {
|
||||
val error: WalletConnectError? = when {
|
||||
sessionsRepository.loadSessions(userWalletId).none { it.topic == sessionRequest.topic } -> {
|
||||
WalletConnectError.WrongUserWallet
|
||||
}
|
||||
sessionRequest.request is WcRequest.CustomRequest -> {
|
||||
WalletConnectError.UnsupportedMethod
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (error != null) {
|
||||
walletConnectRepository.rejectRequest(sessionRequest.topic, sessionRequest.id)
|
||||
return
|
||||
}
|
||||
|
||||
when (sessionRequest.request) {
|
||||
is WcRequest.BnbCancel -> Unit
|
||||
is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest(
|
||||
topic = sessionRequest.topic,
|
||||
id = sessionRequest.id,
|
||||
result = "",
|
||||
)
|
||||
else -> {
|
||||
currentRequest = sessionRequest
|
||||
val data = prepareRequestData(sessionRequest)
|
||||
if (data != null) {
|
||||
handler.onSessionRequest(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun continueWithRequest(request: WcPreparedRequest) {
|
||||
val currentRequest = this.currentRequest
|
||||
if (currentRequest == null || request.topic != currentRequest.topic) return
|
||||
|
||||
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId ?: "") ?: return
|
||||
val signedHash = when (request) {
|
||||
is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction(
|
||||
data = request.preparedRequestData.data.data,
|
||||
networkId = networkId,
|
||||
derivationPath = request.derivationPath,
|
||||
cardId = cardId,
|
||||
)
|
||||
is WcPreparedRequest.EthTransaction -> sdkHelper.completeTransaction(
|
||||
data = request.preparedRequestData,
|
||||
cardId = cardId,
|
||||
)
|
||||
is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage(
|
||||
hashToSign = request.preparedRequestData.hash,
|
||||
networkId = networkId,
|
||||
derivationPath = request.derivationPath,
|
||||
cardId = cardId,
|
||||
)
|
||||
}
|
||||
|
||||
Timber.d("Signed hash: $signedHash")
|
||||
|
||||
if (signedHash == null) {
|
||||
walletConnectRepository.rejectRequest(
|
||||
topic = request.topic,
|
||||
id = request.requestId,
|
||||
)
|
||||
} else {
|
||||
walletConnectRepository.sendRequest(
|
||||
topic = request.topic,
|
||||
id = request.requestId,
|
||||
result = signedHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareRequestData(sessionRequest: WalletConnectEvents.SessionRequest): WcPreparedRequest? {
|
||||
return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.Account
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.NetworkNamespace
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectSession
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletConnectRepository {
|
||||
|
||||
val events: Flow<WalletConnectEvents>
|
||||
|
||||
val activeSessions: Flow<List<WalletConnectSession>>
|
||||
|
||||
fun init(projectId: String)
|
||||
|
||||
fun updateSessions()
|
||||
|
||||
fun pair(uri: String)
|
||||
|
||||
fun disconnect(topic: String)
|
||||
|
||||
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>)
|
||||
|
||||
fun reject()
|
||||
|
||||
fun sendRequest(topic: String, id: Long, result: String)
|
||||
|
||||
fun rejectRequest(topic: String, id: Long)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.Session
|
||||
|
||||
interface WalletConnectSessionsRepository {
|
||||
suspend fun loadSessions(userWallet: String): List<Session>
|
||||
|
||||
suspend fun saveSession(userWallet: String, session: Session)
|
||||
|
||||
suspend fun removeSession(userWallet: String, topic: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
interface WcBlockchainHelper {
|
||||
fun chainIdToNetworkIdOrNull(chainId: String): String?
|
||||
|
||||
fun networkIdToChainIdOrNull(networkId: String): String?
|
||||
|
||||
fun getNamespaceFromFullChainIdOrNull(chainId: String): String?
|
||||
|
||||
fun chainIdToFullNameOrNull(chainId: String): String?
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.datasource.di.SdkMoshi
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
enum class WcJrpcMethods(val code: String) {
|
||||
|
||||
ETH_SIGN("eth_sign"),
|
||||
ETH_PERSONAL_SIGN("personal_sign"),
|
||||
ETH_SIGN_TYPE_DATA("eth_signTypedData"),
|
||||
ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"),
|
||||
ETH_SIGN_TRANSACTION("eth_signTransaction"),
|
||||
ETH_SEND_TRANSACTION("eth_sendTransaction"),
|
||||
BNB_SIGN("bnb_sign"),
|
||||
BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"),
|
||||
SIGN_TRANSACTION("trust_signTransaction"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String): WcJrpcMethods? = values().firstOrNull { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WCSignTransaction(
|
||||
val network: Int,
|
||||
val transaction: String,
|
||||
) : WcRequestData
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WcEthereumSignMessage(
|
||||
val raw: List<String>,
|
||||
val type: WCSignType,
|
||||
) : WcRequestData {
|
||||
enum class WCSignType {
|
||||
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw parameters will always be the message and the address. Depending on the WCSignType,
|
||||
* those parameters can be swapped as description below:
|
||||
*
|
||||
* - MESSAGE: `[address, data ]`
|
||||
* - TYPED_MESSAGE: `[address, data]`
|
||||
* - PERSONAL_MESSAGE: `[data, address]`
|
||||
*
|
||||
* reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata
|
||||
*/
|
||||
val data
|
||||
get() = when (type) {
|
||||
WCSignType.PERSONAL_MESSAGE -> raw[0]
|
||||
else -> raw[1]
|
||||
}
|
||||
|
||||
val address
|
||||
get() = when (type) {
|
||||
WCSignType.PERSONAL_MESSAGE -> raw[1]
|
||||
else -> raw[0]
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WcEthereumTransaction(
|
||||
val from: String,
|
||||
val to: String?,
|
||||
val nonce: String?,
|
||||
val gasPrice: String?,
|
||||
val maxFeePerGas: String?,
|
||||
val maxPriorityFeePerGas: String?,
|
||||
val gas: String?,
|
||||
val gasLimit: String?,
|
||||
val value: String?,
|
||||
val data: String,
|
||||
) : WcRequestData
|
||||
|
||||
interface WcRequestData
|
||||
|
||||
data class WcCustomRequestData(val data: String) : WcRequestData
|
||||
|
||||
sealed class WcRequest(open val data: WcRequestData) {
|
||||
data class EthSign(override val data: WcEthereumSignMessage) : WcRequest(data)
|
||||
data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
|
||||
data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
|
||||
data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data)
|
||||
data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data)
|
||||
data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data)
|
||||
data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data)
|
||||
data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data)
|
||||
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
|
||||
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
fun deserialize(method: String, params: String): WcRequest {
|
||||
val customRequest = WcRequest.CustomRequest(WcCustomRequestData(params))
|
||||
val wcMethod: WcJrpcMethods = WcJrpcMethods.fromCode(method) ?: return customRequest
|
||||
|
||||
return when (wcMethod) {
|
||||
WcJrpcMethods.ETH_SIGN_TRANSACTION -> {
|
||||
val deserializedParams = moshi.adapter<List<WcEthereumTransaction>>(
|
||||
Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java),
|
||||
).fromJsonFirstOrNull(params) ?: return customRequest
|
||||
WcRequest.EthSignTransaction(data = deserializedParams)
|
||||
}
|
||||
WcJrpcMethods.ETH_SEND_TRANSACTION -> {
|
||||
val deserializedParams = moshi.adapter<List<WcEthereumTransaction>>(
|
||||
Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java),
|
||||
).fromJsonFirstOrNull(params) ?: return customRequest
|
||||
WcRequest.EthSendTransaction(data = deserializedParams)
|
||||
}
|
||||
WcJrpcMethods.ETH_SIGN -> {
|
||||
val deserializedParams = moshi.adapter<List<String>>(
|
||||
Types.newParameterizedType(List::class.java, String::class.java),
|
||||
).fromJsonOrNull(params) ?: return customRequest
|
||||
val data = WcEthereumSignMessage(
|
||||
raw = deserializedParams,
|
||||
type = WcEthereumSignMessage.WCSignType.MESSAGE,
|
||||
)
|
||||
WcRequest.EthSign(data = data)
|
||||
}
|
||||
WcJrpcMethods.ETH_PERSONAL_SIGN -> {
|
||||
val deserializedParams = moshi.adapter<List<String>>(
|
||||
Types.newParameterizedType(List::class.java, String::class.java),
|
||||
).fromJsonOrNull(params) ?: return customRequest
|
||||
val data = WcEthereumSignMessage(
|
||||
raw = deserializedParams,
|
||||
type = WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
|
||||
)
|
||||
WcRequest.EthSign(data = data)
|
||||
}
|
||||
WcJrpcMethods.ETH_SIGN_TYPE_DATA, WcJrpcMethods.ETH_SIGN_TYPE_DATA_V4 -> {
|
||||
val deserializedParams = listOf(
|
||||
params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)),
|
||||
params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1),
|
||||
)
|
||||
val data = WcEthereumSignMessage(
|
||||
deserializedParams,
|
||||
WcEthereumSignMessage.WCSignType.TYPED_MESSAGE,
|
||||
)
|
||||
Timber.d("TypedData params: $deserializedParams")
|
||||
WcRequest.EthSign(data)
|
||||
}
|
||||
WcJrpcMethods.BNB_SIGN -> {
|
||||
return deserializeBnb(moshi, params) ?: customRequest
|
||||
}
|
||||
WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> {
|
||||
val deserializedParams = moshi.adapter<List<WCBinanceTxConfirmParam>>(
|
||||
Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java),
|
||||
).fromJsonFirstOrNull(params) ?: return customRequest
|
||||
WcRequest.BnbTxConfirm(data = deserializedParams)
|
||||
}
|
||||
WcJrpcMethods.SIGN_TRANSACTION -> {
|
||||
val deserializedParams = moshi.adapter<List<WCSignTransaction>>(
|
||||
Types.newParameterizedType(List::class.java, WCSignTransaction::class.java),
|
||||
).fromJsonFirstOrNull(params) ?: return customRequest
|
||||
WcRequest.SignTransaction(data = deserializedParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deserializeBnb(moshi: Moshi, params: String): WcRequest? {
|
||||
val cancelOrder = moshi.adapter<List<WcBinanceCancelOrder>>(
|
||||
Types.newParameterizedType(List::class.java, WcBinanceCancelOrder::class.java),
|
||||
).fromJsonFirstOrNull(params)
|
||||
if (cancelOrder != null) return WcRequest.BnbCancel(cancelOrder)
|
||||
|
||||
val tradeOrder = moshi.adapter<List<WcBinanceTradeOrder>>(
|
||||
Types.newParameterizedType(List::class.java, WcBinanceTradeOrder::class.java),
|
||||
).fromJsonFirstOrNull(params)
|
||||
if (tradeOrder != null) return WcRequest.BnbTrade(tradeOrder)
|
||||
|
||||
val transferOrder = moshi.adapter<List<WcBinanceTransferOrder>>(
|
||||
Types.newParameterizedType(List::class.java, WcBinanceTransferOrder::class.java),
|
||||
).fromJsonFirstOrNull(params)
|
||||
if (transferOrder != null) return WcRequest.BnbTransfer(transferOrder)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun <T> JsonAdapter<T>.fromJsonOrNull(data: String): T? {
|
||||
return try {
|
||||
this.fromJson(data)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> JsonAdapter<List<T>>.fromJsonFirstOrNull(data: String): T? {
|
||||
return try {
|
||||
this.fromJson(data)?.firstOrNull()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
|
||||
|
||||
sealed class WcPreparedRequest(
|
||||
open val preparedRequestData: Any,
|
||||
val topic: String,
|
||||
val requestId: Long,
|
||||
val derivationPath: String?,
|
||||
) {
|
||||
class EthSign(
|
||||
override val preparedRequestData: WcPersonalSignData,
|
||||
topic: String,
|
||||
requestId: Long,
|
||||
derivationPath: String?,
|
||||
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
|
||||
|
||||
class EthTransaction(
|
||||
override val preparedRequestData: WcTransactionData,
|
||||
topic: String,
|
||||
requestId: Long,
|
||||
derivationPath: String?,
|
||||
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
|
||||
|
||||
class BnbTransaction(
|
||||
override val preparedRequestData: BnbData,
|
||||
topic: String,
|
||||
requestId: Long,
|
||||
derivationPath: String?,
|
||||
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain
|
||||
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
|
||||
|
||||
class WcSessionRequestConverter(
|
||||
private val blockchainHelper: WcBlockchainHelper,
|
||||
private val sessionsRepository: WalletConnectSessionsRepository,
|
||||
private val sdkHelper: WalletConnectSdkHelper,
|
||||
) {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
suspend fun prepareRequest(
|
||||
sessionRequest: WalletConnectEvents.SessionRequest,
|
||||
userWalletId: String,
|
||||
): WcPreparedRequest? {
|
||||
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId ?: "") ?: return null
|
||||
val derivationPath = getDerivationPath(
|
||||
sessionsRepository = sessionsRepository,
|
||||
sessionRequest = sessionRequest,
|
||||
userWalletId = userWalletId,
|
||||
walletAddress = getWalletAddress(sessionRequest.request),
|
||||
)
|
||||
return when (val request = sessionRequest.request) {
|
||||
is WcRequest.EthSendTransaction -> {
|
||||
val data = sdkHelper.prepareTransactionData(
|
||||
EthTransactionData(
|
||||
transaction = request.data,
|
||||
networkId = networkId,
|
||||
rawDerivationPath = derivationPath,
|
||||
id = sessionRequest.id,
|
||||
topic = sessionRequest.topic,
|
||||
type = WcEthTransactionType.EthSendTransaction,
|
||||
metaName = sessionRequest.metaName,
|
||||
metaUrl = sessionRequest.metaUrl,
|
||||
),
|
||||
) ?: return null
|
||||
WcPreparedRequest.EthTransaction(
|
||||
preparedRequestData = data,
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
is WcRequest.EthSignTransaction -> {
|
||||
val data = sdkHelper.prepareTransactionData(
|
||||
EthTransactionData(
|
||||
transaction = request.data,
|
||||
networkId = networkId,
|
||||
rawDerivationPath = derivationPath,
|
||||
id = sessionRequest.id,
|
||||
topic = sessionRequest.topic,
|
||||
type = WcEthTransactionType.EthSignTransaction,
|
||||
metaName = sessionRequest.metaName,
|
||||
metaUrl = sessionRequest.metaUrl,
|
||||
),
|
||||
) ?: return null
|
||||
WcPreparedRequest.EthTransaction(
|
||||
preparedRequestData = data,
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
is WcRequest.EthSign -> {
|
||||
val data = sdkHelper.prepareDataForPersonalSign(
|
||||
request.data,
|
||||
sessionRequest.topic,
|
||||
sessionRequest.metaName,
|
||||
sessionRequest.id,
|
||||
)
|
||||
WcPreparedRequest.EthSign(
|
||||
preparedRequestData = data,
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
is WcRequest.BnbTrade -> {
|
||||
val data = sdkHelper.prepareBnbTradeOrder(request.data)
|
||||
WcPreparedRequest.BnbTransaction(
|
||||
BnbData(
|
||||
data = data,
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
dAppName = sessionRequest.metaName,
|
||||
),
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
is WcRequest.BnbTransfer -> {
|
||||
val data = sdkHelper.prepareBnbTransferOrder(request.data)
|
||||
WcPreparedRequest.BnbTransaction(
|
||||
BnbData(
|
||||
data = data,
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
dAppName = sessionRequest.metaName,
|
||||
),
|
||||
topic = sessionRequest.topic,
|
||||
requestId = sessionRequest.id,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWalletAddress(request: WcRequest): String? {
|
||||
return when (request) {
|
||||
is WcRequest.BnbTrade -> request.data.accountNumber
|
||||
is WcRequest.BnbTransfer -> request.data.accountNumber
|
||||
is WcRequest.EthSendTransaction -> request.data.from
|
||||
is WcRequest.EthSignTransaction -> request.data.from
|
||||
is WcRequest.EthSign -> request.data.address
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getDerivationPath(
|
||||
sessionsRepository: WalletConnectSessionsRepository,
|
||||
sessionRequest: WalletConnectEvents.SessionRequest,
|
||||
userWalletId: String,
|
||||
walletAddress: String?,
|
||||
): String? {
|
||||
return sessionsRepository.loadSessions(userWalletId)
|
||||
.firstOrNull { it.topic == sessionRequest.topic }
|
||||
?.accounts?.firstOrNull { it.chainId == sessionRequest.chainId && it.walletAddress == walletAddress }
|
||||
?.derivationPath
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
|
||||
|
||||
data class BnbData(
|
||||
val data: BinanceMessageData,
|
||||
val topic: String,
|
||||
val requestId: Long,
|
||||
val dAppName: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
data class ChainWithDerivation(
|
||||
val chain: String,
|
||||
val derivationPath: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
|
||||
|
||||
data class EthTransactionData(
|
||||
val transaction: WcEthereumTransaction,
|
||||
val networkId: String,
|
||||
val rawDerivationPath: String?,
|
||||
val id: Long,
|
||||
val topic: String,
|
||||
val type: WcEthTransactionType,
|
||||
val metaName: String,
|
||||
val metaUrl: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
@JvmInline
|
||||
value class NetworkNamespace(val value: String)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
data class Session(
|
||||
val topic: String,
|
||||
val accounts: List<Account>,
|
||||
) {
|
||||
companion object {
|
||||
fun fromAccounts(accounts: List<Account>, topic: String): Session {
|
||||
return Session(
|
||||
topic = topic,
|
||||
accounts = accounts,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
sealed class WalletConnectError : Exception() {
|
||||
data class ApprovalErrorMissingNetworks(val missingChains: List<String>) : WalletConnectError()
|
||||
data class ApprovalErrorAddNetwork(val networks: List<String>) : WalletConnectError()
|
||||
object ApprovalErrorUnsupportedNetwork : WalletConnectError()
|
||||
data class ExternalApprovalError(override val message: String?) : WalletConnectError()
|
||||
object WrongUserWallet : WalletConnectError()
|
||||
object UnsupportedMethod : WalletConnectError()
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
|
||||
import java.net.URI
|
||||
|
||||
sealed interface WalletConnectEvents {
|
||||
data class SessionProposal(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val url: String,
|
||||
val icons: List<URI>,
|
||||
val chainIds: List<String>,
|
||||
) : WalletConnectEvents
|
||||
|
||||
data class SessionApprovalError(val error: WalletConnectError) : WalletConnectEvents
|
||||
data class SessionApprovalSuccess(val topic: String, val accounts: List<Account>) : WalletConnectEvents
|
||||
data class SessionDeleted(val topic: String) : WalletConnectEvents
|
||||
|
||||
data class SessionRequest(
|
||||
val request: WcRequest,
|
||||
val chainId: String?,
|
||||
val topic: String,
|
||||
val id: Long,
|
||||
val metaName: String,
|
||||
val metaUrl: String,
|
||||
) : WalletConnectEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
data class WalletConnectSession(
|
||||
val topic: String,
|
||||
val icon: String?,
|
||||
val name: String?,
|
||||
val url: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
import com.squareup.moshi.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class WcBinanceCancelOrder(
|
||||
@Json(name = "account_number")
|
||||
accountNumber: String,
|
||||
@Json(name = "chain_id")
|
||||
chainId: String,
|
||||
@Json(name = "data")
|
||||
data: String?,
|
||||
@Json(name = "memo")
|
||||
memo: String?,
|
||||
@Json(name = "sequence")
|
||||
sequence: String,
|
||||
@Json(name = "source")
|
||||
source: String,
|
||||
@Json(name = "msgs")
|
||||
msgs: List<Message>,
|
||||
) : WcBinanceOrder<WcBinanceCancelOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
|
||||
|
||||
enum class MessageKey(val key: String) {
|
||||
REFID("refid"),
|
||||
SENDER("sender"),
|
||||
SYMBOL("symbol"),
|
||||
}
|
||||
|
||||
data class Message(
|
||||
val refid: String,
|
||||
val sender: String,
|
||||
val symbol: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
open class WcBinanceOrder<T>(
|
||||
val accountNumber: String,
|
||||
val chainId: String,
|
||||
val data: String?,
|
||||
val memo: String?,
|
||||
val sequence: String,
|
||||
val source: String,
|
||||
val msgs: List<T>,
|
||||
) : WcRequestData
|
||||
|
||||
data class WCBinanceTxConfirmParam(
|
||||
val ok: Boolean,
|
||||
val errorMsg: String?,
|
||||
) : WcRequestData
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonObject
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class WcBinanceTradeOrder(
|
||||
@Json(name = "account_number")
|
||||
accountNumber: String,
|
||||
@Json(name = "chain_id")
|
||||
chainId: String,
|
||||
data: String?,
|
||||
memo: String?,
|
||||
sequence: String,
|
||||
source: String,
|
||||
msgs: List<Message>,
|
||||
) : WcBinanceOrder<WcBinanceTradeOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
|
||||
|
||||
enum class MessageKey(val key: String) {
|
||||
ID("id"),
|
||||
ORDER_TYPE("ordertype"),
|
||||
PRICE("price"),
|
||||
QUANTITY("quantity"),
|
||||
SENDER("sender"),
|
||||
SIDE("side"),
|
||||
SYMBOL("symbol"),
|
||||
TIME_INFORCE("timeinforce"),
|
||||
}
|
||||
|
||||
data class Message(
|
||||
val id: String,
|
||||
val orderType: Int,
|
||||
val price: Long,
|
||||
val quantity: Long,
|
||||
val sender: String,
|
||||
val side: Int,
|
||||
val symbol: String,
|
||||
val timeInforce: Int,
|
||||
)
|
||||
}
|
||||
|
||||
val tradeOrderDeserializer = jsonDeserializer {
|
||||
WcBinanceTradeOrder.Message(
|
||||
id = it.json[WcBinanceTradeOrder.MessageKey.ID.key].string,
|
||||
orderType = it.json[WcBinanceTradeOrder.MessageKey.ORDER_TYPE.key].int,
|
||||
price = it.json[WcBinanceTradeOrder.MessageKey.PRICE.key].long,
|
||||
quantity = it.json[WcBinanceTradeOrder.MessageKey.QUANTITY.key].long,
|
||||
sender = it.json[WcBinanceTradeOrder.MessageKey.SENDER.key].string,
|
||||
side = it.json[WcBinanceTradeOrder.MessageKey.SIDE.key].int,
|
||||
symbol = it.json[WcBinanceTradeOrder.MessageKey.SYMBOL.key].string,
|
||||
timeInforce = it.json[WcBinanceTradeOrder.MessageKey.TIME_INFORCE.key].int,
|
||||
)
|
||||
}
|
||||
|
||||
val tradeOrderSerializer = jsonSerializer<WcBinanceTradeOrder.Message> {
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.ID.key, it.src.id)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.ORDER_TYPE.key, it.src.orderType)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.PRICE.key, it.src.price)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.QUANTITY.key, it.src.quantity)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SENDER.key, it.src.sender)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SIDE.key, it.src.side)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SYMBOL.key, it.src.symbol)
|
||||
jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.TIME_INFORCE.key, it.src.timeInforce)
|
||||
|
||||
jsonObject
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
data class WcBinanceTradePair(val from: String, val to: String) {
|
||||
companion object {
|
||||
fun from(symbol: String): WcBinanceTradePair? {
|
||||
val pair = symbol.split("_")
|
||||
|
||||
return if (pair.size > 1) {
|
||||
val firstParts = pair[0].split("-")
|
||||
val secondParts = pair[1].split("-")
|
||||
WcBinanceTradePair(firstParts[0], secondParts[0])
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class WcBinanceTransferOrder(
|
||||
@Json(name = "account_number")
|
||||
accountNumber: String,
|
||||
@Json(name = "chain_id")
|
||||
chainId: String,
|
||||
data: String?,
|
||||
memo: String?,
|
||||
sequence: String,
|
||||
source: String,
|
||||
msgs: List<Message>,
|
||||
) : WcBinanceOrder<WcBinanceTransferOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
|
||||
|
||||
enum class MessageKey(val key: String) {
|
||||
INPUTS("inputs"),
|
||||
OUTPUTS("outputs"),
|
||||
}
|
||||
|
||||
data class Message(
|
||||
val inputs: List<Item>,
|
||||
val outputs: List<Item>,
|
||||
) {
|
||||
|
||||
data class Item(
|
||||
val address: String,
|
||||
val coins: List<Coin>,
|
||||
) {
|
||||
|
||||
data class Coin(
|
||||
val amount: Long,
|
||||
val denom: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,8 @@ internal data class AddCustomTokenChooseTokenBottomSheet(
|
|||
* @property tokenSymbolInputField input field to enter the token symbol
|
||||
* @property decimalsInputField input field to enter the token decimals
|
||||
* @property derivationPathSelectorField selector field to select the derivation path
|
||||
* @property derivationPathInputField input field for a custom derivation path
|
||||
* @property showTokenFields if token fields should be shown
|
||||
*/
|
||||
internal data class AddCustomTokenForm(
|
||||
val contractAddressInputField: AddCustomTokenInputField.ContactAddress,
|
||||
|
|
@ -75,6 +77,8 @@ internal data class AddCustomTokenForm(
|
|||
val tokenSymbolInputField: AddCustomTokenInputField.TokenSymbol,
|
||||
val decimalsInputField: AddCustomTokenInputField.Decimals,
|
||||
val derivationPathSelectorField: AddCustomTokenSelectorField.DerivationPath?,
|
||||
val derivationPathInputField: AddCustomTokenInputField.DerivationPath?,
|
||||
val showTokenFields: Boolean = false,
|
||||
)
|
||||
|
||||
/** Base input field model of add custom token screen */
|
||||
|
|
@ -174,6 +178,25 @@ internal sealed interface AddCustomTokenInputField {
|
|||
override val placeholder: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenInputField
|
||||
|
||||
/**
|
||||
* Input field model to enter a custom derivation path
|
||||
*
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property keyboardOptions keyboard options
|
||||
* @property label label
|
||||
* @property placeholder placeholder (hint)
|
||||
* @property showField whether the field should be shown
|
||||
*/
|
||||
data class DerivationPath(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val showField: Boolean = false,
|
||||
) : AddCustomTokenInputField
|
||||
}
|
||||
|
||||
/** Base selector field model of add custom token screen */
|
||||
|
|
@ -251,10 +274,15 @@ internal sealed interface AddCustomTokenSelectorField {
|
|||
override val title: TextReference,
|
||||
override val blockchain: Blockchain,
|
||||
val subtitle: TextReference,
|
||||
val type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN,
|
||||
) : SelectorItem
|
||||
}
|
||||
}
|
||||
|
||||
enum class DerivationPathSelectorType {
|
||||
DEFAULT, CUSTOM, BLOCKCHAIN
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning model of add custom token screen
|
||||
*
|
||||
|
|
@ -276,6 +304,10 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) {
|
|||
object UnsupportedSolanaToken : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message),
|
||||
)
|
||||
|
||||
object WrongDerivationPath : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.custom_token_invalid_derivation_path),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,14 +4,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.*
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -85,6 +78,7 @@ internal object AddCustomTokenPreviewData {
|
|||
onMenuItemClick = {},
|
||||
isEnabled = true,
|
||||
),
|
||||
derivationPathInputField = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import androidx.compose.foundation.layout.imePadding
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconLeft
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconStart
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -25,13 +24,13 @@ import com.tangem.wallet.R
|
|||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) {
|
||||
PrimaryButtonIconLeft(
|
||||
PrimaryButtonIconStart(
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_add),
|
||||
icon = painterResource(id = R.drawable.ic_plus_24),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = model.isEnabled,
|
||||
onClick = model.onClick,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,11 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ExposedDropdownMenuBox
|
||||
import androidx.compose.material.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -62,12 +42,13 @@ internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
|
|||
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
InputField(model = model.contractAddressInputField)
|
||||
if (model.showTokenFields) InputField(model = model.contractAddressInputField)
|
||||
SelectorField(model = model.networkSelectorField)
|
||||
InputField(model = model.tokenNameInputField)
|
||||
InputField(model = model.tokenSymbolInputField)
|
||||
InputField(model = model.decimalsInputField)
|
||||
if (model.showTokenFields) InputField(model = model.tokenNameInputField)
|
||||
if (model.showTokenFields) InputField(model = model.tokenSymbolInputField)
|
||||
if (model.showTokenFields) InputField(model = model.decimalsInputField)
|
||||
model.derivationPathSelectorField?.let { SelectorField(model = it) }
|
||||
if (model.derivationPathInputField?.showField == true) InputField(model = model.derivationPathInputField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +84,7 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
|
|||
is AddCustomTokenInputField.Decimals -> model.isEnabled
|
||||
is AddCustomTokenInputField.TokenName -> model.isEnabled
|
||||
is AddCustomTokenInputField.TokenSymbol -> model.isEnabled
|
||||
is AddCustomTokenInputField.DerivationPath -> true
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.blockchain.common.DerivationStyle
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.HDWalletError
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.*
|
||||
|
|
@ -135,18 +136,28 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
tokenSymbolInputField = createTokenSymbolInputField(),
|
||||
decimalsInputField = createDecimalsInputField(),
|
||||
derivationPathSelectorField = createDerivationPathsSelectorField(),
|
||||
derivationPathInputField = createDerivationPathInputField(),
|
||||
)
|
||||
}
|
||||
|
||||
fun createDerivationPathSelectorItem(blockchain: Blockchain): SelectorItem.TitleWithSubtitle {
|
||||
return if (blockchain == Blockchain.Unknown) {
|
||||
SelectorItem.TitleWithSubtitle(
|
||||
fun createDerivationPathSelectorAdditionalItem(
|
||||
blockchain: Blockchain,
|
||||
type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN,
|
||||
): SelectorItem.TitleWithSubtitle {
|
||||
return when (type) {
|
||||
DerivationPathSelectorType.DEFAULT -> SelectorItem.TitleWithSubtitle(
|
||||
title = TextReference.Res(R.string.custom_token_derivation_path_default),
|
||||
subtitle = TextReference.Res(R.string.custom_token_derivation_path_default),
|
||||
blockchain = Blockchain.Unknown,
|
||||
type = DerivationPathSelectorType.DEFAULT,
|
||||
)
|
||||
} else {
|
||||
SelectorItem.TitleWithSubtitle(
|
||||
DerivationPathSelectorType.CUSTOM -> SelectorItem.TitleWithSubtitle(
|
||||
title = TextReference.Res(R.string.custom_token_custom_derivation),
|
||||
subtitle = TextReference.Res(R.string.custom_token_custom_derivation),
|
||||
blockchain = Blockchain.Unknown,
|
||||
type = DerivationPathSelectorType.CUSTOM,
|
||||
)
|
||||
DerivationPathSelectorType.BLOCKCHAIN -> SelectorItem.TitleWithSubtitle(
|
||||
title = blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath?.let(TextReference::Str)
|
||||
?: TextReference.Res(R.string.custom_token_derivation_path_default),
|
||||
subtitle = TextReference.Str(blockchain.fullName),
|
||||
|
|
@ -175,7 +186,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
onValueChange = actionsHandler::onContactAddressValueChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
label = TextReference.Res(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000"),
|
||||
placeholder = TextReference.Str(value = CONTRACT_ADDRESS_PLACEHOLDER),
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
error = null,
|
||||
|
|
@ -196,8 +207,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown)
|
||||
return listOf(defaultNetwork) + Blockchain.values()
|
||||
.filter { blockchain ->
|
||||
(blockchain.isEvm() || blockchain.canHandleTokens()) &&
|
||||
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
|
||||
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
|
||||
}
|
||||
.sortedBy(Blockchain::fullName)
|
||||
.map(::createNetworkSelectorItem)
|
||||
|
|
@ -231,7 +241,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
onValueChange = actionsHandler::onDecimalsValueChange,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
|
||||
label = TextReference.Res(R.string.custom_token_decimals_input_title),
|
||||
placeholder = TextReference.Str(value = "8"),
|
||||
placeholder = TextReference.Str(value = DECIMALS_PLACEHOLDER),
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -250,11 +260,31 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getDerivationPathsSelectorItems(): List<SelectorItem.TitleWithSubtitle> {
|
||||
val defaultDerivationPath = createDerivationPathSelectorItem(Blockchain.Unknown)
|
||||
return listOf(defaultDerivationPath) + Blockchain.values()
|
||||
.filter { blockchain -> blockchain.isEvm() && blockchain.isSupportedInApp() && !blockchain.isTestnet() }
|
||||
return listOf(
|
||||
createDerivationPathSelectorAdditionalItem(
|
||||
blockchain = Blockchain.Unknown,
|
||||
type = DerivationPathSelectorType.DEFAULT,
|
||||
),
|
||||
createDerivationPathSelectorAdditionalItem(
|
||||
blockchain = Blockchain.Unknown,
|
||||
type = DerivationPathSelectorType.CUSTOM,
|
||||
),
|
||||
) + Blockchain.values()
|
||||
.filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() }
|
||||
.sortedBy(Blockchain::fullName)
|
||||
.map(::createDerivationPathSelectorItem)
|
||||
.map(::createDerivationPathSelectorAdditionalItem)
|
||||
}
|
||||
|
||||
private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? {
|
||||
if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null
|
||||
|
||||
return AddCustomTokenInputField.DerivationPath(
|
||||
value = "",
|
||||
onValueChange = actionsHandler::onDerivationPathValueChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
label = TextReference.Res(R.string.custom_token_custom_derivation),
|
||||
placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -306,25 +336,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
Timber.e(it)
|
||||
}
|
||||
|
||||
updateDerivationPathSelector()
|
||||
updateWarnings()
|
||||
updateFloatingButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDerivationPathSelector() {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathSelectorField = uiState.form.derivationPathSelectorField?.copy(
|
||||
isEnabled = uiState.form.networkSelectorField.selectedItem.blockchain.isEvm(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isDerivationPathSelected(): Boolean {
|
||||
return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
|
||||
this != null && this != Blockchain.Unknown
|
||||
this != null && this != Blockchain.Unknown ||
|
||||
uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM &&
|
||||
!uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -397,6 +418,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateFloatingButton() {
|
||||
uiState = updateStateWithDerivationError(uiState)
|
||||
if (isCustomTokenAlreadyAdded()) {
|
||||
uiState = uiState.copySealed(
|
||||
warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
|
|
@ -405,6 +427,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
val state = when {
|
||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
|
|
@ -418,7 +441,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(
|
||||
isEnabled = error is ContractAddressValidatorResult.Success && isSupportedToken,
|
||||
isEnabled = error is ContractAddressValidatorResult.Success &&
|
||||
isSupportedToken && isCorrectDerivationInput,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -430,17 +454,43 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
else -> {
|
||||
uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(
|
||||
isEnabled = if (isNetworkSelected()) !isBlockchainAlreadyAdded() else false,
|
||||
isEnabled = if (isNetworkSelected()) {
|
||||
!isBlockchainAlreadyAdded() && isCorrectDerivationInput
|
||||
} else {
|
||||
false
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
uiState = state.copySealed(
|
||||
warnings = uiState.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder {
|
||||
val updatedWarnings = if (isWrongDerivationPathEntered(
|
||||
derivationPathSelectorType = uiState.form.derivationPathSelectorField?.selectedItem?.type,
|
||||
derivationPath = getDerivationPath(),
|
||||
)
|
||||
) {
|
||||
uiState.warnings + AddCustomTokenWarning.WrongDerivationPath
|
||||
} else {
|
||||
uiState.warnings - AddCustomTokenWarning.WrongDerivationPath
|
||||
}
|
||||
return uiState.copySealed(
|
||||
warnings = updatedWarnings,
|
||||
)
|
||||
}
|
||||
|
||||
private fun isWrongDerivationPathEntered(
|
||||
derivationPathSelectorType: DerivationPathSelectorType?,
|
||||
derivationPath: DerivationPath?,
|
||||
): Boolean {
|
||||
return derivationPathSelectorType == DerivationPathSelectorType.CUSTOM && derivationPath == null
|
||||
}
|
||||
|
||||
private fun isCustomTokenAlreadyAdded(): Boolean {
|
||||
return when (getCustomTokenType()) {
|
||||
CustomTokenType.TOKEN -> isTokenAlreadyAdded()
|
||||
|
|
@ -520,16 +570,36 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getDerivationPath(): DerivationPath? {
|
||||
return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) {
|
||||
DerivationPathSelectorType.CUSTOM ->
|
||||
createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "")
|
||||
else ->
|
||||
getDerivationPathForBlockchain(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
|
||||
return try {
|
||||
DerivationPath(rawPath)
|
||||
} catch (error: HDWalletError) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? {
|
||||
if (blockchain == null) return null
|
||||
|
||||
val derivationStyle = if (!isDerivationPathSelected()) {
|
||||
reduxStateHolder.scanResponse?.card?.derivationStyle
|
||||
} else {
|
||||
DerivationStyle.LEGACY
|
||||
}
|
||||
|
||||
return when (val derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
|
||||
Blockchain.Unknown -> uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
else -> derivationNetwork
|
||||
}?.derivationPath(derivationStyle)
|
||||
val derivationNetwork = if (blockchain == Blockchain.Unknown) {
|
||||
uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
} else {
|
||||
blockchain
|
||||
}
|
||||
return derivationNetwork.derivationPath(derivationStyle)
|
||||
}
|
||||
|
||||
private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
|
||||
|
|
@ -566,7 +636,6 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
is ContractAddressValidatorResult.Error -> {
|
||||
handleContractAddressErrorValidation(type = validatorResult.type)
|
||||
updateDerivationPathSelector()
|
||||
updateWarnings()
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
|
@ -574,11 +643,13 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onNetworkSelectorItemClick(index: Int) {
|
||||
val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index))
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
||||
selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index)),
|
||||
selectedItem = selectedItem,
|
||||
),
|
||||
showTokenFields = selectedItem.blockchain.canHandleTokens(),
|
||||
),
|
||||
)
|
||||
onContactAddressValueChange(uiState.form.contractAddressInputField.value)
|
||||
|
|
@ -612,12 +683,26 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onDerivationPathSelectorItemClick(index: Int) {
|
||||
val field = requireNotNull(uiState.form.derivationPathSelectorField)
|
||||
val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField)
|
||||
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
|
||||
val derivationInputField = requireNotNull(uiState.form.derivationPathInputField)
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathSelectorField = field.copy(
|
||||
selectedItem = requireNotNull(field.items.getOrNull(index)),
|
||||
derivationPathSelectorField = derivationSelector.copy(
|
||||
selectedItem = selected,
|
||||
),
|
||||
derivationPathInputField = derivationInputField.copy(
|
||||
showField = selected.type == DerivationPathSelectorType.CUSTOM,
|
||||
),
|
||||
),
|
||||
)
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onDerivationPathValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
updateFloatingButton()
|
||||
|
|
@ -691,7 +776,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
|
||||
derivationPathSelectorField = derivationPathSelectorField?.copy(
|
||||
isEnabled = true,
|
||||
selectedItem = formStateBuilder.createDerivationPathSelectorItem(Blockchain.Unknown),
|
||||
selectedItem = formStateBuilder
|
||||
.createDerivationPathSelectorAdditionalItem(
|
||||
blockchain = Blockchain.Unknown,
|
||||
type = DerivationPathSelectorType.DEFAULT,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -722,5 +811,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
TestTokenItem(name = "USDT (invalid - 1/3 of address)", address = "Es9vMFrzaCERmJ"),
|
||||
TestTokenItem(name = "ETH (full)", address = "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk"),
|
||||
)
|
||||
const val DERIVATION_PATH_PLACEHOLDER = "m/44'/0'/0'/0/0"
|
||||
const val DECIMALS_PLACEHOLDER = "8"
|
||||
const val CONTRACT_ADDRESS_PLACEHOLDER = "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,21 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.BottomSheetScaffold
|
||||
import androidx.compose.material.BottomSheetScaffoldState
|
||||
import androidx.compose.material.BottomSheetState
|
||||
import androidx.compose.material.BottomSheetValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconLeft
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconStart
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ScreenState
|
||||
|
|
@ -189,12 +162,12 @@ fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
|
|||
|
||||
@Composable
|
||||
private fun AddButton(state: MutableState<AddCustomTokenState>) {
|
||||
PrimaryButtonIconLeft(
|
||||
PrimaryButtonIconStart(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_add),
|
||||
icon = painterResource(id = R.drawable.ic_plus_24),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = state.value.screenState.addButton.isEnabled,
|
||||
onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesRe
|
|||
}
|
||||
}
|
||||
val allowedSecurityOptions = when {
|
||||
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() || cardTypesResolver.isSaltPay() -> {
|
||||
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> {
|
||||
EnumSet.of(SecurityOption.LongTap)
|
||||
}
|
||||
card.settings.isBackupAllowed -> {
|
||||
|
|
@ -116,7 +116,7 @@ private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesRe
|
|||
|
||||
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
|
||||
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
|
||||
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isSaltPay() || cardTypesResolver.isStart2Coin()
|
||||
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
|
||||
return !isNotAllowed
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.walletconnect.Topic
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
|
||||
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
|
||||
import com.tangem.wallet.R
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
|
||||
|
|
@ -58,7 +62,7 @@ sealed class WalletConnectAction : Action {
|
|||
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :
|
||||
WalletConnectAction()
|
||||
|
||||
data class DisconnectSession(val session: WCSession) : WalletConnectAction()
|
||||
data class DisconnectSession(val topic: String, val session: WCSession?) : WalletConnectAction()
|
||||
|
||||
data class RemoveSession(val session: WCSession) : WalletConnectAction()
|
||||
|
||||
|
|
@ -66,7 +70,7 @@ sealed class WalletConnectAction : Action {
|
|||
val transaction: WCEthereumTransaction,
|
||||
val session: WalletConnectSession,
|
||||
val id: Long,
|
||||
val type: WcTransactionType,
|
||||
val type: WcEthTransactionType,
|
||||
) :
|
||||
WalletConnectAction()
|
||||
|
||||
|
|
@ -78,11 +82,11 @@ sealed class WalletConnectAction : Action {
|
|||
val id: Long,
|
||||
) : WalletConnectAction()
|
||||
|
||||
data class SendTransaction(val session: WCSession) : WalletConnectAction()
|
||||
data class SendTransaction(val topic: Topic) : WalletConnectAction()
|
||||
|
||||
data class SignMessage(val session: WCSession) : WalletConnectAction()
|
||||
data class SignMessage(val topic: Topic) : WalletConnectAction()
|
||||
|
||||
data class RejectRequest(val session: WCSession, val id: Long) : WalletConnectAction()
|
||||
data class RejectRequest(val topic: Topic, val id: Long) : WalletConnectAction()
|
||||
|
||||
object NotEnoughFunds : WalletConnectAction(), NotificationAction {
|
||||
override val messageResource = R.string.wallet_connect_create_tx_not_enough_funds
|
||||
|
|
@ -108,7 +112,22 @@ sealed class WalletConnectAction : Action {
|
|||
data class Sign(
|
||||
val id: Long,
|
||||
val data: ByteArray,
|
||||
val sessionData: WCSession,
|
||||
val topic: Topic,
|
||||
) : WalletConnectAction()
|
||||
}
|
||||
|
||||
//region WalletConnect 2.0
|
||||
object ApproveProposal : WalletConnectAction()
|
||||
object RejectProposal : WalletConnectAction()
|
||||
|
||||
object SessionEstablished : WalletConnectAction()
|
||||
data class SessionRejected(val error: WalletConnectError) : WalletConnectAction()
|
||||
data class SessionListUpdated(val sessions: List<WcSessionForScreen>) : WalletConnectAction()
|
||||
|
||||
data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
|
||||
|
||||
object RejectUnsupportedRequest : WalletConnectAction()
|
||||
|
||||
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
|
||||
//endregion WalletConnect 2.0
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import com.tangem.blockchain.common.DerivationStyle
|
|||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -19,8 +21,16 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
|||
import com.tangem.tap.domain.walletconnect.BnbHelper
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectManager
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
|
||||
import com.tangem.tap.domain.walletconnect.extensions.toWcEthTransaction
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.Account
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -29,8 +39,14 @@ import org.rekotlin.Action
|
|||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class WalletConnectMiddleware {
|
||||
private var walletConnectManager = WalletConnectManager()
|
||||
private val walletConnectInteractor: WalletConnectInteractor
|
||||
get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectInteractor)
|
||||
private val walletConnectRepository: WalletConnectRepository
|
||||
get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectRepository)
|
||||
|
||||
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
|
|
@ -115,9 +131,13 @@ class WalletConnectMiddleware {
|
|||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard))
|
||||
}
|
||||
is WalletConnectAction.OpenSession -> {
|
||||
walletConnectManager.connect(
|
||||
wcUri = action.wcUri,
|
||||
)
|
||||
val index = action.wcUri.indexOf("@")
|
||||
when (action.wcUri[index + 1]) {
|
||||
'1' -> {
|
||||
walletConnectManager.connect(wcUri = action.wcUri)
|
||||
}
|
||||
'2' -> walletConnectRepository.pair(uri = action.wcUri)
|
||||
}
|
||||
}
|
||||
is WalletConnectAction.RefuseOpeningSession -> {
|
||||
store.dispatch(
|
||||
|
|
@ -134,11 +154,15 @@ class WalletConnectMiddleware {
|
|||
walletConnectManager.approve(action.session)
|
||||
}
|
||||
is WalletConnectAction.DisconnectSession -> {
|
||||
walletConnectManager.disconnect(action.session)
|
||||
if (action.session != null) {
|
||||
walletConnectManager.disconnect(action.session)
|
||||
} else {
|
||||
walletConnectInteractor.disconnectSession(action.topic)
|
||||
}
|
||||
}
|
||||
is WalletConnectAction.HandleTransactionRequest -> {
|
||||
walletConnectManager.handleTransactionRequest(
|
||||
transaction = action.transaction,
|
||||
transaction = action.transaction.toWcEthTransaction(),
|
||||
session = action.session,
|
||||
id = action.id,
|
||||
type = action.type,
|
||||
|
|
@ -152,23 +176,31 @@ class WalletConnectMiddleware {
|
|||
)
|
||||
}
|
||||
is WalletConnectAction.RejectRequest -> {
|
||||
walletConnectManager.rejectRequest(action.session, action.id)
|
||||
walletConnectManager.rejectRequest(action.topic, action.id)
|
||||
walletConnectInteractor.rejectRequest(action.topic, action.id)
|
||||
}
|
||||
is WalletConnectAction.SendTransaction -> {
|
||||
walletConnectManager.completeTransaction(action.session)
|
||||
walletConnectManager.completeTransaction(action.topic)
|
||||
}
|
||||
is WalletConnectAction.SignMessage -> {
|
||||
walletConnectManager.sendSignedMessage(action.session)
|
||||
walletConnectManager.sendSignedMessage(action.topic)
|
||||
}
|
||||
is WalletConnectAction.BinanceTransaction.Trade -> {
|
||||
val messageData = BnbHelper.createMessageData(action.order)
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.BnbTransactionDialog(
|
||||
data = messageData,
|
||||
session = action.sessionData.session,
|
||||
sessionId = action.id,
|
||||
dAppName = action.sessionData.peerMeta.name,
|
||||
WcPreparedRequest.BnbTransaction(
|
||||
preparedRequestData = BnbData(
|
||||
data = messageData,
|
||||
topic = action.sessionData.session.topic,
|
||||
requestId = action.id,
|
||||
dAppName = action.sessionData.peerMeta.name,
|
||||
),
|
||||
topic = action.sessionData.session.topic,
|
||||
requestId = action.id,
|
||||
derivationPath = action.sessionData.wallet.derivationPath?.rawPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -178,10 +210,17 @@ class WalletConnectMiddleware {
|
|||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.BnbTransactionDialog(
|
||||
data = messageData,
|
||||
session = action.sessionData.session,
|
||||
sessionId = action.id,
|
||||
dAppName = action.sessionData.peerMeta.name,
|
||||
WcPreparedRequest.BnbTransaction(
|
||||
preparedRequestData = BnbData(
|
||||
data = messageData,
|
||||
topic = action.sessionData.session.topic,
|
||||
requestId = action.id,
|
||||
dAppName = action.sessionData.peerMeta.name,
|
||||
),
|
||||
topic = action.sessionData.session.topic,
|
||||
requestId = action.id,
|
||||
derivationPath = action.sessionData.wallet.derivationPath?.rawPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -190,7 +229,7 @@ class WalletConnectMiddleware {
|
|||
walletConnectManager.signBnb(
|
||||
id = action.id,
|
||||
data = action.data,
|
||||
sessionData = action.sessionData,
|
||||
topic = action.topic,
|
||||
)
|
||||
}
|
||||
is WalletConnectAction.SwitchBlockchain -> {
|
||||
|
|
@ -226,6 +265,70 @@ class WalletConnectMiddleware {
|
|||
is WalletConnectAction.UpdateBlockchain -> {
|
||||
walletConnectManager.updateBlockchain(action.updatedSession)
|
||||
}
|
||||
is WalletConnectAction.ApproveProposal -> {
|
||||
val accounts = store.state.walletState.walletManagers
|
||||
.mapNotNull {
|
||||
val wallet = it.wallet
|
||||
val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull(
|
||||
wallet.blockchain.toNetworkId(),
|
||||
)
|
||||
chainId?.let {
|
||||
Account(
|
||||
chainId,
|
||||
wallet.address,
|
||||
wallet.publicKey.derivationPath?.rawPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
walletConnectInteractor.approveSessionProposal(accounts)
|
||||
}
|
||||
is WalletConnectAction.RejectProposal -> {
|
||||
walletConnectInteractor.rejectSessionProposal()
|
||||
}
|
||||
is WalletConnectAction.SessionEstablished -> {
|
||||
}
|
||||
is WalletConnectAction.SessionRejected -> {
|
||||
when (action.error) {
|
||||
is WalletConnectError.ApprovalErrorAddNetwork -> {
|
||||
val blockchains = action.error.networks.mapNotNull { Blockchain.fromNetworkId(it)?.fullName }
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.AddNetwork(blockchains.toString()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
|
||||
}
|
||||
is WalletConnectError.ExternalApprovalError -> {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
AppDialog.SimpleOkWarningDialog(
|
||||
message = action.error.message ?: "",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
is WalletConnectAction.ShowSessionRequest -> {
|
||||
val dialog: WalletConnectDialog = when (val request = action.sessionRequest) {
|
||||
is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request)
|
||||
is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request)
|
||||
is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request)
|
||||
}
|
||||
store.dispatch(GlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
is WalletConnectAction.PerformRequestedAction -> {
|
||||
scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) }
|
||||
}
|
||||
is WalletConnectAction.RejectUnsupportedRequest -> {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,15 +27,23 @@ object WalletConnectReducer {
|
|||
state.sessions.filterNot { it.session.toUri() == action.session.toUri() }
|
||||
state.copy(sessions = sessions)
|
||||
}
|
||||
is WalletConnectAction.UnsupportedCard -> state.copy(loading = false)
|
||||
is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false)
|
||||
is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false)
|
||||
is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false)
|
||||
is WalletConnectAction.UnsupportedCard,
|
||||
is WalletConnectAction.RefuseOpeningSession,
|
||||
is WalletConnectAction.OpeningSessionTimeout,
|
||||
is WalletConnectAction.FailureEstablishingSession,
|
||||
-> state.copy(loading = false)
|
||||
is WalletConnectAction.UpdateBlockchain -> state.copy(
|
||||
sessions = state.sessions
|
||||
.filterNot { it.peerId == action.updatedSession.peerId } + action.updatedSession,
|
||||
)
|
||||
|
||||
is WalletConnectAction.ApproveProposal -> state.copy(loading = true)
|
||||
is WalletConnectAction.RejectProposal,
|
||||
is WalletConnectAction.SessionEstablished,
|
||||
is WalletConnectAction.SessionRejected,
|
||||
-> state.copy(loading = false)
|
||||
is WalletConnectAction.SessionListUpdated -> state.copy(
|
||||
wc2Sessions = action.sessions,
|
||||
)
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import com.tangem.blockchain.common.WalletManager
|
|||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
|
||||
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
|
||||
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
|
||||
|
|
@ -16,6 +20,7 @@ import com.trustwallet.walletconnect.models.session.WCSession
|
|||
data class WalletConnectState(
|
||||
val loading: Boolean = false,
|
||||
val sessions: List<WalletConnectSession> = listOf(),
|
||||
val wc2Sessions: List<WcSessionForScreen> = listOf(),
|
||||
val newSessionData: NewWcSessionData? = null,
|
||||
)
|
||||
|
||||
|
|
@ -95,43 +100,47 @@ sealed class WalletConnectDialog : StateDialog {
|
|||
val networks: List<Blockchain>,
|
||||
) : WalletConnectDialog()
|
||||
|
||||
data class SessionProposalDialog(
|
||||
val sessionProposal: WalletConnectEvents.SessionProposal,
|
||||
val networks: String,
|
||||
val onApprove: () -> Unit,
|
||||
val onReject: () -> Unit,
|
||||
) : WalletConnectDialog()
|
||||
|
||||
data class ChooseNetwork(
|
||||
val session: WalletConnectSession,
|
||||
val networks: List<Blockchain>,
|
||||
) : WalletConnectDialog()
|
||||
|
||||
data class RequestTransaction(val dialogData: TransactionRequestDialogData) :
|
||||
data class RequestTransaction(val data: WcPreparedRequest.EthTransaction) :
|
||||
WalletConnectDialog()
|
||||
|
||||
data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog()
|
||||
data class PersonalSign(val data: WcPreparedRequest.EthSign) : WalletConnectDialog()
|
||||
data class BnbTransactionDialog(
|
||||
val data: BinanceMessageData,
|
||||
val session: WCSession,
|
||||
val sessionId: Long,
|
||||
val dAppName: String,
|
||||
val data: WcPreparedRequest.BnbTransaction,
|
||||
) : WalletConnectDialog()
|
||||
}
|
||||
|
||||
data class WcTransactionData(
|
||||
val type: WcTransactionType,
|
||||
val type: WcEthTransactionType,
|
||||
val transaction: TransactionData,
|
||||
val session: WalletConnectSession,
|
||||
val topic: String,
|
||||
val id: Long,
|
||||
val walletManager: WalletManager,
|
||||
val dialogData: TransactionRequestDialogData,
|
||||
)
|
||||
|
||||
enum class WcTransactionType {
|
||||
enum class WcEthTransactionType {
|
||||
EthSignTransaction,
|
||||
EthSendTransaction,
|
||||
}
|
||||
|
||||
data class WcPersonalSignData(
|
||||
val hash: ByteArray,
|
||||
val session: WalletConnectSession,
|
||||
val topic: String,
|
||||
val id: Long,
|
||||
val dialogData: PersonalSignDialogData,
|
||||
|
||||
val type: WcEthereumSignMessage.WCSignType,
|
||||
)
|
||||
|
||||
sealed class BinanceMessageData(
|
||||
|
|
|
|||
|
|
@ -1,23 +1,9 @@
|
|||
package com.tangem.tap.features.details.ui.common
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.RadioButtonDefaults
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -25,7 +11,7 @@ import androidx.compose.ui.res.colorResource
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconRight
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
@ -110,13 +96,13 @@ fun EmptyTopBarWithNavigation(
|
|||
|
||||
@Composable
|
||||
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
|
||||
PrimaryButtonIconRight(
|
||||
PrimaryButtonIconEnd(
|
||||
text = title,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
icon = painterResource(id = R.drawable.ic_tangem_24),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,13 +32,8 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
SettingsElement.WalletConnect -> {
|
||||
if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null
|
||||
}
|
||||
SettingsElement.SendFeedback ->
|
||||
if (cardTypesResolver?.isSaltPay() != true) it else null
|
||||
SettingsElement.LinkMoreCards -> {
|
||||
// if (state.createBackupAllowed) it else null
|
||||
// TODO: SaltPay: temporary excluding backup process for Visa cards
|
||||
if (state.createBackupAllowed && cardTypesResolver?.isSaltPay() != true) it else null
|
||||
}
|
||||
SettingsElement.SendFeedback -> it
|
||||
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
|
||||
SettingsElement.PrivacyPolicy -> {
|
||||
if (state.privacyPolicyUrl != null) it else null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,11 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -36,6 +23,7 @@ import com.tangem.tap.common.analytics.events.Settings
|
|||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) {
|
||||
|
|
@ -164,7 +152,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) {
|
|||
private fun WalletConnectScreenPreview() {
|
||||
WalletConnectScreen(
|
||||
state = WalletConnectScreenState(
|
||||
sessions = listOf(
|
||||
sessions = persistentListOf(
|
||||
WcSessionForScreen(
|
||||
description = "session from some dApp",
|
||||
sessionId = "",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class WalletConnectScreenState(
|
||||
val sessions: List<WcSessionForScreen>,
|
||||
val sessions: ImmutableList<WcSessionForScreen>,
|
||||
val isLoading: Boolean = false,
|
||||
val onRemoveSession: (String) -> Unit = {},
|
||||
val onAddSession: (String?) -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -4,23 +4,35 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
|
||||
class WalletConnectViewModel(private val store: Store<AppState>) {
|
||||
fun updateState(state: WalletConnectState): WalletConnectScreenState {
|
||||
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
|
||||
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions
|
||||
return WalletConnectScreenState(
|
||||
state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) },
|
||||
sessions.toImmutableList(),
|
||||
isLoading = state.loading,
|
||||
onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions) },
|
||||
onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions, state.wc2Sessions) },
|
||||
onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRemoveSession(sessionUri: String, sessions: List<WalletConnectSession>) {
|
||||
private fun onRemoveSession(
|
||||
sessionUri: String,
|
||||
sessions: List<WalletConnectSession>,
|
||||
wc2sessions: List<WcSessionForScreen>,
|
||||
) {
|
||||
sessions
|
||||
.firstOrNull { it.session.toUri() == sessionUri }
|
||||
?.let { baseSession ->
|
||||
store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session))
|
||||
store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session.topic, baseSession.session))
|
||||
return
|
||||
}
|
||||
wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let {
|
||||
store.dispatch(WalletConnectAction.DisconnectSession(sessionUri, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,15 @@ 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.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
|
||||
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
|
||||
|
||||
object BnbTransactionDialog {
|
||||
fun create(
|
||||
data: BinanceMessageData,
|
||||
session: WCSession,
|
||||
sessionId: Long,
|
||||
dAppName: String,
|
||||
context: Context,
|
||||
): AlertDialog {
|
||||
fun create(preparedData: WcPreparedRequest.BnbTransaction, context: Context): AlertDialog {
|
||||
val data = preparedData.preparedRequestData.data
|
||||
val message = when (data) {
|
||||
is BinanceMessageData.Trade -> data.tradeData.map {
|
||||
context.getString(
|
||||
|
|
@ -37,7 +32,7 @@ object BnbTransactionDialog {
|
|||
|
||||
val fullMessage = context.getString(
|
||||
R.string.wallet_connect_bnb_sign_message,
|
||||
dAppName,
|
||||
preparedData.preparedRequestData.dAppName,
|
||||
message,
|
||||
)
|
||||
val positiveButtonTitle = context.getText(R.string.common_sign)
|
||||
|
|
@ -48,14 +43,15 @@ object BnbTransactionDialog {
|
|||
setPositiveButton(positiveButtonTitle) { _, _ ->
|
||||
store.dispatch(
|
||||
WalletConnectAction.BinanceTransaction.Sign(
|
||||
id = sessionId,
|
||||
id = preparedData.requestId,
|
||||
data = data.data,
|
||||
sessionData = session,
|
||||
topic = preparedData.topic,
|
||||
),
|
||||
)
|
||||
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
|
||||
}
|
||||
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
|
||||
store.dispatch(WalletConnectAction.RejectRequest(session, sessionId))
|
||||
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
|
|
|
|||
|
|
@ -3,22 +3,24 @@ 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.domain.walletconnect2.domain.WcPreparedRequest
|
||||
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
|
||||
|
||||
object PersonalSignDialog {
|
||||
fun create(data: PersonalSignDialogData, context: Context): AlertDialog {
|
||||
fun create(preparedData: WcPreparedRequest.EthSign, context: Context): AlertDialog {
|
||||
val data = preparedData.preparedRequestData.dialogData
|
||||
val message = context.getString(R.string.wallet_connect_alert_sign_message, data.message)
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(context.getString(R.string.wallet_connect_title))
|
||||
setMessage(message)
|
||||
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
|
||||
store.dispatch(WalletConnectAction.SignMessage(data.session))
|
||||
store.dispatch(WalletConnectAction.SignMessage(data.topic))
|
||||
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
|
||||
}
|
||||
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
|
||||
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
|
||||
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
|
|
@ -30,6 +32,6 @@ object PersonalSignDialog {
|
|||
data class PersonalSignDialogData(
|
||||
val dAppName: String,
|
||||
val message: String,
|
||||
val session: WCSession,
|
||||
val topic: String,
|
||||
val id: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
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.domain.walletconnect2.domain.models.WalletConnectEvents
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
object SessionProposalDialog {
|
||||
fun create(
|
||||
sessionProposal: WalletConnectEvents.SessionProposal,
|
||||
networks: String,
|
||||
context: Context,
|
||||
onApprove: () -> Unit,
|
||||
onReject: () -> Unit,
|
||||
): AlertDialog {
|
||||
val message = context.getString(
|
||||
R.string.wallet_connect_request_session_start,
|
||||
sessionProposal.name,
|
||||
networks,
|
||||
sessionProposal.url,
|
||||
)
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(context.getString(R.string.wallet_connect_title))
|
||||
setMessage(message)
|
||||
setPositiveButton(context.getText(R.string.common_start)) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
onApprove()
|
||||
}
|
||||
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
onReject()
|
||||
}
|
||||
setOnCancelListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
onReject()
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,15 @@ 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.domain.walletconnect2.domain.WcPreparedRequest
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.trustwallet.walletconnect.models.session.WCSession
|
||||
|
||||
object TransactionDialog {
|
||||
fun create(data: TransactionRequestDialogData, context: Context): AlertDialog {
|
||||
fun create(preparedData: WcPreparedRequest.EthTransaction, context: Context): AlertDialog {
|
||||
val data = preparedData.preparedRequestData.dialogData
|
||||
val message = context.getString(
|
||||
R.string.wallet_connect_create_tx_message,
|
||||
data.dAppName,
|
||||
|
|
@ -22,22 +23,23 @@ object TransactionDialog {
|
|||
)
|
||||
|
||||
val positiveButtonTitle = when (data.type) {
|
||||
WcTransactionType.EthSignTransaction -> context.getText(R.string.common_sign)
|
||||
WcTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send)
|
||||
WcEthTransactionType.EthSignTransaction -> context.getText(R.string.common_sign)
|
||||
WcEthTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send)
|
||||
}
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(context.getString(R.string.wallet_connect_title))
|
||||
setMessage(message)
|
||||
setPositiveButton(positiveButtonTitle) { _, _ ->
|
||||
if (data.isEnoughFundsToSend) {
|
||||
store.dispatch(WalletConnectAction.SendTransaction(data.session))
|
||||
store.dispatch(WalletConnectAction.SendTransaction(data.topic))
|
||||
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
|
||||
} else {
|
||||
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
|
||||
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
|
||||
store.dispatch(WalletConnectAction.NotEnoughFunds)
|
||||
}
|
||||
}
|
||||
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
|
||||
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
|
||||
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
|
|
@ -54,7 +56,7 @@ data class TransactionRequestDialogData(
|
|||
val totalAmount: String,
|
||||
val balance: String,
|
||||
val isEnoughFundsToSend: Boolean,
|
||||
val session: WCSession,
|
||||
val topic: String,
|
||||
val id: Long,
|
||||
val type: WcTransactionType,
|
||||
val type: WcEthTransactionType,
|
||||
)
|
||||
|
|
@ -40,12 +40,6 @@ class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(da
|
|||
override fun getPreferenceKey(): String = "tangem_tos_accepted"
|
||||
}
|
||||
|
||||
class SaltPayDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
|
||||
override fun type(): DisclaimerType = DisclaimerType.SaltPay
|
||||
override fun getUri(): Uri = Uri.parse("$baseUrl/soltpay_tos.html")
|
||||
override fun getPreferenceKey(): String = "saltPay_tos_accepted"
|
||||
}
|
||||
|
||||
class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
|
||||
override fun type(): DisclaimerType = DisclaimerType.Start2Coin
|
||||
override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion()))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.features.disclaimer
|
||||
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
|
||||
|
|
@ -12,14 +11,12 @@ import java.util.Locale
|
|||
*/
|
||||
enum class DisclaimerType {
|
||||
Tangem,
|
||||
SaltPay,
|
||||
Start2Coin,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun get(cardDTO: CardDTO): DisclaimerType {
|
||||
return when {
|
||||
cardDTO.isSaltPay -> SaltPay
|
||||
cardDTO.isStart2Coin -> Start2Coin
|
||||
else -> Tangem
|
||||
}
|
||||
|
|
@ -31,7 +28,6 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
|
|||
val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId)
|
||||
return when (this) {
|
||||
DisclaimerType.Tangem -> TangemDisclaimer(dataProvider)
|
||||
DisclaimerType.SaltPay -> SaltPayDisclaimer(dataProvider)
|
||||
DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.disclaimer.ui
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.View.OVER_SCROLL_NEVER
|
||||
import android.webkit.WebView
|
||||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
|
|
@ -27,21 +28,6 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
|
|||
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
|
||||
private val webViewClient = DisclaimerWebViewClient()
|
||||
|
||||
override fun configureTransitions() {
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
when (store.state.disclaimerState.showedFromScreen) {
|
||||
AppScreen.Home -> {
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
|
||||
}
|
||||
AppScreen.Details -> {
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addBackPressHandler(handler = this)
|
||||
|
|
@ -86,11 +72,29 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
|
|||
super.onStop()
|
||||
}
|
||||
|
||||
override fun configureTransitions() {
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
when (store.state.disclaimerState.showedFromScreen) {
|
||||
AppScreen.Home -> {
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
|
||||
}
|
||||
AppScreen.Details -> {
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(DisclaimerAction.OnBackPressed)
|
||||
}
|
||||
|
||||
override fun newState(state: DisclaimerState) = with(binding) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
updateUiVisibility(state.disclaimer, state.progressState)
|
||||
binding.webView.loadUrl(state.disclaimer.getUri().toString())
|
||||
}
|
||||
|
||||
private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) {
|
||||
|
|
@ -111,16 +115,19 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
|
|||
}
|
||||
ProgressState.Error -> {
|
||||
root.beginDelayedTransition()
|
||||
webView.hide()
|
||||
groupAccept.hide()
|
||||
webView.show()
|
||||
groupAccept.show()
|
||||
groupLoading.hide()
|
||||
groupError.show()
|
||||
groupError.hide()
|
||||
webView.loadLocalTermsOfServices()
|
||||
}
|
||||
else -> {
|
||||
webView.loadUrl(disclaimer.getUri().toString())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(DisclaimerAction.OnBackPressed)
|
||||
private fun WebView.loadLocalTermsOfServices() {
|
||||
loadData(localTermsOfServices, "text/html", "UTF-8")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.tap.features.disclaimer.ui
|
||||
|
||||
internal val localTermsOfServices = """
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Legal Disclaimer</title>
|
||||
<style>
|
||||
@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Light.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Light.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Light.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Light.ttf) format("truetype");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Regular.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Regular.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Regular.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Regular.ttf) format("truetype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Medium.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Medium.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Medium.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Medium.ttf) format("truetype");font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Semibold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Semibold.ttf) format("truetype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Bold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Bold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Bold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Bold.ttf) format("truetype");font-weight:700;font-style:normal;font-display:swap}*{margin:0;padding:0}body{padding:0;margin:1rem;color:rgb(0 0 0);font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}h1{font-weight:700;font-size:30px;line-height:1.2;margin-bottom:1rem}p{font-size:16px;line-height:20px;letter-spacing:-.24px;margin-bottom:1rem}
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Legal Disclaimer</h1>
|
||||
<p> 1. Tangem application (Software)</p>
|
||||
<p> The Software is intended for usage only with Tangem hardware wallets (Cards) via NFC interface. The
|
||||
Software DOES NOT:</p>
|
||||
<p> a) Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain
|
||||
wallets holding digital assets, including crypto-currency.</p>
|
||||
<p> b) Generate, store, transmit, or have access to secret keys, passwords, passphrases, recovery phrases
|
||||
that can be used to restore or to copy private (secret) keys to blockchain wallets holding digital assets, including
|
||||
crypto-currency.</p>
|
||||
<p> c) Provide exchange, trading, investment services on behalf of Tangem AG.</p>
|
||||
<p> 2. Risks related to the use of Software</p>
|
||||
<p> Tangem will not be responsible for any losses, damages or claims arising from events falling within the
|
||||
scope of the following five categories:</p>
|
||||
<p> a) Mistakes made by the user of any cryptocurrency-related software or service, e.g., forgotten
|
||||
passwords, payments sent to wrong addresses, and accidental deletion of blockchain wallets on Cards.</p>
|
||||
<p> b) Problems of Software and/or any blockchain- or cryptocurrency- related software or service, e.g.,
|
||||
corrupted files, incorrectly constructed transactions, unsafe cryptographic libraries, malware.</p>
|
||||
<p> c) Technical failures in the hardware of the user, including Cards, of any cryptocurrency-related
|
||||
software or service, e.g., data loss due to a faulty or damaged storage device.</p>
|
||||
<p> d) Security problems experienced by the user of any cryptocurrency-related software or service, e.g.,
|
||||
unauthorized access to users' wallets and/or accounts.</p>
|
||||
<p> e) Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy
|
||||
of service providers, information security attacks on service providers, and fraud conducted by third parties.</p>
|
||||
<p> 3. Trading and Investment risks</p>
|
||||
<p> There is considerable exposure to risk in any crypto-currency or other digital asset exchange
|
||||
transaction. Any transaction involving currencies involves risks including, but not limited to, the potential for
|
||||
changing economic conditions that may substantially affect the price or liquidity of a currency. Investments in
|
||||
crypto-currency exchange speculation may also be susceptible to sharp rises and falls as the relevant market values
|
||||
fluctuate. It is for this reason that when speculating in such markets it is advisable to use only risk capital.</p>
|
||||
<p> 4. Electronic Trading Risks</p>
|
||||
<p> Before you engage in transactions using an electronic system, you should carefully review the rules and
|
||||
regulations of the exchanges offering the system and/or listing the instruments you intend to trade. Online trading
|
||||
has inherent risk due to system response and access times that may vary due to market conditions, system
|
||||
performance, and other factors. You should understand these and additional risks before trading.</p>
|
||||
<p> 5. Compliance with tax obligations </p>
|
||||
<p> The users of the Software are solely responsible to determinate what, if any, taxes apply to their
|
||||
crypto-currency transactions. The owners of, or contributors to, the Software are NOT responsible for determining
|
||||
the taxes that apply to crypto-currency transactions. </p>
|
||||
<p> 6. No warranties </p>
|
||||
<p> The Software is provided on an "as is" basis without any warranties of any kind regarding the
|
||||
Software and/or any content, data, materials and/or services provided on the Software.
|
||||
</p>
|
||||
<p> 7. Limitation of liability</p>
|
||||
<p> Unless otherwise required by law, in no event shall the owners of, or contributors to, the Software be
|
||||
liable for any damages of any kind, including, but not limited to, loss of use, loss of profits, or loss of data
|
||||
arising out of or in any way connected with the use of the Software. In no way are the owners of, or contributors
|
||||
to, the Software responsible for the actions, decisions, or other behavior taken or not taken by you in reliance
|
||||
upon the Software.</p>
|
||||
<p> 8. Last amendment</p>
|
||||
<p> This disclaimer was amended for the last time on October 1st, 2020.</p></body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue