Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-20 21:08:12 +03:00
commit 23d9050035
340 changed files with 4881 additions and 7536 deletions

View file

@ -205,4 +205,4 @@
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

View file

@ -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)

View file

@ -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

View file

@ -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,
),
)
}

View file

@ -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() {

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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

View file

@ -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"

View file

@ -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",

View file

@ -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(

View file

@ -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)) {

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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,

View file

@ -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(

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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"

View file

@ -60,7 +60,6 @@ class UserWalletIdBuilder private constructor(
ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
ProductType.Note,
ProductType.Wallet,
ProductType.SaltPay,
ProductType.Start2Coin,
-> null
},

View file

@ -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),
)
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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))
}
}

View file

@ -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))

View file

@ -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()
}
}
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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) {

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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)
}
},
)
}

View file

@ -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,
),
)
}
}

View file

@ -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),

View file

@ -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

View file

@ -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?,

View file

@ -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 -> {

View file

@ -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
},
)
}

View file

@ -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 = ":"

View file

@ -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)
}
}

View file

@ -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())
}
}

View file

@ -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()}"
}
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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?
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?)

View file

@ -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,
)

View file

@ -0,0 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class ChainWithDerivation(
val chain: String,
val derivationPath: String?,
)

View file

@ -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,
)

View file

@ -0,0 +1,4 @@
package com.tangem.tap.domain.walletconnect2.domain.models
@JvmInline
value class NetworkNamespace(val value: String)

View file

@ -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,
)
}
}
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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?,
)

View file

@ -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,
)
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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),
)
}
/**

View file

@ -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,
)
}

View file

@ -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,
)

View file

@ -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(

View file

@ -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"
}
}

View file

@ -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) },
)

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}
}
}

View file

@ -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
}
}

View file

@ -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(

View file

@ -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,
)
}

View file

@ -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
}

View file

@ -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 = "",

View file

@ -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 = {},

View file

@ -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))
}
}
}

View file

@ -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)

View file

@ -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,
)

View file

@ -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()
}
}

View file

@ -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,
)

View file

@ -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()))

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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>&nbsp; &nbsp; 1. Tangem application (Software)</p>
<p>&nbsp; &nbsp; The Software is intended for usage only with Tangem hardware wallets (Cards) via NFC interface. The
Software DOES NOT:</p>
<p>&nbsp; &nbsp; a) Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain
wallets holding digital assets, including crypto-currency.</p>
<p>&nbsp; &nbsp; 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>&nbsp; &nbsp; c) Provide exchange, trading, investment services on behalf of Tangem AG.</p>
<p>&nbsp; &nbsp; 2. Risks related to the use of Software</p>
<p>&nbsp; &nbsp; 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>&nbsp; &nbsp; 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>&nbsp; &nbsp; 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>&nbsp; &nbsp; 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>&nbsp; &nbsp; d) Security problems experienced by the user of any cryptocurrency-related software or service, e.g.,
unauthorized access to users&apos; wallets and/or accounts.</p>
<p>&nbsp; &nbsp; 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>&nbsp; &nbsp; 3. Trading and Investment risks</p>
<p>&nbsp; &nbsp; 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>&nbsp; &nbsp; 4. Electronic Trading Risks</p>
<p>&nbsp; &nbsp; 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>&nbsp; &nbsp; 5. Compliance with tax obligations &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; 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. &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; 6. No warranties &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; The Software is provided on an &quot;as is&quot; basis without any warranties of any kind regarding the
Software and/or any content, data, materials and/or services provided on the Software. &nbsp; &nbsp; &nbsp;
&nbsp;</p>
<p>&nbsp; &nbsp; 7. Limitation of liability</p>
<p>&nbsp; &nbsp; 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>&nbsp; &nbsp; 8. Last amendment</p>
<p>&nbsp; &nbsp; 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