Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-10 16:47:17 +03:00
commit 5fa587d8cc
635 changed files with 18802 additions and 9118 deletions

12
.gitignore vendored
View file

@ -3,12 +3,24 @@
# Built application files
/build
/buildSrc
**/build/.transforms/**
**/build/classes/**
**/build/generated/**
**/build/intermediates/**
**/build/kotlin/**
**/build/libs/**
**/build/outputs/**
**/build/tmp/**
# Local configuration file (sdk path, etc)
local.properties
# Google services
# Huawei services
app/agconnect-services.json
app/src/debug/google-services.json
app/src/internal/google-services.json
app/src/external/google-services.json

View file

@ -11,9 +11,14 @@ plugins {
alias(deps.plugins.firebase.crashlytics)
alias(deps.plugins.firebase.perf)
alias(deps.plugins.ksp)
id(deps.plugins.agconnect.get().pluginId)
id("configuration")
}
agcp {
manifest = false
}
android {
namespace = "com.tangem.wallet"
testOptions {
@ -53,12 +58,35 @@ android {
keyPassword = keystoreProperties["key_password"] as String
}
}
flavorDimensions += "services"
productFlavors {
create("google") {
dimension = "services"
buildConfigField("String", "FLAVOR_NAME", "\"google\"")
}
create("huawei") {
dimension = "services"
buildConfigField("String", "FLAVOR_NAME", "\"huawei\"")
}
}
buildTypes {
debug {
buildConfigField("String", "BUILD_TYPE", "\"debug\"")
}
release {
buildConfigField("String", "BUILD_TYPE", "\"release\"")
}
}
}
configurations.all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
exclude(group = "com.github.komputing.kethereum")
exclude(group = "com.android.tools.build", module = "gradle")
resolutionStrategy {
dependencySubstitution {
@ -121,7 +149,6 @@ dependencies {
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.models)
implementation(projects.domain.notifications.toggles)
implementation(projects.domain.swap.models)
implementation(projects.domain.swap)
implementation(projects.domain.walletManager)
@ -243,6 +270,8 @@ dependencies {
implementation(projects.features.tangempay.details.impl)
implementation(projects.features.tangempay.main.api)
implementation(projects.features.tangempay.main.impl)
implementation(projects.features.tangempay.onboarding.api)
implementation(projects.features.tangempay.onboarding.impl)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.tokenRecieve.impl)
@ -318,7 +347,6 @@ dependencies {
implementation(deps.coil.gif)
implementation(deps.coil.svg)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.compose.accompanist.appCompatTheme)
@ -379,4 +407,9 @@ dependencies {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group = "com.google.guava", module = "listenablefuture")
}
/** Huawei flavor-specific dependencies */
"huaweiImplementation"(deps.huawei.push)
"huaweiImplementation"(deps.agconnect.agcp)
"huaweiImplementation"(deps.agconnect.core)
}

View file

@ -7,6 +7,13 @@
-keep class com.google.android.gms.internal.** { *; }
-keepclasseswithmembers class com.google.firebase.FirebaseException
# huawei push kit
-ignorewarnings
-keepattributes SourceFile,LineNumberTable
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
# hedera sdk
-keep class com.hedera.hashgraph.sdk.** { *; }
-keep interface com.hedera.hashgraph.sdk.** { *; }

View file

@ -14,9 +14,13 @@ import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.tangem.common.allure.FailedStepScreenshotInterceptor
import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule
import kotlinx.coroutines.runBlocking
@ -46,6 +50,12 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var appPreferencesStore: AppPreferencesStore
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
@Inject
lateinit var promoRepository: PromoRepository
private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant(
@ -86,10 +96,12 @@ abstract class BaseTestCase : TestCase(
value = false
)
}
promoRepository.setNeverToShowWalletPromo(PromoId.Sepa)
}
apiEnvironmentRule.setup(apiConfigsManager)
ActivityScenario.launch(MainActivity::class.java)
Intents.init()
setFeatureToggles()
additionalBeforeSection()
}.after {
additionalAfterSection()
@ -113,4 +125,18 @@ abstract class BaseTestCase : TestCase(
{
composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth)
}
private fun setFeatureToggles() {
runBlocking {
with(featureTogglesManager as MutableFeatureTogglesManager) {
changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true)
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true)
changeToggle("SEND_VIA_SWAP_ENABLED", true)
changeToggle("SWAP_REDESIGN_ENABLED", true)
changeToggle("SEND_REDESIGN_ENABLED", true)
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true)
}
}
}
}

View file

@ -3,5 +3,7 @@ package com.tangem.common.constants
object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val WAIT_UNTIL_TIMEOUT = 20_000L
}

View file

@ -0,0 +1,105 @@
package com.tangem.common.utils
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import timber.log.Timber
import java.util.concurrent.TimeUnit
/**
*
*/
fun getWcUri(
network: String = "ethereum",
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Getting WC URI for network: $network")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения
.readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа
.writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи
.callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса
.build()
val request = Request.Builder()
.url("$baseUrl/wc_uri?network=$network")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
val jsonObject = JSONObject(body)
if (jsonObject.getBoolean("success")) {
val wcUri = jsonObject.getString("wcUri")
Timber.i("Got WC URI successfully: $wcUri")
wcUri
} else {
Timber.e("API returned error: ${jsonObject.optString("error", "Unknown")}")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error getting WC URI")
null
}
}
fun checkServiceHealth(
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Checking service health")
val client = OkHttpClient()
val request = Request.Builder()
.url("$baseUrl/health")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
if (body.isEmpty()) {
Timber.e("Response body is empty")
return null
}
val jsonObject = JSONObject(body)
val status = jsonObject.optString("status", "")
if (status.isNotEmpty()) {
Timber.i("Got status successfully: $status")
status
} else {
Timber.e("Status field is missing or empty")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error checking health")
null
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onMainScreen
import com.tangem.screens.onMarketsTooltipScreen
import com.tangem.screens.onStoriesScreen
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openMainScreen(
productType: ProductType? = null,
mockContent: MockContent? = null,
alreadyActivatedDialogIsShown : Boolean = false
) {
if (productType != null) {
MockProvider.setMocks(productType)
}
if (mockContent != null) {
MockProvider.setMocks(mockContent)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
}
if (alreadyActivatedDialogIsShown) {
step("Click on 'This is my wallet' button") {
AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() }
}
}
step("Assert 'Main' screen is displayed") {
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Click on 'Market Tooltip' screen") {
onMarketsTooltipScreen { contentContainer.clickWithAssertion() }
}
}
fun BaseTestCase.synchronizeAddresses(balance: String) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
}
fun BaseTestCase.openDeviceSettingsScreen() {
step("Open wallet details") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.performClick() }
}
step("Click on 'Device settings' button") {
onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() }
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.scenarios
import android.content.Intent
import android.content.Intent.*
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import io.github.kakaocup.kakao.intent.KIntent
fun openAppByDeepLink(deepLinkUri: String?) {
val deeplinkScheme = "tangem://wc?uri="
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
val intent = Intent(ACTION_VIEW, Uri.parse(deeplinkScheme + deepLinkUri)).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
fun checkSendEMailIntentCalled() {
val expectedIntent = KIntent {
hasAction(ACTION_CHOOSER)
hasExtra(EXTRA_TITLE, "Send mail...")
hasExtraWithKey(EXTRA_INTENT)
}
expectedIntent.intended()
}

View file

@ -0,0 +1,21 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDeviceSettingsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) {
step("Click on 'Scan card or ring' button") {
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }
}
step("Assert 'Reset to Factory Settings' button title is displayed") {
onDeviceSettingsScreen { resetToFactorySettingsButtonTitle.assertIsDisplayed() }
}
step("Assert 'Reset to Factory Settings' button subtitle is displayed") {
onDeviceSettingsScreen { resetToFactorySettingsButtonSubtitle(withBackup).assertIsDisplayed() }
}
step("Click on 'Reset to Factory Settings' button") {
onDeviceSettingsScreen { resetToFactorySettingsButtonTitle.performClick() }
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.AlreadyUsedWalletDialogPageObject
import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkFailedTransactionDialog() {
step("Assert failed transaction dialog title is displayed") {
onFailedTransactionDialog { title.assertIsDisplayed() }
}
step("Assert failed transaction dialog text is displayed") {
onFailedTransactionDialog { text.assertIsDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
onFailedTransactionDialog { cancelButton.assertIsDisplayed() }
}
step("Assert 'Support' button is displayed") {
onFailedTransactionDialog { supportButton.assertIsDisplayed() }
}
}
fun checkScanWarningDialog() {
step("Assert 'Scan warning' dialog title is displayed") {
ScanWarningDialogPageObject { warningTitle.isDisplayed() }
}
step("Assert warning dialog message is displayed") {
ScanWarningDialogPageObject { warningMessage.isDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
ScanWarningDialogPageObject { cancelButton.isDisplayed() }
}
step("Assert 'How to scan' button is displayed") {
ScanWarningDialogPageObject { howToScanButton.isDisplayed() }
}
step("Assert 'Request support' button is displayed") {
ScanWarningDialogPageObject { requestSupportButton.isDisplayed() }
}
}
fun checkAlreadyUsedWalletDialog() {
step("Assert 'Already used Wallet' dialog title is displayed") {
AlreadyUsedWalletDialogPageObject { title.isDisplayed() }
}
step("Assert dialog message is displayed") {
AlreadyUsedWalletDialogPageObject { message.isDisplayed() }
}
step("Assert 'This is my wallet' button is displayed") {
AlreadyUsedWalletDialogPageObject { message.isDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
AlreadyUsedWalletDialogPageObject { cancelButton.isDisplayed() }
}
step("Assert 'Request support' button is displayed") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() }
}
}

View file

@ -1,41 +0,0 @@
package com.tangem.scenarios
import androidx.compose.ui.test.junit4.ComposeTestRule
import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.github.kakaocup.compose.node.element.ComposeScreen
class OpenMainScreenScenario(
private val testRule: ComposeTestRule,
private val productType: ProductType? = null,
) : Scenario() {
override val steps: TestContext<Unit>.() -> Unit = {
if (productType != null) {
MockProvider.setMocks(productType)
}
ComposeScreen.onComposeScreen<DisclaimerPageObject>(testRule) {
step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesPageObject>(testRule) {
step("Click on \"Scan\" button") {
scanButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<MainScreenPageObject>(testRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
ComposeScreen.onComposeScreen<MarketsTooltipPageObject>(testRule) {
step("Close Markets tooltip"){
contentContainer.performClick()
}
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onResetCardScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkResetCardScreen(withBackup: Boolean = false) {
step("Assert title is displayed") {
onResetCardScreen { title.assertIsDisplayed() }
}
step("Assert 'Attention' image is displayed") {
onResetCardScreen { attentionImage.assertIsDisplayed() }
}
step("Assert 'Attention' subtitle is displayed") {
onResetCardScreen { subtitle.assertIsDisplayed() }
}
step("Assert description is displayed") {
onResetCardScreen { description.assertIsDisplayed() }
}
step("Assert 'Lost wallet' checkbox is displayed") {
onResetCardScreen { lostWalletAccessCheckBox.assertIsDisplayed() }
}
if (withBackup) {
step("Assert 'Lost password restore' checkbox is displayed") {
onResetCardScreen { lostPasswordRestoreCheckBox.assertIsDisplayed() }
}
} else {
step("Assert 'Lost password restore' checkbox doesn't exist") {
onResetCardScreen { lostPasswordRestoreCheckBox.assertDoesNotExist() }
}
}
}
fun BaseTestCase.checkCheckBoxLogic(withBackup: Boolean = false) {
if (withBackup) {
step("Click on 'Lost wallet' checkbox") {
onResetCardScreen { lostWalletAccessCheckBox.performClick() }
}
step("Assert 'Lost wallet' checkbox is enabled") {
onResetCardScreen { lostWalletAccessCheckBox.assertIsEnabled() }
}
step("Assert 'Reset the card' button is disabled") {
onResetCardScreen { resetCardButton.assertIsNotEnabled() }
}
step("Click on 'Lost password restore' checkbox") {
onResetCardScreen { lostPasswordRestoreCheckBox.performClick() }
}
step("Assert 'Lost password restore' checkbox is enabled") {
onResetCardScreen { lostPasswordRestoreCheckBox.assertIsEnabled() }
}
step("Assert 'Reset the card' button is enabled") {
onResetCardScreen { resetCardButton.assertIsEnabled() }
}
step("Click on 'Lost password restore' checkbox") {
onResetCardScreen { lostPasswordRestoreCheckBox.performClick() }
}
step("Assert 'Reset the card' button is disabled") {
onResetCardScreen { resetCardButton.assertIsNotEnabled() }
}
step("Click on 'Lost wallet' checkbox") {
onResetCardScreen { lostWalletAccessCheckBox.performClick() }
}
step("Click on 'Lost password restore' checkbox") {
onResetCardScreen { lostPasswordRestoreCheckBox.performClick() }
}
step("Assert 'Reset the card' button is disabled") {
onResetCardScreen { resetCardButton.assertIsNotEnabled() }
}
} else {
step("Click on 'Lost wallet' checkbox") {
onResetCardScreen { lostWalletAccessCheckBox.performClick() }
}
step("Assert 'Lost wallet' checkbox is enabled") {
onResetCardScreen { lostWalletAccessCheckBox.assertIsEnabled() }
}
step("Assert 'Reset the card' button is enabled") {
onResetCardScreen { resetCardButton.assertIsEnabled() }
}
}
}

View file

@ -0,0 +1,139 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkWalletConnectBottomSheet() {
step("Assert 'Wallet Connect' bottom sheet title is displayed") {
onWalletConnectBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app icon is displayed") {
onWalletConnectBottomSheet { appIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app name is displayed") {
onWalletConnectBottomSheet { appName.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet approve icon is displayed") {
onWalletConnectBottomSheet { approveIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app URL is displayed") {
onWalletConnectBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request icon is displayed") {
onWalletConnectBottomSheet { connectionRequestIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request text is displayed") {
onWalletConnectBottomSheet { connectionRequestText.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request chevron is displayed") {
onWalletConnectBottomSheet { connectionRequestChevron.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet icon is displayed") {
onWalletConnectBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet title is displayed") {
onWalletConnectBottomSheet { walletNameTitle.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet name is displayed") {
onWalletConnectBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet networks icon is displayed") {
onWalletConnectBottomSheet { networksIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet networks title is displayed") {
onWalletConnectBottomSheet { networksTitle.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet right networks icons is displayed") {
onWalletConnectBottomSheet { networksIcons.assertIsDisplayed() }
}
step("'Wallet Connect' bottom sheet networks selector icon is displayed") {
onWalletConnectBottomSheet { networksSelectorIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet 'Cancel' button is displayed") {
onWalletConnectBottomSheet { cancelButton.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet 'Connect' button is displayed") {
onWalletConnectBottomSheet { connectButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectScreen() {
step("Assert 'Wallet Connect' title is displayed") {
onWalletConnectScreen { title.assertIsDisplayed() }
}
step("Assert 'More' button is displayed") {
onWalletConnectScreen { moreButton.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectScreen { walletName.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectScreen { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectScreen { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectScreen { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectScreen { appUrl.assertIsDisplayed() }
}
step("Assert 'New Connection' button is displayed") {
onWalletConnectScreen { newConnectionButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
step("Assert connection details title is displayed") {
onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() }
}
step("Assert date is displayed") {
onWalletConnectDetailsBottomSheet { date.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onWalletConnectDetailsBottomSheet { closeButton.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectDetailsBottomSheet { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectDetailsBottomSheet { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectDetailsBottomSheet { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectDetailsBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert wallet icon is displayed") {
onWalletConnectDetailsBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert wallet title is displayed") {
onWalletConnectDetailsBottomSheet { walletTitle.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectDetailsBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Connected networks' title is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworksTitle.assertIsDisplayed() }
}
step("Assert connected network item is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkItem.assertIsDisplayed() }
}
step("Assert connected network icon is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() }
}
step("Assert connected dApp name: '$dAppName'") {
onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) }
}
step("Assert connected network symbol is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() }
}
step("Assert 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KTextView
import io.github.kakaocup.kakao.text.KButton
object AlreadyUsedWalletDialogPageObject : KScreen<AlreadyUsedWalletDialogPageObject>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val title = KTextView {
withText(R.string.security_alert_title)
}
val message = KTextView {
withText(R.string.wallet_been_activated_message)
}
val cancelButton = KButton {
withText(R.string.common_cancel)
}
val requestSupportButton = KButton {
withText(R.string.alert_button_request_support)
}
val thisIsMyWalletButton = KButton {
withText(R.string.this_is_my_wallet_title)
}
}

View file

@ -38,7 +38,7 @@ class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProv
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(R.string.common_unknown_error))
useUnmergedTree = true
}

View file

@ -0,0 +1,61 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.DeviceSettingsScreenTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DeviceSettingsPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(DeviceSettingsScreenTestTags.LAZY_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val imageBlock: KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK)
useUnmergedTree = true
}
val resetToFactorySettingsButtonTitle: KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.ITEM_TITLE)
hasText(getResourceString(R.string.card_settings_reset_card_to_factory))
useUnmergedTree = true
}
val scanCardOrRingButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.scan_card_settings_button))
useUnmergedTree = true
}
fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE)
useUnmergedTree = true
if (withBackup) {
hasText(getResourceString(R.string.reset_card_with_backup_to_factory_message))
} else {
hasText(getResourceString(R.string.reset_card_without_backup_to_factory_message))
}
}
}
internal fun BaseTestCase.onDeviceSettingsScreen(function: DeviceSettingsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -4,7 +4,7 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.DialogTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
@ -14,7 +14,7 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(DialogTestTags.DIALOG_CONTAINER)
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val cancelButton: KNode = child {

View file

@ -0,0 +1,28 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.core.ui.R
import io.github.kakaocup.kakao.text.KButton
import io.github.kakaocup.kakao.text.KTextView
object FailedCardVerificationDialogPageObject : KScreen<FailedCardVerificationDialogPageObject>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val title = KTextView {
withId(R.id.alertTitle)
}
val message = KTextView {
withId(R.id.message)
}
val cancelButton = KButton {
withText(R.string.common_cancel)
}
val understandButton = KButton {
withText(R.string.common_understand)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.common.ui.R as CommonUIR
class FailedTransactionDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<FailedTransactionDialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
hasText(getResourceString(CommonUIR.string.send_alert_transaction_failed_title))
useUnmergedTree = true
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel))
}
val supportButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_support))
}
}
internal fun BaseTestCase.onFailedTransactionDialog(function: FailedTransactionDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -8,6 +8,7 @@ import com.tangem.common.extensions.hasLazyListItemPosition
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.feature.wallet.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
@ -35,7 +36,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
)
val synchronizeAddressesButton: KNode = child {
val screenContainer: KNode = child {
hasTestTag(MainScreenTestTags.SCREEN_CONTAINER)
}
val synchronizeAddressesButton: KNode = lazyList.child {
hasText(getResourceString(R.string.common_generate_addresses))
}
@ -44,6 +49,29 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasText(getResourceString(R.string.common_buy))
}
val notificationContainer: KNode = child {
hasTestTag(NotificationTestTags.CONTAINER)
useUnmergedTree = true
}
val devCardNotificationIcon: KNode = child {
hasAnySibling(withText(getResourceString(R.string.warning_developer_card_title)))
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
val devCardNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.warning_developer_card_title))
useUnmergedTree = true
}
val devCardNotificationMessage: KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(R.string.warning_developer_card_message))
useUnmergedTree = true
}
/**
* Find token list item with title and address
*/

View file

@ -0,0 +1,65 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.ResetCardScreenTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class ResetCardPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ResetCardPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(ResetCardScreenTestTags.TITLE)
hasText(getResourceString(R.string.card_settings_reset_card_to_factory))
useUnmergedTree = true
}
val attentionImage: KNode = child {
hasTestTag(ResetCardScreenTestTags.ATTENTION_IMAGE)
useUnmergedTree = true
}
val subtitle: KNode = child {
hasTestTag(ResetCardScreenTestTags.SUBTITLE)
hasText(getResourceString(R.string.common_attention))
useUnmergedTree = true
}
val description: KNode = child {
hasTestTag(ResetCardScreenTestTags.DESCRIPTION)
useUnmergedTree = true
}
val lostWalletAccessCheckBox: KNode = child {
hasTestTag(ResetCardScreenTestTags.CHECKBOX)
hasAnySibling(
withTestTag(ResetCardScreenTestTags.CHECKBOX_TEXT) and
withText(getResourceString(R.string.reset_card_to_factory_condition_1))
)
useUnmergedTree = true
}
val lostPasswordRestoreCheckBox: KNode = child {
hasTestTag(ResetCardScreenTestTags.CHECKBOX)
hasAnySibling(
withTestTag(ResetCardScreenTestTags.CHECKBOX_TEXT) and
withText(getResourceString(R.string.reset_card_to_factory_condition_2))
)
useUnmergedTree = true
}
val resetCardButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.reset_card_to_factory_button_title))
}
}
internal fun BaseTestCase.onResetCardScreen(function: ResetCardPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,36 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KTextView
import io.github.kakaocup.kakao.text.KButton
object ScanWarningDialogPageObject : KScreen<ScanWarningDialogPageObject>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val warningTitle = KTextView {
withText(R.string.common_warning)
}
val warningMessage = KTextView {
withText(R.string.alert_troubleshooting_scan_card_title)
}
val tryAgainButton = KButton {
withId(R.id.try_again_button)
}
val howToScanButton = KButton {
withId(R.id.how_to_scan_button)
}
val requestSupportButton = KButton {
withId(R.id.request_support_button)
}
val cancelButton = KButton {
withId(R.id.cancel_button)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.SendAddressScreenTestTags
import com.tangem.features.send.v2.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) {
val addressTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
useUnmergedTree = true
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_next))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendAddressScreen(function: SendAddressPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,30 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val sendButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_send))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,7 +3,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.StakingSendScreenTestTags
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
@ -12,11 +12,11 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasTestTag as withTestTag
class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendPageObject>(semanticsProvider = semanticsProvider) {
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER)
hasTestTag(SendScreenTestTags.SCREEN_CONTAINER)
}
val title: KNode = child {
@ -25,44 +25,44 @@ class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
val amountContainerTitle: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE)
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE)
useUnmergedTree = true
}
val amountContainerText: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT)
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT)
useUnmergedTree = true
}
val amountInputTextField: KNode = child {
hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD)
hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT)
hasTestTag(SendScreenTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
val currencyButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON))
hasTestTag(SendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(SendScreenTestTags.CURRENCY_ICON))
useUnmergedTree = true
}
val fiatButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON))
hasTestTag(SendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(SendScreenTestTags.FIAT_ICON))
useUnmergedTree = true
}
val maxButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.MAX_BUTTON)
hasTestTag(SendScreenTestTags.MAX_BUTTON)
useUnmergedTree = true
}
val previousButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON)
hasTestTag(SendScreenTestTags.PREVIOUS_BUTTON)
useUnmergedTree = true
}
@ -74,5 +74,5 @@ class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) =
internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,6 +3,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseAmountBlockTestTags
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
@ -11,8 +12,8 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendDetailsPageObject>(semanticsProvider = semanticsProvider) {
class StakingConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingConfirmPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
@ -20,12 +21,12 @@ class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsP
}
val primaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT)
hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT)
hasTestTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
@ -47,5 +48,5 @@ class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsP
}
internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) =
internal fun BaseTestCase.onStakingConfirmScreen(function: StakingConfirmPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -57,7 +57,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
hasTestTag(NotificationTestTags.MESSAGE)
useUnmergedTree = true
}

View file

@ -111,6 +111,12 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
hasText(getResourceString(R.string.common_buy))
}
@OptIn(ExperimentalTestApi::class)
val sendButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_send))
}
}
internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) =

View file

@ -0,0 +1,108 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect))
hasTestTag(WalletConnectBottomSheetTestTags.TITLE)
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_URL)
useUnmergedTree = true
}
val connectionRequestIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_ICON)
useUnmergedTree = true
}
val connectionRequestText: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_TEXT)
useUnmergedTree = true
}
val connectionRequestChevron: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_CHEVRON)
useUnmergedTree = true
}
val walletIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_ICON)
useUnmergedTree = true
}
val walletNameTitle: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME)
useUnmergedTree = true
}
val networksIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICON)
useUnmergedTree = true
}
val networksTitle: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_TITLE)
useUnmergedTree = true
}
val networksIcons: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICONS)
useUnmergedTree = true
}
val networksSelectorIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON)
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasText(getResourceString(R.string.common_cancel))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
val connectButton: KNode = child {
hasText(getResourceString(R.string.wc_common_connect))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectBottomSheet(function: WalletConnectBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,107 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectDetailsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectDetailsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.TITLE)
hasText(getResourceString(WalletConnectImplR.string.wc_connected_app_title))
useUnmergedTree = true
}
val date: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.DATE)
useUnmergedTree = true
}
val closeButton: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val networksBlockTitle: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_connected_networks))
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_URL)
useUnmergedTree = true
}
val walletIcon: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON)
useUnmergedTree = true
}
val walletTitle: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME)
useUnmergedTree = true
}
val connectedNetworksTitle: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORKS_TITLE)
useUnmergedTree = true
}
val connectedNetworkItem: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ITEM)
useUnmergedTree = true
}
val connectedNetworkIcon: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ICON)
useUnmergedTree = true
}
val connectedNetworkName: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_NAME)
useUnmergedTree = true
}
val connectedNetworkSymbol: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_SYMBOL)
useUnmergedTree = true
}
val disconnectButton: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.common_disconnect))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectDetailsBottomSheet(function: WalletConnectDetailsBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,59 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.WalletConnectScreenTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(R.string.wc_connections))
useUnmergedTree = true
}
val moreButton: KNode = child {
hasTestTag(WalletConnectScreenTestTags.MORE_BUTTON)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectScreenTestTags.WALLET_NAME)
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_NAME)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_URL)
useUnmergedTree = true
}
val newConnectionButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.wc_new_connection))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -21,7 +21,7 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
val linkMoreCardsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_row_title_create_backup))
}
val cardSettingsButton: KNode = walletSettingsItem.child {
val deviceSettingsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.card_settings_title))
}
val referralProgramButton: KNode = walletSettingsItem.child {

View file

@ -6,8 +6,9 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -36,13 +37,10 @@ class BuyTokenTest : BaseTestCase() {
setWireMockScenarioState(scenarioName, "Error")
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -84,13 +82,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -175,13 +170,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -250,13 +242,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -339,13 +328,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
@ -437,13 +423,10 @@ class BuyTokenTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }

View file

@ -3,7 +3,7 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.scenarios.openMainScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onReferralProgramScreen
import com.tangem.screens.onTopBar
@ -19,7 +19,9 @@ class DetailsTest : BaseTestCase() {
@Test
fun walletWithoutBackupDetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open 'Main Screen'") {
openMainScreen()
}
onTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
@ -53,7 +55,7 @@ class DetailsTest : BaseTestCase() {
linkMoreCardsButton.assertIsDisplayed()
}
step("Assert 'Card Settings' button is visible") {
cardSettingsButton.assertIsDisplayed()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button is visible") {
referralProgramButton.assertIsDisplayed()
@ -67,7 +69,9 @@ class DetailsTest : BaseTestCase() {
// @Test
fun wallet2DetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
}
onTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
@ -101,7 +105,7 @@ class DetailsTest : BaseTestCase() {
linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert 'Card Settings' button is visible") {
cardSettingsButton.assertIsDisplayed()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button is visible") {
referralProgramButton.assertIsDisplayed()
@ -115,7 +119,9 @@ class DetailsTest : BaseTestCase() {
@Test
fun noteDetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note))
step("Open 'Main Screen'") {
openMainScreen(ProductType.Note)
}
onTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
@ -146,7 +152,7 @@ class DetailsTest : BaseTestCase() {
}
onWalletSettingsScreen {
step("Assert 'Card Settings' button is visible") {
cardSettingsButton.assertIsDisplayed()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button does not exist") {
referralProgramButton.assertIsNotDisplayed()
@ -162,7 +168,9 @@ class DetailsTest : BaseTestCase() {
@Test
fun validateReferralProgramScreenTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open 'Main Screen'") {
openMainScreen()
}
step("Open wallet details") {
onTopBar { moreButton.clickWithAssertion() }
}

View file

@ -0,0 +1,176 @@
package com.tangem.tests
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.redux.StateDialog
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class FeedbackTest : BaseTestCase() {
@AllureId("894")
@DisplayName("Send feedback: from details")
@Test
fun sendFeedbackFromDetailsTest() {
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click 'Contact support' button") {
onDetailsScreen { contactSupportButton.clickWithAssertion() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
@AllureId("893")
@DisplayName("Send feedback: failed transaction")
@Test
fun sendFeedbackFromFailedTransactionTest() {
val balance = TOTAL_BALANCE
val tokenName = "Polygon"
val recipientAddress = RECIPIENT_ADDRESS
val sendAmount = "1"
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Assert input text field has value: '$sendAmount'") {
onSendScreen { amountInputTextField.assertTextContains(value = sendAmount, substring = true) }
}
step("Click 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Enter address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Click 'Send' button") {
onSendConfirmScreen { sendButton.clickWithAssertion() }
}
step("Check 'Failed transaction' dialog") {
checkFailedTransactionDialog()
}
step("Click on 'Support' button") {
onFailedTransactionDialog { supportButton.performClick() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
@AllureId("3985")
@DisplayName("Send feedback: from 'Warning' dialog after card scan")
@Test
fun sendFeedbackFromScanScreenTest() {
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
MockProvider.resetEmulateError()
}
).run {
Allure.step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Set scanning error") {
MockProvider.setEmulateError(TangemSdkError.TagLost())
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.performClick() }
}
step("Force show 'Scan warning' dialog"){
runOnUiThread {
val scanFailsState = StateDialog.ScanFailsDialog(source = StateDialog.ScanFailsSource.MAIN)
store.dispatch(GlobalAction.ShowDialog(scanFailsState))
}
}
step("Check 'Scan warning' dialog") {
checkScanWarningDialog()
}
step("Click on 'Request support' button") {
ScanWarningDialogPageObject { requestSupportButton.click() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
@AllureId("3986")
@DisplayName("Send feedback: from scan already used wallet alert dialog")
@Ignore("TODO On CI Already used wallet doesn't displayed")
@Test
fun sendFeedbackAfterScanAlreadyUsedWalletTest() {
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Set mocks for Wallet2") {
MockProvider.setMocks(ProductType.Wallet2)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
}
step("Check 'Already used Wallet' dialog") {
checkAlreadyUsedWalletDialog()
}
step("Click on 'Request support' button") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.click() }
}
step("Check 'Contact support' intent is called") {
checkSendEMailIntentCalled()
}
}
}
}

View file

@ -3,8 +3,9 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -21,13 +22,10 @@ class HideTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
setupHooks().run {
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }

View file

@ -1,7 +1,7 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.scenarios.openMainScreen
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@ -11,7 +11,9 @@ class MainScreenTest : BaseTestCase() {
@Test
fun goToMain() {
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
step("Open 'Main Screen'") {
openMainScreen()
}
}
}

View file

@ -5,9 +5,10 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -23,8 +24,9 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run {
val tokenTitle = "Ethereum"
val tokenNetwork = "Ethereum network"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
@ -90,14 +92,12 @@ class OrganizeTokensTest : BaseTestCase() {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
@ -129,8 +129,9 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
@ -178,14 +179,12 @@ class OrganizeTokensTest : BaseTestCase() {
val polygonTitle = "Polygon"
val polExMaticTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {

View file

@ -0,0 +1,85 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.*
import com.tangem.tap.domain.sdk.mocks.content.BackupWalletMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class ResetCardTest : BaseTestCase() {
@AllureId("3988")
@DisplayName("Reset card: reset Wallet 1.0 card without backup")
@Test
fun resetWallet1CardWithoutBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Open 'Reset card' screen") {
openResetCardScreen()
}
step("Check 'Reset card' screen") {
checkResetCardScreen()
}
step("Check checkbox logic") {
checkCheckBoxLogic()
}
}
}
@AllureId("3987")
@DisplayName("Reset card: reset Wallet 1.0 card with backup")
@Test
fun resetWallet1CardWithBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(mockContent = BackupWalletMockContent)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Open 'Reset card' screen") {
openResetCardScreen(withBackup = true)
}
step("Check 'Reset card' screen") {
checkResetCardScreen(withBackup = true)
}
step("Check checkbox logic") {
checkCheckBoxLogic(withBackup = true)
}
}
}
@AllureId("3974")
@DisplayName("Reset card: reset Wallet 2.0 card with backup")
@Ignore("toDo [REDACTED_TASK_KEY]: On CI Already used wallet dialog doesn't displayed")
@Test
fun resetWallet2CardWithBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Open 'Reset card' screen") {
openResetCardScreen(withBackup = true)
}
step("Check 'Reset card' screen") {
checkResetCardScreen(withBackup = true)
}
step("Check checkbox logic") {
checkCheckBoxLogic(withBackup = true)
}
}
}
}

View file

@ -5,8 +5,9 @@ import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -20,7 +21,7 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingBlockTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val balance = "$3,299.37"
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
@ -35,13 +36,13 @@ class StakingTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -75,7 +76,7 @@ class StakingTest : BaseTestCase() {
@Test
fun validateStakingMoreScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val balance = "$3,299.37"
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
val stakingAmount = "1"
@ -91,13 +92,13 @@ class StakingTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -151,73 +152,73 @@ class StakingTest : BaseTestCase() {
onStakingDetailsScreen { stakeMoreButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
onSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
onSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
onSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
onSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
onSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
onStakingConfirmScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
}
}
}
@ -243,13 +244,13 @@ class StakingTest : BaseTestCase() {
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -306,73 +307,73 @@ class StakingTest : BaseTestCase() {
onStakingDetailsScreen { stakeButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
onSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
onSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
onSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
onSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
onSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
onSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
onSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
onSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
onSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
onSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
onSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
onSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
onSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
onStakingConfirmScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
onStakingConfirmScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
onStakingConfirmScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
onStakingConfirmScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
onStakingConfirmScreen { stakeButton.assertIsDisplayed() }
}
}
}

View file

@ -9,8 +9,9 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -32,13 +33,10 @@ class SwapTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -111,13 +109,10 @@ class SwapTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
@ -149,7 +144,7 @@ class SwapTokenTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("3546")
@AllureId("3547")
@DisplayName("Swap: change network fee")
@Test
fun changeNetworkFeeTest() {
@ -159,13 +154,10 @@ class SwapTokenTest : BaseTestCase() {
val balance = TOTAL_BALANCE
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
openMainScreen()
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }

View file

@ -0,0 +1,180 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.common.utils.getWcUri
import com.tangem.screens.*
import com.tangem.scenarios.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class WalletConnectTest : BaseTestCase() {
@AllureId("3833")
@DisplayName("WC (React App): open session from deeplink on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionOnMainScreen() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
@AllureId("3834")
@DisplayName("WC (React App): open session from deeplink not on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionNotOnMainScreen() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Assert 'Wallet Connect' bottom sheet is displayed") {
onWalletConnectBottomSheet { connectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
@AllureId("886")
@DisplayName("WC (React App): open session from deeplink ")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSession() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Open recent apps") {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.8f)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.scenarios.openMainScreen
import com.tangem.screens.onMainScreen
import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class WarningTest : BaseTestCase() {
@AllureId("898")
@DisplayName("Warnings: check 'Dev card' warning")
@Test
fun devCardWarningTest() {
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen(mockContent = DevWalletMockContent)
}
step("Assert 'Dev card' notification title is displayed") {
onMainScreen { devCardNotificationTitle.assertIsDisplayed() }
}
step("Assert 'Dev card' notification message is displayed") {
onMainScreen { devCardNotificationMessage.assertIsDisplayed() }
}
step("Assert 'Dev card' notification icon is displayed") {
onMainScreen { devCardNotificationIcon.assertIsDisplayed() }
}
}
}
@AllureId("3991")
@DisplayName("Warnings: check 'Dev card' warning is not displayed for release card")
@Test
fun releaseCardWarningTest() {
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Assert 'Dev card' notification title is not displayed") {
onMainScreen { devCardNotificationTitle.assertIsNotDisplayed() }
}
step("Assert 'Dev card' notification message is not displayed") {
onMainScreen { devCardNotificationMessage.assertIsNotDisplayed() }
}
step("Assert 'Dev card' notification icon is not displayed") {
onMainScreen { devCardNotificationIcon.assertIsNotDisplayed() }
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.data
package com.tangem.tap
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.data
package com.tangem.tap.di
import com.tangem.tap.data.FirebasePushNotificationsTokenProvider
import com.tangem.tap.FirebasePushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
@ -10,7 +10,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface PushNotificationsModule {
internal interface GooglePushModule {
@Binds
@Singleton

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name="com.tangem.tap.HuaweiPushService"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true" />
</application>
</manifest>

View file

@ -0,0 +1,50 @@
package com.tangem.tap
import android.content.Context
import com.google.firebase.messaging.FirebaseMessaging
import com.huawei.agconnect.AGConnectOptionsBuilder
import com.huawei.hms.aaid.HmsInstanceId
import com.huawei.hms.common.ApiException
import com.tangem.google.GoogleServicesHelper
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
internal class HuaweiPushNotificationsTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
) : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(context)
return if (isGoogleServicesAvailable) {
try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
""
}
} else {
withContext(coroutineDispatcherProvider.io) {
try {
val appId = AGConnectOptionsBuilder().build(context).getString(APP_ID_KEY)
val token = HmsInstanceId.getInstance(context).getToken(appId, TOKEN_REQUEST_MODE)
Timber.i("Requested token from HuaweiService: $token")
token
} catch (e: ApiException) {
Timber.i("Fetching token from HuaweiService failed cause: ${e.message}")
""
}
}
}
}
companion object {
private const val APP_ID_KEY = "client/app_id"
private const val TOKEN_REQUEST_MODE = "HCM"
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.tap
import android.os.Bundle
import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
import com.tangem.google.GoogleServicesHelper
import com.tangem.tap.common.pushes.PushNotificationDelegate
import timber.log.Timber
class HuaweiPushService : HmsMessageService() {
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
override fun onNewToken(token: String?, bundle: Bundle?) {
super.onNewToken(token, bundle)
Timber.i("HuaweiPushService: On new token from HuaweiService: $token")
}
override fun onTokenError(e: Exception?, bundle: Bundle?) {
super.onTokenError(e, bundle)
Timber.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}")
}
override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message)
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this)
if (isGoogleServicesAvailable) return
val notification = message?.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
pushNotificationDelegate.showNotification(
dataMap = message.dataOfMap,
title = notification.title,
body = notification.body,
channelId = channelId,
priority = message.urgency,
imageUrl = notification.imageUrl,
vibratePattern = notification.vibrateConfig,
)
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.tap.HuaweiPushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface HuaweiPushModule {
@Binds
@Singleton
fun bindPushNotificationsTokenProvider(impl: HuaweiPushNotificationsTokenProvider): PushNotificationsTokenProvider
}

View file

@ -264,6 +264,16 @@
android:host="promo"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="onboard-visa"
android:scheme="tangem" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

View file

@ -1,160 +1,165 @@
{
"coins" : [
"coins": [
{
"id" : "matic-token",
"symbol" : "MATIC",
"name" : "Polygon",
"networks" : [
"id": "matic-token",
"symbol": "MATIC",
"name": "Polygon",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x0000000000000000000000000000000000001010",
"decimalCount": 18
}
]
},
{
"id" : "kaspa",
"symbol" : "KAS",
"name" : "Kaspa",
"networks" : [
"id": "kaspa",
"symbol": "KAS",
"name": "Kaspa",
"networks": [
{
"networkId" : "kaspa/test"
"networkId": "kaspa/test"
}
]
},
{
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"networks" : [
"id": "Dai Stablecoin-DAI",
"symbol": "DAI",
"name": "Dai Stablecoin",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount": 18
}
]
},
{
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"networks" : [
"id": "Dummy ERC20-DERC20",
"symbol": "DERC20",
"name": "Dummy ERC20",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount": 18
}
]
},
{
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"networks" : [
"id": "Ether-ETH",
"symbol": "ETH",
"name": "Ether",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount": 18
}
]
},
{
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"networks" : [
"id": "Test Token-TST",
"symbol": "TST",
"name": "Test Token",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount": 18
}
]
},
{
"id" : "tether",
"symbol" : "USDT",
"name" : "Tether USD",
"networks" : [
"id": "tether",
"symbol": "USDT",
"name": "Tether USD",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
"networkId": "ethereum/test",
"contractAddress": "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount": 6
},
{
"networkId" : "tron/test",
"contractAddress" : "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount" : 6
"networkId": "binance-smart-chain/test",
"contractAddress": "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount" : 18
"networkId": "tron/test",
"contractAddress": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount": 6
},
{
"networkId": "binance-smart-chain/test",
"contractAddress": "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount": 18
}
]
},
{
"id" : "just-gov",
"symbol" : "JST",
"name" : "JUST GOV",
"networks" : [
"id": "just-gov",
"symbol": "JST",
"name": "JUST GOV",
"networks": [
{
"networkId" : "tron/test",
"contractAddress" : "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount" : 18
"networkId": "tron/test",
"contractAddress": "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"networks" : [
"id": "Wrapped Ether-WETH",
"symbol": "WETH",
"name": "Wrapped Ether",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"networks" : [
"id": "Wrapped Matic-WMATIC",
"symbol": "WMATIC",
"name": "Wrapped Matic",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount": 18
}
]
},
{
"id" : "solana",
"symbol" : "SOL",
"name" : "Solana",
"networks" : [
"id": "solana",
"symbol": "SOL",
"name": "Solana",
"networks": [
{
"networkId" : "solana/test"
"networkId": "solana/test"
}
]
},
@ -162,263 +167,262 @@
"id": "the-open-network",
"symbol": "TON",
"name": "Toncoin",
"networks":
[
"networks": [
{
"networkId": "the-open-network/test"
}
]
},
{
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"networks" : [
"id": "Tangem Coin A-TCA",
"symbol": "TCA",
"name": "Tangem Coin A",
"networks": [
{
"networkId" : "solana/test",
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
"networkId": "solana/test",
"contractAddress": "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount": 9
}
]
},
{
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"networks" : [
"id": "Tangem Coin B-TCB",
"symbol": "TCB",
"name": "Tangem Coin B",
"networks": [
{
"networkId" : "solana/test",
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
"networkId": "solana/test",
"contractAddress": "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount": 9
}
]
},
{
"id" : "binancecoin",
"symbol" : "BNB",
"name" : "Binance",
"networks" : [
"id": "binancecoin",
"symbol": "BNB",
"name": "Binance",
"networks": [
{
"networkId" : "binancecoin/test"
"networkId": "binancecoin/test"
},
{
"networkId" : "binance-smart-chain/test"
"networkId": "binance-smart-chain/test"
}
]
},
{
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"networks" : [
"id": "Hemster - 452-HEM",
"symbol": "HEM",
"name": "Hemster - 452",
"networks": [
{
"networkId" : "binancecoin/test",
"contractAddress" : "HEM-452",
"decimalCount" : 8
"networkId": "binancecoin/test",
"contractAddress": "HEM-452",
"decimalCount": 8
}
]
},
{
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"networks" : [
"id": "Binance-Peg BTCB Token-BTCB",
"symbol": "BTCB",
"name": "Binance-Peg BTCB Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"networks" : [
"id": "Binance-Peg BUSD Token-BUSD",
"symbol": "BUSD",
"name": "Binance-Peg BUSD Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"networks" : [
"id": "Binance-Peg USDC Token-USDC",
"symbol": "USDC",
"name": "Binance-Peg USDC Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"networks" : [
"id": "Binance-Peg XRP-XRP",
"symbol": "XRP",
"name": "Binance-Peg XRP",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount": 18
}
]
},
{
"id" : "ethereum",
"symbol" : "ETH",
"name" : "Ethereum",
"networks" : [
"id": "ethereum",
"symbol": "ETH",
"name": "Ethereum",
"networks": [
{
"networkId" : "ethereum/test"
"networkId": "ethereum/test"
}
]
},
{
"id" : "ethereum-classic",
"symbol" : "ETC",
"name" : "Ethereum Classic",
"networks" : [
"id": "ethereum-classic",
"symbol": "ETC",
"name": "Ethereum Classic",
"networks": [
{
"networkId" : "ethereum-classic/test"
"networkId": "ethereum-classic/test"
}
]
},
{
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"networks" : [
"id": "Weenus-WEENUS",
"symbol": "WEENUS",
"name": "Weenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
"networkId": "ethereum/test",
"contractAddress": "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount": 18
}
]
},
{
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"networks" : [
"id": "Xeenus-XEENUS",
"symbol": "XEENUS",
"name": "Xeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
"networkId": "ethereum/test",
"contractAddress": "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount": 18
}
]
},
{
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"networks" : [
"id": "Yeenus-YEENUS",
"symbol": "YEENUS",
"name": "Yeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
"networkId": "ethereum/test",
"contractAddress": "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount": 8
}
]
},
{
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"networks" : [
"id": "Zeenus-ZEENUS",
"symbol": "ZEENUS",
"name": "Zeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
"networkId": "ethereum/test",
"contractAddress": "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount": 0
}
]
},
{
"id" : "avalanche-2",
"symbol" : "AVAX",
"name" : "Avalanche",
"networks" : [
"id": "avalanche-2",
"symbol": "AVAX",
"name": "Avalanche",
"networks": [
{
"networkId" : "avalanche/test"
"networkId": "avalanche/test"
}
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"networks" : [
"id": "The Fuji stablecoin-FUJISTABLE",
"symbol": "FUJISTABLE",
"name": "The Fuji stablecoin",
"networks": [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
"networkId": "avalanche/test",
"contractAddress": "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount": 6
}
]
},
{
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"networks" : [
"id": "To the Moon-FUJIMOON",
"symbol": "FUJIMOON",
"name": "To the Moon",
"networks": [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
"networkId": "avalanche/test",
"contractAddress": "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount": 9
}
]
},
{
"id" : "fantom",
"symbol" : "FTM",
"name" : "Fantom",
"networks" : [
"id": "fantom",
"symbol": "FTM",
"name": "Fantom",
"networks": [
{
"networkId" : "fantom/test"
"networkId": "fantom/test"
}
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"networks" : [
"id": "Fantom USD-FUSD",
"symbol": "FUSD",
"name": "Fantom USD",
"networks": [
{
"networkId" : "fantom/test",
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
"networkId": "fantom/test",
"contractAddress": "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"networks" : [
"id": "Wrapped Fantom-WFTM",
"symbol": "WFTM",
"name": "Wrapped Fantom",
"networks": [
{
"networkId" : "fantom/test",
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
"networkId": "fantom/test",
"contractAddress": "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount": 18
}
]
},
{
"id" : "bitcoin",
"symbol" : "BTC",
"name" : "Bitcoin",
"networks" : [
"id": "bitcoin",
"symbol": "BTC",
"name": "Bitcoin",
"networks": [
{
"networkId" : "bitcoin/test"
"networkId": "bitcoin/test"
}
]
},
{
"id" : "vechain",
"symbol" : "VET",
"name" : "VeChain",
"networks" : [
"id": "vechain",
"symbol": "VET",
"name": "VeChain",
"networks": [
{
"networkId" : "vechain/test"
"networkId": "vechain/test"
}
]
},
@ -435,34 +439,34 @@
]
},
{
"id" : "tron",
"symbol" : "TRX",
"name" : "Tron",
"networks" : [
{
"networkId" : "tron/test"
}
]
"id": "tron",
"symbol": "TRX",
"name": "Tron",
"networks": [
{
"networkId": "tron/test"
}
]
},
{
"id" : "algorand",
"symbol" : "ALGO",
"name" : "Algorand",
"networks" : [
{
"networkId" : "algorand/test"
}
]
"id": "algorand",
"symbol": "ALGO",
"name": "Algorand",
"networks": [
{
"networkId": "algorand/test"
}
]
},
{
"id" : "arbitrum-one",
"symbol" : "ETH",
"name" : "Arbitrum",
"networks" : [
{
"networkId" : "arbitrum-one/test"
}
]
"id": "arbitrum-one",
"symbol": "ETH",
"name": "Arbitrum",
"networks": [
{
"networkId": "arbitrum-one/test"
}
]
},
{
"id": "stellar",
@ -485,12 +489,12 @@
]
},
{
"id" : "polkadot",
"symbol" : "DOT",
"name" : "Polkadot",
"networks" : [
"id": "polkadot",
"symbol": "DOT",
"name": "Polkadot",
"networks": [
{
"networkId" : "polkadot/test"
"networkId": "polkadot/test"
}
]
},
@ -509,8 +513,7 @@
"id": "kava",
"symbol": "KAVA",
"name": "Kava EVM",
"networks":
[
"networks": [
{
"networkId": "kava/test"
}
@ -520,8 +523,7 @@
"id": "telos",
"symbol": "TLOS",
"name": "Telos EVM",
"networks":
[
"networks": [
{
"networkId": "telos/test"
}
@ -531,8 +533,7 @@
"id": "ravencoin",
"symbol": "RVN",
"name": "Ravencoin",
"networks":
[
"networks": [
{
"networkId": "ravencoin/test"
}
@ -542,8 +543,7 @@
"id": "cosmos",
"symbol": "ATOM",
"name": "Cosmos Hub",
"networks":
[
"networks": [
{
"networkId": "cosmos/test"
}
@ -798,6 +798,30 @@
"networkId": "alephium/test"
}
]
},
{
"id": "chainlink",
"name": "Chainlink",
"symbol": "LINK",
"networks": [
{
"networkId": "ethereum/test",
"contractAddress": "0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5",
"decimalCount": 18
}
]
},
{
"id": "gho",
"name": "GHO",
"symbol": "GHO",
"networks": [
{
"networkId": "ethereum/test",
"contractAddress": "0xc4bF5CbDaBE595361438F8c6a187bDc330539c60",
"decimalCount": 18
}
]
}
]
}

View file

@ -29,12 +29,13 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -50,7 +51,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@EntryPoint
@InstallIn(SingletonComponent::class)
@ -71,8 +71,6 @@ interface ApplicationEntryPoint {
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
fun getWalletConnect2Repository(): WalletConnect2Repository
fun getScanCardProcessor(): ScanCardProcessor
fun getAppCurrencyRepository(): AppCurrencyRepository
@ -107,7 +105,7 @@ interface ApplicationEntryPoint {
fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getWalletMetaInfoUseCase(): GetWalletMetaInfoUseCase
fun getUrlOpener(): UrlOpener
@ -151,4 +149,6 @@ interface ApplicationEntryPoint {
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
fun getWcInitializeUseCase(): WcInitializeUseCase
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -22,6 +23,7 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
@Suppress("LongParameterList")
internal class LockUserWalletsTimer(
private val context: Context,
private val settingsRepository: SettingsRepository,
@ -30,6 +32,7 @@ internal class LockUserWalletsTimer(
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val coroutineScope: CoroutineScope,
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@ -55,6 +58,9 @@ internal class LockUserWalletsTimer(
)
if (shouldOpenWelcomeScreenOnResume) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
clearAllHotWalletContextualUnlockUseCase.invoke()
}
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
@ -120,6 +126,7 @@ internal class LockUserWalletsTimer(
start()
}
.onRight {
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}

View file

@ -40,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
@ -49,12 +50,11 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
@ -64,11 +64,9 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
@ -116,9 +114,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var scanCardUseCase: ScanCardUseCase
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@Inject
lateinit var settingsRepository: SettingsRepository
@ -171,9 +166,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var deeplinkFactory: DeepLinkFactory
@Inject
internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles
@Inject
internal lateinit var urlOpener: UrlOpener
@ -183,9 +175,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var intentProcessor: IntentProcessor
@Inject
internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler
@Inject
internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler
@ -198,10 +187,13 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles
@Inject
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
internal val viewModel: MainViewModel by viewModels()
private val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -286,6 +278,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
hotWalletFeatureToggles = hotWalletFeatureToggles,
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
)
initIntentHandlers()
@ -293,7 +286,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
store.dispatch(
DaggerGraphAction.SetActivityDependencies(
scanCardUseCase = scanCardUseCase,
walletConnectInteractor = walletConnectInteractor,
cardSdkConfigRepository = cardSdkConfigRepository,
),
)
@ -369,10 +361,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private fun initIntentHandlers() {
intentProcessor.addHandler(onPushClickedIntentHandler)
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(walletConnectLinkIntentHandler)
}
}
private fun updateAppTheme(appThemeMode: AppThemeMode) {
@ -525,8 +513,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
// Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs
if (tangemPayFeatureToggles.isTangemPayEnabled) {
store.dispatchNavigationAction {
replaceAll(AppRoute.TangemPayDetails)
lifecycleScope.launch {
val selectedUserWalledId = userWalletsListRepository.selectedUserWalletSync()?.walletId
store.dispatchNavigationAction {
replaceAll(AppRoute.TangemPayDetails(requireNotNull(selectedUserWalledId)))
}
}
} else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {

View file

@ -47,7 +47,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -75,9 +75,12 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
import timber.log.Timber
lateinit var store: Store<AppState>
@ -111,9 +114,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val cardScanningFeatureToggles: CardScanningFeatureToggles
get() = entryPoint.getCardScanningFeatureToggles()
private val walletConnect2Repository: WalletConnect2Repository
get() = entryPoint.getWalletConnect2Repository()
private val scanCardProcessor: ScanCardProcessor
get() = entryPoint.getScanCardProcessor()
@ -165,8 +165,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase
get() = entryPoint.getSendFeedbackEmailUseCase()
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase
get() = entryPoint.getWalletMetaInfoUseCase()
private val urlOpener
get() = entryPoint.getUrlOpener()
@ -236,6 +236,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase()
// endregion
private val appScope = MainScope()
@ -289,15 +292,16 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
tangemAppLoggerInitializer.initialize()
Timber.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
}
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
@ -329,7 +333,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
)
appStateHolder.mainStore = store
walletConnect2Repository.init(
wcInitializeUseCase.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
)
}
@ -342,7 +347,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
daggerGraphState = DaggerGraphState(
networkConnectionManager = networkConnectionManager,
cardScanningFeatureToggles = cardScanningFeatureToggles,
walletConnectRepository = walletConnect2Repository,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,
@ -357,7 +361,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
issuersConfigStorage = issuersConfigStorage,
urlOpener = urlOpener,
shareManager = shareManager,

View file

@ -5,9 +5,10 @@ import android.content.Context
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.common.ui.ScanFailsDialog
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.common.ui.SimpleOkDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
@ -59,88 +60,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_not_valid_card,
context = context,
)
is WalletConnectDialog.AddNetwork -> {
val message = context.getString(
R.string.wallet_connect_error_missing_blockchains,
) + state.dialog.networks.joinToString()
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = message,
context = context,
)
}
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_same_wcuri,
context = context,
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_timeout,
context = context,
)
}
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context)
is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context)
is WalletConnectDialog.BnbTransactionDialog ->
BnbTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.UnsupportedNetwork -> {
val warning = if (state.dialog.networks.isNullOrEmpty()) {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks.joinToString()
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = warning,
context = context,
)
}
is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_unsupported_dapp,
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 WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.SignTransactionsDialog -> SignTransactionsDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = state.dialog.error.message,
context = context,
)
is WalletConnectDialog.UnsupportedWcVersion -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.unsupported_wc_version,
context = context,
)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,

View file

@ -1,120 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
/**
[REDACTED_AUTHOR]
*/
internal sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Wallet Connect", event, params) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionInitiated(source: SourceType) : WalletConnect(
event = "Session Initiated",
params = mapOf(
AnalyticsParam.SOURCE to when (source) {
SourceType.QR -> "QR"
SourceType.DEEPLINK -> "DeepLink"
SourceType.CLIPBOARD -> "Clipboard"
SourceType.ETC -> "etc"
},
),
)
data object SessionFailed : WalletConnect(
event = "Session Failed",
)
class DAppConnectionRequested(
blockchainNames: List<String>,
) : WalletConnect(
event = "dApp Connection Requested",
params = mapOf(
AnalyticsParam.NETWORKS to blockchainNames.joinToString(","),
),
)
class DAppConnected(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class DAppConnectionFailed(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connection Failed",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect(
event = "dApp Disconnected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
),
)
class SignatureRequestHandled(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Handled",
params = params.toParamsMap(),
)
class SignatureRequestReceived(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Received",
params = params.toParamsMap(),
)
class SignatureRequestFailed(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Failed",
params = params.toParamsMap(),
)
data class RequestHandledParams(
val dAppName: String,
val dAppUrl: String,
val methodName: String,
val blockchain: String,
val errorCode: String? = null,
val errorDescription: String? = null,
) {
fun toParamsMap(): Map<String, String> {
val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param
val code = errorCode ?: SUCCESS_CODE
return buildMap {
put(AnalyticsParam.DAPP_NAME, dAppName)
put(AnalyticsParam.DAPP_URL, dAppUrl)
put(AnalyticsParam.METHOD_NAME, methodName)
put(AnalyticsParam.BLOCKCHAIN, blockchain)
put(AnalyticsParam.VALIDATION, validation)
put(AnalyticsParam.ERROR_CODE, code)
if (errorDescription != null) {
put(AnalyticsParam.ERROR_DESCRIPTION, errorDescription)
}
}
}
}
enum class Validation(val param: String) {
SUCCESS("Success"),
FAIL("Fail"),
}
private companion object {
const val SUCCESS_CODE = "0"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.common.buildconfig
import com.tangem.utils.buildConfig.AppConfigurationProvider
import com.tangem.wallet.BuildConfig
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class AppConfigurationProviderImpl @Inject constructor() : AppConfigurationProvider {
override fun isDebug(): Boolean = BuildConfig.BUILD_TYPE == "debug"
override fun isHuawei(): Boolean = BuildConfig.FLAVOR_NAME == "huawei"
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.common.pushes
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
class PushNotificationDelegate(private val context: Context) {
@Suppress("LongParameterList")
fun showNotification(
dataMap: Map<String, String>,
title: String?,
body: String?,
channelId: String,
priority: Int,
imageUrl: Uri? = null,
vibratePattern: LongArray?,
) {
val intent = Intent(context, MainActivity::class.java).apply {
dataMap.forEach { (key, value) ->
putExtra(key, value)
}
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
val notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(title)
.setContentText(body)
.setPriority(priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(vibratePattern)
.apply {
imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(context, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
context,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(context)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
}
}

View file

@ -1,30 +1,17 @@
package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
import timber.log.Timber
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
@ -36,73 +23,18 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
val intent = Intent(applicationContext, MainActivity::class.java).apply {
message.data.forEach {
putExtra(it.key, it.value)
}
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
pushNotificationDelegate.showNotification(
dataMap = message.data,
title = notification.title,
body = notification.body,
channelId = channelId,
priority = message.priority,
imageUrl = notification.imageUrl,
vibratePattern = notification.vibrateTimings,
)
val notificationBuilder =
NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(notification.title)
.setContentText(notification.body)
.setPriority(message.priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(notification.vibrateTimings)
.apply {
notification.imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(applicationContext, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
applicationContext,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(applicationContext)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -5,8 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -19,7 +17,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -30,7 +27,6 @@ data class AppState(
logMiddleware,
GlobalMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,

View file

@ -0,0 +1,39 @@
package com.tangem.tap.data
import android.content.Context
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
private const val DEFAULT_KEY = "tangem_pay_default_key"
@Singleton
internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : TangemPayStorage {
private val secureStorage by lazy {
AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "tangem_pay_storage",
)
}
override suspend fun store(authHeader: String) = withContext(dispatcherProvider.io) {
secureStorage.store(authHeader.encodeToByteArray(throwOnInvalidSequence = true), DEFAULT_KEY)
}
override suspend fun get(): String? = withContext(dispatcherProvider.io) {
secureStorage.get(DEFAULT_KEY)?.decodeToString(throwOnInvalidSequence = true)
}
override suspend fun clear() = withContext(dispatcherProvider.io) {
secureStorage.delete(DEFAULT_KEY)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.tap.common.buildconfig.AppConfigurationProviderImpl
import com.tangem.utils.buildConfig.AppConfigurationProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppConfigurationModule {
@Binds
@Singleton
fun bindAppConfigurationProvider(impl: AppConfigurationProviderImpl): AppConfigurationProvider
}

View file

@ -4,7 +4,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,10 +18,6 @@ internal object IntentHandlingModule {
@Singleton
fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler()
@Provides
@Singleton
fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler()
@Provides
@Singleton
fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler =

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.tap.data.DefaultTangemPayStorage
import com.tangem.tap.data.DefaultVisaAuthTokenStorage
import com.tangem.tap.data.DefaultVisaOTPStorage
import dagger.Binds
@ -21,4 +23,8 @@ internal interface VisaStorageModule {
@Binds
@Singleton
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
@Binds
@Singleton
fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
@ -18,8 +18,8 @@ internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase {
return GetCardInfoUseCase(feedbackRepository = feedbackRepository)
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetWalletMetaInfoUseCase {
return GetWalletMetaInfoUseCase(feedbackRepository = feedbackRepository)
}
@Provides

View file

@ -6,7 +6,6 @@ import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -33,29 +32,21 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesFetchNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): FetchNFTCollectionsUseCase = FetchNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@Singleton
fun providesRefreshAllNFTUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshAllNFTUseCase = RefreshAllNFTUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@ -127,16 +118,12 @@ internal object NFTDomainModule {
fun provideDisableWalletNFTUseCase(
walletsRepository: WalletsRepository,
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): DisableWalletNFTUseCase {
return DisableWalletNFTUseCase(
walletsRepository = walletsRepository,
nftRepository = nftRepository,
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -1,11 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.*
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.repository.PushNotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Module
import dagger.Provides
@ -79,12 +76,6 @@ internal object NotificationsDomainModule {
)
}
@Provides
@Singleton
fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles {
return DefaultNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
@Provides
@Singleton
fun provideGetNetworksAvailableForNotifications(

View file

@ -1,11 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module
import dagger.Provides
@ -120,6 +116,12 @@ internal object OnrampDomainModule {
return OnrampSaveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSepaAvailableUseCase(onrampRepository: OnrampRepository): OnrampSepaAvailableUseCase {
return OnrampSepaAvailableUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampUpdateTransactionStatusUseCase(
@ -231,4 +233,34 @@ internal object OnrampDomainModule {
fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase {
return GetLegacyTopUpUrlUseCase(legacyTopUpRepository)
}
@Provides
@Singleton
fun provideGetOnrampAllOffersUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
settingsRepository: SettingsRepository,
): GetOnrampAllOffersUseCase {
return GetOnrampAllOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
settingsRepository = settingsRepository,
)
}
@Provides
@Singleton
fun provideGetOnrampOffersUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
onrampTransactionRepository: OnrampTransactionRepository,
settingsRepository: SettingsRepository,
): GetOnrampOffersUseCase {
return GetOnrampOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
onrampTransactionRepository = onrampTransactionRepository,
settingsRepository = settingsRepository,
)
}
}

View file

@ -54,8 +54,8 @@ internal object SettingsDomainModule {
@Singleton
fun providesShouldShowSaveWalletScreenUseCase(
settingsRepository: SettingsRepository,
): ShouldShowSaveWalletScreenUseCase {
return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository)
): ShouldShowAskBiometryUseCase {
return ShouldShowAskBiometryUseCase(settingsRepository = settingsRepository)
}
@Provides
@ -138,10 +138,8 @@ internal object SettingsDomainModule {
@Provides
@Singleton
fun provideSetSaveWalletScreenShownUseCase(
settingsRepository: SettingsRepository,
): SetSaveWalletScreenShownUseCase {
return SetSaveWalletScreenShownUseCase(settingsRepository = settingsRepository)
fun provideSetSaveWalletScreenShownUseCase(settingsRepository: SettingsRepository): SetAskBiometryShownUseCase {
return SetAskBiometryShownUseCase(settingsRepository = settingsRepository)
}
@Provides

View file

@ -47,7 +47,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
@ -56,7 +55,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -111,13 +109,11 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -156,7 +152,6 @@ internal object TokensDomainModule {
dispatchers: CoroutineDispatcherProvider,
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
@ -165,7 +160,6 @@ internal object TokensDomainModule {
currencyChecksRepository = currencyChecksRepository,
currencyStatusOperations = baseCurrencyStatusOperations,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -177,7 +171,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
@ -186,7 +179,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -214,9 +206,8 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCryptoCurrencyUseCase {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles)
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
@Provides
@ -238,13 +229,11 @@ internal object TokensDomainModule {
fun provideApplyTokenListSortingUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
dispatchers = dispatchers,
)
}
@ -304,14 +293,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideIsCryptoCurrencyCoinCouldHideUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): IsCryptoCurrencyCoinCouldHideUseCase {
return IsCryptoCurrencyCoinCouldHideUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -330,13 +315,11 @@ internal object TokensDomainModule {
fun provideGetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetBalanceNotEnoughForFeeWarningUseCase {
return GetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
dispatchers = dispatchers,
)
}
@ -377,16 +360,12 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -402,18 +381,13 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
@ -422,15 +396,10 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}

View file

@ -6,8 +6,6 @@ import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
@ -63,18 +61,14 @@ internal object TransactionDomainModule {
fun provideAssociateAssetUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
@ -9,8 +10,8 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
@ -25,7 +26,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
@Module
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@ -352,4 +353,72 @@ internal object WalletsDomainModule {
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesIsUpgradeWalletNotificationEnabledUseCase(
walletsRepository: WalletsRepository,
): IsUpgradeWalletNotificationEnabledUseCase {
return IsUpgradeWalletNotificationEnabledUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesDismissUpgradeWalletNotificationUseCase(
walletsRepository: WalletsRepository,
): DismissUpgradeWalletNotificationUseCase {
return DismissUpgradeWalletNotificationUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesUnlockHotWalletContextualUseCase(
hotWalletAccessor: HotWalletAccessor,
): UnlockHotWalletContextualUseCase {
return UnlockHotWalletContextualUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesGetHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): GetHotWalletContextualUnlockUseCase {
return GetHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesClearHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): ClearHotWalletContextualUnlockUseCase {
return ClearHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesClearAllHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): ClearAllHotWalletContextualUnlockUseCase {
return ClearAllHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesExportSeedPhraseUseCase(hotWalletAccessor: HotWalletAccessor): ExportSeedPhraseUseCase {
return ExportSeedPhraseUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.di.hot
import com.tangem.data.wallets.hot.DefaultHotWalletAccessor
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.features.hot.TangemHotSDKProxy
import dagger.Binds
@ -15,4 +17,8 @@ internal interface TangemHotSdkModule {
@Binds
@Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletAccessor(default: DefaultHotWalletAccessor): HotWalletAccessor
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.domain.notifications
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
internal class DefaultNotificationsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : NotificationsFeatureToggles {
override val isNotificationsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED")
}

View file

@ -265,7 +265,7 @@ internal class LegacyScanProcessor @Inject constructor(
onOk = { mainScope.launch { onSuccess() } },
onSupportClick = {
val cardInfo =
store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {

View file

@ -0,0 +1,270 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
object BackupWalletMockContent : MockContent {
private val primaryCard = PrimaryCard(
cardId = "AC05000000086747",
batchId = "AC05",
cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39),
linkingKey = byteArrayOf(
2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121,
-57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91,
),
existingWalletsCount = 3, isHDWalletAllowed = true,
issuer = Card.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
manufacturer = Card.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1743759687),
signature = byteArrayOf(),
),
walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug),
firmwareVersion = FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
isKeysImportAllowed = false,
certificate = null,
)
override val cardDto = CardDTO(
cardId = "AC05000000086747",
batchId = "AC05",
cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39),
firmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 20,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = true,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = true,
isHDWalletAllowed = true,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
EllipticCurve.Bls12381G2,
EllipticCurve.Bls12381G2Aug,
EllipticCurve.Bls12381G2Pop,
EllipticCurve.Bip0340,
),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 0,
hasBackup = true,
derivedKeys = mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14),
),
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
curve = EllipticCurve.Ed25519,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 1,
hasBackup = true,
derivedKeys = emptyMap(),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
),
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.Active(1),
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = null,
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(
entries = mapOf(
ByteArrayKey(
byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch
publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121),
chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge
publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124),
chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
)
override val extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110),
chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
)
override val successResponse = SuccessResponse(cardId = "AC05000000086747")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = mapOf(
ByteArrayKey(
byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
primaryCard = primaryCard,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -0,0 +1,270 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
object DevWalletMockContent : MockContent {
private val primaryCard = PrimaryCard(
cardId = "AC05000000086747",
batchId = "AC05",
cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39),
linkingKey = byteArrayOf(
2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121,
-57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91,
),
existingWalletsCount = 3, isHDWalletAllowed = true,
issuer = Card.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
manufacturer = Card.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1743759687),
signature = byteArrayOf(),
),
walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug),
firmwareVersion = FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Sdk,
),
isKeysImportAllowed = false,
certificate = null,
)
override val cardDto = CardDTO(
cardId = "AC05000000086747",
batchId = "AC05",
cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39),
firmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Sdk,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 20,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = true,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = true,
isHDWalletAllowed = true,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
EllipticCurve.Bls12381G2,
EllipticCurve.Bls12381G2Aug,
EllipticCurve.Bls12381G2Pop,
EllipticCurve.Bip0340,
),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14),
),
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
curve = EllipticCurve.Ed25519,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 1,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
),
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = null,
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(
entries = mapOf(
ByteArrayKey(
byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch
publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121),
chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge
publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124),
chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
)
override val extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110),
chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
)
override val successResponse = SuccessResponse(cardId = "AC05000000086747")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = mapOf(
ByteArrayKey(
byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
primaryCard = primaryCard,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -22,7 +22,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.pins.SetUserCodeCommand
@ -46,7 +46,7 @@ class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) : CardSessionRunnable<VisaCardActivationResponse> {
@ -168,7 +168,7 @@ class VisaCardActivationTask @AssistedInject constructor(
signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either {
val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
val tokens = visaAuthRemoteDataSource.getAccessTokens(signedChallenge)
.getOrElse { raise(it.tangemError) }
visaAuthTokenStorage.store(cardId, tokens)

View file

@ -4,9 +4,5 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.tokens.TokensFeatureToggles
internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isWalletBalanceFetcherEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "WALLET_BALANCE_FETCHER_ENABLED")
}
@Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles

View file

@ -11,21 +11,16 @@ import com.tangem.common.map
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.core.wallets.error.*
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.wallets.R
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.core.wallets.error.DeleteWalletError
import com.tangem.domain.core.wallets.error.LockWalletsError
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.core.wallets.error.SetLockError
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
@ -38,6 +33,8 @@ import com.tangem.utils.ProviderSuspend
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@Suppress("LongParameterList", "LargeClass")
internal class DefaultUserWalletsListRepository(
@ -54,32 +51,40 @@ internal class DefaultUserWalletsListRepository(
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
override val selectedUserWallet = MutableStateFlow<UserWallet?>(null)
private val mutex = Mutex()
override suspend fun load() {
if (userWallets.value != null) return
mutex.withLock {
if (userWallets.value != null) return
if (savePersistentInformation().not()) {
// If we don't save persistent information, we don't need to load user wallets
// and we should clear any existing data
clearPersistentData()
userWallets.value = emptyList()
return
}
val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
publicInformationRepository.getAll()
.map { it.toUserWallets() }
.flatMap { wallets ->
sensitiveInformationRepository.getAll(unsecuredEncryptionKeys)
.map { wallets.updateWith(it) }
}.doOnSuccess {
userWallets.value = it
if (savePersistentInformation().not()) {
// If we don't save persistent information, we don't need to load user wallets
// and we should clear any existing data
clearPersistentData()
updateWallets { emptyList() }
return
}
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId }
?: userWallets.value?.firstOrNull()
val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
publicInformationRepository.getAll()
.map { it.toUserWallets() }
.flatMap { wallets ->
sensitiveInformationRepository.getAll(unsecuredEncryptionKeys)
.map { wallets.updateWith(it) }
}
.doOnSuccess { loadedWallets ->
userWallets.update { toUpdate ->
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId }
?: loadedWallets.firstOrNull()?.also {
selectedUserWalletRepository.set(it.walletId)
}
loadedWallets
}
}
}
}
override suspend fun userWalletsSync(): List<UserWallet> {
@ -118,7 +123,7 @@ internal class DefaultUserWalletsListRepository(
}
// update the userWallets state and add if it doesn't exist
userWallets.update { currentWallets ->
updateWallets { currentWallets ->
val wallets = currentWallets ?: emptyList()
if (wallets.any { it.walletId == userWallet.walletId }) {
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
@ -191,18 +196,17 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.delete(userWalletIds)
val userWalletsBeforeDelete = userWallets.value ?: return@either
userWallets.update { currentWallets ->
currentWallets?.filterNot { it.walletId in userWalletIds }
}
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
userWallets.value?.findAvailableUserWallet(
userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
)
val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() }
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
val newSelected = updatedWallets?.findAvailableUserWallet(
currentWallets.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
)
selectedUserWalletRepository.set(newSelected?.walletId)
newSelected
}
updatedWallets
}
}
@ -248,7 +252,7 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } }
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } }
.doOnFailure { error ->
raise(UnlockWalletError.UnableToUnlock)
}
@ -266,9 +270,14 @@ internal class DefaultUserWalletsListRepository(
raise(UnlockWalletError.ScannedCardWalletNotMatched)
}
saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true)
.mapLeft { UnlockWalletError.UnableToUnlock }
.bind()
val encryptionKey = UserWalletEncryptionKey(
walletId = userWallet.walletId,
encryptionKey = scanResponse.encryptionKey ?: raise(UnlockWalletError.UnableToUnlock),
)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } }
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock) }
}
.doOnFailure {
raise(UnlockWalletError.UserCancelled)
@ -278,7 +287,8 @@ internal class DefaultUserWalletsListRepository(
}
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val userWalletIds = userWalletsSync().map { it.walletId }.toSet()
val userWallets = userWalletsSync()
val userWalletIds = userWallets.map { it.walletId }.toSet()
val biometricKeys = runCatching {
userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse {
@ -291,7 +301,7 @@ internal class DefaultUserWalletsListRepository(
val unlockedWalletsIds = allKeys.map { it.walletId }
val unlockedWallets = unlockedWalletsIds.mapNotNull { id ->
userWalletsSync().firstOrNull { it.walletId == id }
userWallets.firstOrNull { it.walletId == id }
}
// Remove all password attempts for unlocked hot wallets
@ -306,7 +316,7 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
userWallets.update { it?.updateWith(sensitiveInfo) }
updateWallets { userWallets.updateWith(sensitiveInfo) }
}
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
}
@ -318,7 +328,7 @@ internal class DefaultUserWalletsListRepository(
raise(LockWalletsError.NothingToLock)
}
userWallets.update {
updateWallets {
it?.map {
if (it.walletId !in unsecuredWalletIds) {
it.lock()
@ -389,6 +399,17 @@ internal class DefaultUserWalletsListRepository(
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
}
private fun updateWallets(block: (List<UserWallet>?) -> List<UserWallet>?) {
userWallets.update {
val updated = block(it)
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
updated?.find { it.walletId == currentSelected.walletId }
}
updated
}
}
/**
* Find the nearest available wallet that can be selected
*

View file

@ -4,6 +4,7 @@ import com.tangem.common.extensions.calculateSha256
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.models.MobileWallet
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
internal val UserWallet.encryptionKey: ByteArray?
@ -19,6 +20,9 @@ private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
return message.calculateHmacSha256(keyHash)
}
val ScanResponse.encryptionKey: ByteArray?
get() = findPublicKey(this.card.wallets)?.let { calculateEncryptionKey(it) }
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
return wallets.firstOrNull()?.publicKey
}

View file

@ -15,7 +15,7 @@ import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.error.VisaCardScanError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyResponse
@ -26,7 +26,7 @@ import javax.inject.Inject
import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
) {
@ -83,7 +83,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(
val challengeResponse = visaAuthRemoteDataSource.getCardWalletAuthChallenge(
cardId = card.cardId,
// This is the wallet public key, not the address and it's alright, as the API expects it in this format
cardWalletAddress = wallet.publicKey.toHexString(),
@ -122,7 +122,7 @@ internal class VisaCardScanHandler @Inject constructor(
cardWalletAddress: String,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(signedChallenge = signedChallenge)
.getOrElse {
Timber.i("Failed to get Access token for Wallet public key authorization.")
return if (
@ -149,7 +149,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting authorization challenge to sign")
val challengeResponse = visaAuthRepository.getCardAuthChallenge(
val challengeResponse = visaAuthRemoteDataSource.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
).getOrElse {
@ -174,7 +174,7 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(
val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(
signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
salt = attestCardKeyResponse.salt.toHexString(),

View file

@ -1,77 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.registerTypeAdapter
import com.google.gson.GsonBuilder
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.calculateSha256
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.tangem.utils.extensions.stripZeroPlainString
import timber.log.Timber
internal object BnbHelper {
fun createMessageData(order: WcBinanceTransferOrder): BinanceMessageData.Transfer {
val input = order.msgs.first().inputs.first()
val output = order.msgs.first().inputs.first()
val currency =
input.coins.firstOrNull()?.denom ?: Blockchain.Binance
val amount = input.coins
.mapNotNull { if (it.denom == currency) it.amount else null }
.sum()
.toBigDecimal()
.movePointLeft(Blockchain.Binance.decimals())
.stripZeroPlainString()
val gson = GsonBuilder()
.registerTypeAdapter(tradeOrderSerializer)
.serializeNulls()
.create()
return BinanceMessageData.Transfer(
outputAddress = output.address,
amount = amount,
address = input.address,
data = gson.toJson(order).toByteArray().calculateSha256(),
)
}
fun createMessageData(order: WcBinanceTradeOrder): BinanceMessageData.Trade {
val address = order.msgs.first().sender
val tradeData = order.msgs.map {
val price = it.price.toBigDecimal()
.movePointLeft(Blockchain.Binance.decimals())
.stripTrailingZeros()
val quantity = it.quantity.toBigDecimal()
.movePointLeft(Blockchain.Binance.decimals())
.stripTrailingZeros()
val amount = price * quantity
val symbol = it.symbol.substringBefore("-")
TradeData(
price = "$price ${Blockchain.Binance.currency}",
quantity = "$quantity $symbol",
amount = "$amount ${Blockchain.Binance.currency}",
symbol = symbol,
)
}
val gson = GsonBuilder()
.registerTypeAdapter(tradeOrderSerializer)
.serializeNulls()
.create()
val serialized = gson.toJson(order)
Timber.d(serialized)
return BinanceMessageData.Trade(
tradeData = tradeData,
address = address,
data = serialized.toByteArray().calculateSha256(),
)
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
object EthSignHelper {
private val gson: Gson by lazy {
GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create()
}
fun tryToParseEthTypedMessageString(message: String): String? {
return try {
val messageString = message
.replace("\\", "")
.removePrefix("\"")
.removeSuffix("\"")
val messageJson = JsonParser().parse(messageString)
val filteredMap = gson.fromJson<Map<*, *>>(messageJson)
.filterKeys { it == "domain" || it == "message" }
gson.toJson(filteredMap)
} catch (exception: Exception) {
null
}
}
}

View file

@ -1,513 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.*
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect2.domain.TransactionType
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
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.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import org.json.JSONArray
import org.json.JSONObject
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LargeClass")
class WalletConnectSdkHelper {
private val userWalletsListManager by lazy {
store.inject(DaggerGraphState::generalUserWalletsListManager)
}
@Suppress("MagicNumber")
suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData {
val transaction = data.transaction
val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { "Blockchain not found" }
val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) {
"WalletManager not found"
}
walletManager.safeUpdate(isDemoCard())
val wallet = walletManager.wallet
val balance = requireNotNull(wallet.amounts[AmountType.Coin]?.value) {
"Coin balance not found"
}
val decimals = wallet.blockchain.decimals()
val value = (transaction.value ?: "0")
.hexToBigDecimal()
.movePointLeft(decimals)
requireNotNull(value) {
"Transaction amount is null"
}
// TODO move fee calculation to SDK getFee() [REDACTED_JIRA]
val gasLimit = getGasLimitFromTx(value, walletManager, transaction, blockchain)
val gasPrice = getGasPrice(walletManager, transaction)
val feeDecimal = (gasLimit * gasPrice).movePointLeft(decimals)
val total = value + feeDecimal
val feeAmount = Amount(feeDecimal, wallet.blockchain)
val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" }
val fee = if (blockchain.isEvm()) {
// TODO [REDACTED_JIRA]
// workaround for Mantle, remove after [REDACTED_JIRA]
val patchedAmount = if (blockchain == Blockchain.Mantle) {
feeAmount.copy(value = feeAmount.value?.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER))
} else {
feeAmount
}
Fee.Ethereum.Legacy(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger())
} else {
Fee.Common(feeAmount)
}
val transactionData = TransactionData.Uncompiled(
amount = Amount(value, wallet.blockchain),
fee = fee,
sourceAddress = transaction.from,
destinationAddress = destinationAddress,
extras = EthereumTransactionExtras(
callData = CompiledSmartContractCallData(transaction.data.removePrefix(HEX_PREFIX).hexToBytes()),
gasLimit = gasLimit.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
),
)
val dialogData = TransactionRequestDialogData(
dAppName = data.metaName,
dAppUrl = data.metaUrl,
amount = value.toFormattedString(decimals),
feeAmount = feeDecimal.toFormattedString(decimals),
totalAmount = total.toFormattedString(decimals),
balance = balance.toFormattedString(decimals),
isEnoughFundsToSend = balance - total >= BigDecimal.ZERO,
topic = data.topic,
id = data.id,
type = data.type,
)
return WcTransactionData(
type = data.type,
transaction = transactionData,
topic = data.topic,
id = data.id,
walletManager = walletManager,
dialogData = dialogData,
)
}
fun isDemoCard(): Boolean {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return false
return userWallet is UserWallet.Cold && userWallet.scanResponse.isDemoCard()
}
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
}
suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? {
return when (data.type) {
WcEthTransactionType.EthSendTransaction -> sendTransaction(data, cardId)
WcEthTransactionType.EthSignTransaction -> signTransaction(data, cardId)
}
}
private suspend fun getGasPrice(walletManager: WalletManager, transaction: WcEthereumTransaction): BigDecimal {
val txGasPrice = transaction.gasPrice?.hexToBigDecimal()
if (txGasPrice != null) {
return txGasPrice
}
return when (val result = (walletManager as? EthereumGasLoader)?.getGasPrice()) {
is Result.Success -> result.data.toBigDecimal()
is Result.Failure -> {
(result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") }
error("Unable to get gas price: ${result.error}")
}
null -> error("Gas price is null")
}
}
private suspend fun getGasLimitFromTx(
value: BigDecimal,
walletManager: WalletManager,
transaction: WcEthereumTransaction,
blockchain: Blockchain,
): BigDecimal {
return transaction.gas?.hexToBigDecimal()
?: transaction.gasLimit?.hexToBigDecimal()
?: getGasLimitFromBlockchain(
value = value,
walletManager = walletManager,
transaction = transaction,
).increaseForMantleIfNeeded(blockchain)
}
private suspend fun getGasLimitFromBlockchain(
value: BigDecimal,
walletManager: WalletManager,
transaction: WcEthereumTransaction,
): BigDecimal {
val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit(
amount = Amount(value, walletManager.wallet.blockchain),
destination = transaction.to ?: "",
callData = CompiledSmartContractCallData(transaction.data.hexToBytes()),
)
return when (gasLimitResult) {
is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2"))
is Result.Failure -> {
(gasLimitResult.error as? Throwable)?.let { Timber.e(it, "getGasLimit failed") }
DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided
}
else -> DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided
}
}
// TODO Workaround for Mantle. Remove after [REDACTED_JIRA]
private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal {
return if (blockchain == Blockchain.Mantle) {
this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
} else {
this
}
}
private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? {
val sdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
signer = store.inject(DaggerGraphState::transactionSignerFactory).createTransactionSigner(
cardId = cardId,
sdk = sdk,
twinKey = null, // use null here because twin doesn't support WC
),
)
return when (result) {
is Result.Success -> {
val hash = result.data.hash
if (hash.startsWith(HEX_PREFIX)) {
hash
} else {
HEX_PREFIX + hash
}
}
is Result.Failure -> {
Timber.e(result.error as BlockchainSdkError)
null
}
}
}
private suspend fun signTransaction(data: WcTransactionData, cardId: String?): String? {
val dataToSign = EthereumUtils.buildTransactionToSign(
transactionData = data.transaction,
blockchain = data.walletManager.wallet.blockchain,
)
val command = SignHashCommand(
hash = dataToSign.hash,
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath,
)
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
initialMessage = Message(),
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
transactionToSign = dataToSign,
walletPublicKey = data.walletManager.wallet.publicKey,
blockchain = data.walletManager.wallet.blockchain,
).toHexString()
if (hash.startsWith(HEX_PREFIX)) {
hash
} else {
HEX_PREFIX + hash
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade {
return BnbHelper.createMessageData(data)
}
fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer {
return BnbHelper.createMessageData(data)
}
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 = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
initialMessage = Message(),
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
getBnbResultString(
key.toHexString(),
result.data.signature.toHexString(),
)
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
fun prepareDataForPersonalSign(
message: WcSignMessage,
topic: String,
metaName: String,
id: Long,
): WcPersonalSignData {
val messageData = when (message.type) {
WcSignMessage.WCSignType.MESSAGE,
WcSignMessage.WCSignType.PERSONAL_MESSAGE,
-> createMessageData(message)
WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58()
}
val messageString = message.data.hexToAscii()
?: EthSignHelper.tryToParseEthTypedMessageString(message.data)
?: message.data
val dialogData = PersonalSignDialogData(
dAppName = metaName,
message = messageString,
topic = topic,
id = id,
)
return WcPersonalSignData(
hash = requireNotNull(messageData) { "Message data must not be null" },
topic = topic,
id = id,
dialogData = dialogData,
type = message.type,
)
}
private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data)
private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this)
suspend fun signPersonalMessage(
hashToSign: ByteArray,
networkId: String,
type: WcSignMessage.WCSignType,
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(
runnable = command,
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
return when (type) {
WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash)
else -> UnmarshalHelper.unmarshalSignatureExtended(
signature = signedHash,
hash = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().formatHex()
.lowercase() // use lowercase because some dapps cant handle UPPERCASE
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
/**
* Returns result of signing prepared to send in WC request
*/
suspend fun signTransaction(
hashToSign: ByteArray,
networkId: String,
type: TransactionType,
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(
runnable = command,
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
when (type) {
TransactionType.SOLANA_TX -> {
getSolanaResultString(signedHash)
}
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
/**
* Returns result of signing prepared to send in WC request
*/
suspend fun signTransactions(
hashesToSign: List<ByteArray>,
networkId: String,
type: TransactionType,
derivationPath: String?,
cardId: String?,
): String? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
val sdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
val signer = store.inject(DaggerGraphState::transactionSignerFactory).createTransactionSigner(
cardId = cardId,
sdk = sdk,
twinKey = null,
)
return when (val signingResult = signer.sign(hashesToSign, wallet.publicKey)) {
is CompletionResult.Failure -> {
Timber.e(signingResult.error.customMessage)
null
}
is CompletionResult.Success -> {
when (type) {
TransactionType.SOLANA_TX -> {
val result = signingResult.data.mapIndexed { index, bytes ->
byteArrayOf(1) + bytes + hashesToSign[index]
}
getSolanaResultTxHashesString(result)
}
}
}
else -> {
null
}
}
}
private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }"
/**
* Build json object
* {
* "transactions": [
* "signed_tx_hash"
* ]
* }
*/
private fun getSolanaResultTxHashesString(signedHashes: List<ByteArray>): String {
val result = JSONObject()
val transactions = JSONArray()
signedHashes.forEach {
transactions.put(it.encodeBase64())
}
result.put("transactions", transactions)
return result.toString()
}
private fun getBnbResultString(publicKey: String, signature: String) =
"{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
private companion object {
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
// TODO remove after [REDACTED_JIRA]
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.tap.domain.walletconnect2.app
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.domain.walletconnect.model.legacy.Account
internal class TangemWcBlockchainHelper : WcBlockchainHelper {
private val supportedNonEvmBlockchains = setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.toNetworkId()
}
override fun chainIdsToBlockchains(chainIds: List<String>): List<Blockchain> {
return chainIds.mapNotNull {
it.parseId()?.chainIdToBlockchain()
}.distinct()
}
override fun chainIdToMissingNetworkNameOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.fullName
?: if (parsedId.first == EVM_NAMESPACE) {
chainId
} else {
parsedId.first.replaceFirstChar(Char::titlecase)
}
}
override fun networkIdToChainIdOrNull(networkId: String): List<String> {
val blockchain = Blockchain.fromNetworkId(networkId)
val namespace = blockchain?.getCaip2Namespace() ?: return emptyList()
return blockchain.getCaip2ChainIds().map {
"$namespace$CHAIN_SEPARATOR$it"
}
}
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
}
override fun chainIdsToAccounts(
walletAddress: String,
chainIds: List<String>,
derivationPath: String?,
): List<Account> {
return chainIds.map { chainId ->
Account(chainId, walletAddress, derivationPath)
}
}
private fun Blockchain.getCaip2ChainIds(): List<String> {
if (this.isEvm()) return listOfNotNull(this.getChainId()?.toString())
return when (this) {
/*
* The WC sample application and documentation use a commented out chain ID. However, in real dApps
* uncommented is used.
* Docs: https://docs.walletconnect.com/advanced/multichain/chain-list
*
* */
Blockchain.Solana -> listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")
Blockchain.SolanaTestnet -> listOf("z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z")
Blockchain.Polkadot -> listOf("91b171bb158e2d3848fa23a9f1c25182")
Blockchain.Tron -> listOf("0x2b6653dc")
else -> emptyList()
}
}
private fun Blockchain.getCaip2Namespace(): String? {
return when {
this.isEvm() -> EVM_NAMESPACE
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR)
else -> null
}
}
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
SOLANA_NAMESPACE -> Blockchain.Solana
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
}
}
}
private companion object {
const val EVM_NAMESPACE = "eip155"
const val SOLANA_NAMESPACE = "solana"
const val CHAIN_SEPARATOR = ":"
const val TESTNET_SEPARATOR = "/"
}
}

View file

@ -1,53 +0,0 @@
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
internal 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(proposal)) },
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)
}
override fun onPairConnectError(error: Throwable) {
store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error))
}
}

View file

@ -1,686 +0,0 @@
package com.tangem.tap.domain.walletconnect2.data
import android.app.Application
import arrow.core.flatten
import com.reown.android.Core
import com.reown.android.CoreClient
import com.reown.android.relay.ConnectionType
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.UnsupportedDApps
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
import timber.log.Timber
internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
private val stub: LegacyWalletConnectRepositoryStub,
private val legacy: DefaultLegacyWalletConnectRepository,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val wcInitializeUseCase: WcInitializeUseCase,
private val wcPairService: WcPairService,
) : LegacyWalletConnectRepository {
private val isNewWc by lazy {
walletConnectFeatureToggles.isRedesignedWalletConnectEnabled
}
override val events: Flow<WalletConnectEvents> by lazy {
if (isNewWc) stub.events else legacy.events
}
override val activeSessions: Flow<List<WalletConnectSession>> by lazy {
if (isNewWc) stub.activeSessions else legacy.activeSessions
}
override val currentSessions: List<WalletConnectSession> by lazy {
if (isNewWc) stub.currentSessions else legacy.currentSessions
}
override fun init(projectId: String) {
if (isNewWc) {
wcInitializeUseCase.init(projectId)
} else {
legacy.init(projectId)
}
}
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) {
if (isNewWc) stub.setUserNamespaces(userNamespaces) else legacy.setUserNamespaces(userNamespaces)
}
override fun updateSessions() {
if (isNewWc) stub.updateSessions() else legacy.updateSessions()
}
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
val src = when (source) {
SourceType.QR -> WcPairRequest.Source.QR
SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK
SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD
SourceType.ETC -> WcPairRequest.Source.ETC
}
if (isNewWc) {
wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId))
} else {
legacy.pair(userWalletId = userWalletId, uri = uri, source = source)
}
}
override fun disconnect(topic: String) {
if (isNewWc) stub.disconnect(topic) else legacy.disconnect(topic)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
if (isNewWc) stub.approve(userNamespaces, blockchainNames) else legacy.approve(userNamespaces, blockchainNames)
}
override fun reject() {
if (isNewWc) stub.reject() else legacy.reject()
}
override fun sendRequest(requestData: RequestData, result: String) {
if (isNewWc) stub.sendRequest(requestData, result) else legacy.sendRequest(requestData, result)
}
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) {
if (isNewWc) stub.rejectRequest(requestData, error) else legacy.rejectRequest(requestData, error)
}
override fun cancelRequest(topic: String, id: Long, message: String) {
if (isNewWc) stub.cancelRequest(topic, id, message) else legacy.cancelRequest(topic, id, message)
}
}
internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository {
override val events: Flow<WalletConnectEvents> = emptyFlow()
override val activeSessions: Flow<List<WalletConnectSession>> = emptyFlow()
override val currentSessions: List<WalletConnectSession> = listOf()
override fun init(projectId: String) = Unit
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) = Unit
override fun updateSessions() = Unit
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit
override fun disconnect(topic: String) = Unit
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) = Unit
override fun reject() = Unit
override fun sendRequest(requestData: RequestData, result: String) = Unit
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) = Unit
override fun cancelRequest(topic: String, id: Long, message: String) = Unit
}
@Suppress("LargeClass")
internal class DefaultLegacyWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
) : LegacyWalletConnectRepository {
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
private val blockchainHelper by lazy { TangemWcBlockchainHelper() }
override var currentSessions: List<WalletConnectSession> = emptyList()
private set
/**
* @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.e("Error while initializing client: $error")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(error.throwable.message),
),
)
}
}
WalletKit.initialize(
Wallet.Params.Init(core = CoreClient),
onSuccess = {
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(error.throwable.message),
),
)
}
},
)
}
private fun defineWalletDelegate(): WalletKit.WalletDelegate {
return object : WalletKit.WalletDelegate {
@Suppress("LongMethod")
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.i("sessionProposal: $sessionProposal")
this@DefaultLegacyWalletConnectRepository.sessionProposal = sessionProposal
if (sessionProposal.name in UnsupportedDApps.list) {
Timber.i("Unsupported DApp")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.UnsupportedDApp,
),
)
}
return
}
val missingNetworks = findMissingNetworks(
namespaces = sessionProposal.requiredNamespaces,
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
)
if (missingNetworks.isNotEmpty()) {
Timber.i("Not added blockchains: $missingNetworks")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()),
),
)
}
return
}
val optionalMissingNetwork = findMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
)
val optionalWithoutMissingNetworks = removeMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
)
// for cases when optionalNamespaces is not empty but we doesn't support none of them
if (optionalMissingNetwork.isNotEmpty() &&
optionalWithoutMissingNetworks.isEmpty() &&
sessionProposal.requiredNamespaces.isEmpty() // if requiredNamespaces is not empty we can connect
) {
Timber.i("Not added optional blockchains: $optionalMissingNetwork")
val unsupportedNetworks = sessionProposal.optionalNamespaces.values
.flatMap { it.chains ?: emptyList() }
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
scope.launch {
val error = if (unsupportedNetworks.isNotEmpty()) {
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks),
)
} else {
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ApprovalErrorMissingNetworks(optionalMissingNetwork.toList()),
)
}
_events.emit(error)
}
return
}
val requiredChainIds = sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() }
val optionalChainIds = optionalWithoutMissingNetworks.toList()
val networks = (requiredChainIds + optionalChainIds)
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.distinct()
analyticsHandler.send(WalletConnect.DAppConnectionRequested(networks))
scope.launch {
_events.emit(
WalletConnectEvents.SessionProposal(
sessionProposal.name,
sessionProposal.description,
sessionProposal.url,
sessionProposal.icons,
requiredChainIds,
optionalChainIds,
),
)
}
}
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
Timber.i("sessionRequest: $sessionRequest")
val request = wcRequestDeserializer.deserialize(
method = sessionRequest.request.method,
params = sessionRequest.request.params,
)
Timber.i("sessionRequestParsed: $request")
when (request) {
is WcRequest.AddChain -> {
// we can send approval automatically, because in WC 2.0 the list of chains is approved when
// initial connection is established
sendRequest(
RequestData(
topic = sessionRequest.topic,
requestId = sessionRequest.request.id,
blockchain = sessionRequest.chainId.toString(),
method = WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code,
),
result = "",
)
}
else -> {
val event = WalletConnect.SignatureRequestReceived(
WalletConnect.RequestHandledParams(
dAppName = sessionRequest.peerMetaData?.name ?: "",
dAppUrl = sessionRequest.peerMetaData?.url ?: "",
methodName = sessionRequest.request.method,
blockchain = sessionRequest.chainId
?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "",
),
)
analyticsHandler.send(event)
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 ?: "",
method = sessionRequest.request.method,
),
)
}
}
}
}
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))
updateSessionsInternal().join()
}
}
Timber.i("onSessionDelete: $sessionDelete")
}
override fun onSessionExtend(session: Wallet.Model.Session) {
Timber.i("onSessionExtend: $session")
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
// Triggered when wallet receives the session settlement response from Dapp
Timber.i("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(),
),
)
updateSessionsInternal().join()
}
}
}
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
// Triggered when wallet receives the session update response from Dapp
Timber.i("onSessionUpdateResponse: $sessionUpdateResponse")
updateSessionsInternal()
}
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
// Triggered whenever the connection state is changed
Timber.i("onConnectionStateChange: $state")
if (state.isAvailable) updateSessionsInternal()
}
override fun onError(error: Wallet.Model.Error) {
// Triggered whenever there is an issue inside the SDK
Timber.i("onError: $error")
}
}
}
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) {
this.userNamespaces = userNamespaces
}
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair(
params = Wallet.Params.Pair(uri),
onSuccess = {
Timber.i("Paired successfully: $it")
},
onError = {
Timber.e("Error while pairing: $it")
analyticsHandler.send(WalletConnect.SessionFailed)
scope.launch {
_events.emit(
WalletConnectEvents.PairConnectError(it.throwable),
)
}
},
)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal)
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 preparedRequiredNamespaces = sessionProposal.requiredNamespaces
.map { requiredNamespace ->
val accountsRequired = requiredNamespace.value.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val optionalNamespace = sessionProposal.optionalNamespaces[requiredNamespace.key]
val accountsOptional = optionalNamespace?.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val methods = (requiredNamespace.value.methods + (optionalNamespace?.methods ?: emptyList()))
.distinct()
requiredNamespace.key to Wallet.Model.Namespace.Session(
accounts = (accountsRequired + accountsOptional).distinct(),
methods = methods,
events = requiredNamespace.value.events,
)
}.toMap()
val sessionApproval = Wallet.Params.SessionApprove(
proposerPublicKey = sessionProposal.proposerPublicKey,
namespaces = preparedRequiredNamespaces.ifEmpty {
sessionProposal.createPreparedOptionalNamespaces(userChains)
}.filterValues { it.accounts.isNotEmpty() && it.chains?.isNotEmpty() == true },
)
Timber.i("Session approval is prepared for sending: $sessionApproval")
WalletKit.approveSession(
params = sessionApproval,
onSuccess = {
Timber.i("Approved successfully: $it")
analyticsHandler.send(
WalletConnect.DAppConnected(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
},
onError = {
Timber.e("Error while approving: $it")
analyticsHandler.send(
WalletConnect.DAppConnectionFailed(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(it.throwable.message),
),
)
}
},
)
}
private fun Wallet.Model.SessionProposal.createPreparedOptionalNamespaces(
userChains: Map<String, Set<String>>,
): Map<String, Wallet.Model.Namespace.Session> {
return optionalNamespaces
.map { optionalNamespace ->
val accountsOptional = optionalNamespace.value.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val methods = optionalNamespace.value.methods
optionalNamespace.key to Wallet.Model.Namespace.Session(
accounts = accountsOptional.distinct(),
methods = methods,
chains = userChains.keys
.filter { it.startsWith(optionalNamespace.key) }
.toList(),
events = optionalNamespace.value.events,
)
}
.toMap()
}
override fun sendRequest(requestData: RequestData, result: String) {
val session = currentSessions.find { it.topic == requestData.topic }
// Add Ethereum Chain method is processed without user input, skip logging it
if (requestData.method != WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code) {
analyticsHandler.send(
WalletConnect.SignatureRequestHandled(
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
),
),
)
}
val params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = requestData.requestId,
result = result,
),
)
Timber.i("Session request response: $params")
WalletKit.respondSessionRequest(
params = params,
onSuccess = { response ->
Timber.i("Session request responded successfully: $response")
},
onError = { error ->
Timber.e(error.throwable, "Error while responging session request")
val handledParams = WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = WalletConnectError.ValidationError.error,
errorDescription = error.throwable.message,
)
analyticsHandler.send(WalletConnect.SignatureRequestFailed(handledParams))
},
)
}
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) {
val session = currentSessions.find { it.topic == requestData.topic }
analyticsHandler.send(
WalletConnect.SignatureRequestFailed(
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = error.toString(),
),
),
)
cancelRequest(requestData.topic, requestData.requestId, error.error)
}
override fun cancelRequest(topic: String, id: Long, message: String) {
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
id = id,
code = 0,
message = message,
),
),
onSuccess = {},
onError = {},
)
}
override fun reject() {
WalletKit.rejectSession(
params = Wallet.Params.SessionReject(
proposerPublicKey = sessionProposal?.proposerPublicKey ?: "",
reason = "",
),
onSuccess = {
Timber.i("Rejected successfully: $it")
},
onError = {
Timber.e("Error while rejecting: $it")
},
)
}
override fun disconnect(topic: String) {
val session = currentSessions.find { it.topic == topic }
WalletKit.disconnectSession(
params = Wallet.Params.SessionDisconnect(topic),
onSuccess = {
analyticsHandler.send(
WalletConnect.SessionDisconnected(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
),
)
updateSessionsInternal()
Timber.i("Disconnected successfully: $it")
},
onError = {
Timber.e("Error while disconnecting: $it")
},
)
}
fun send(topic: String, id: Long, data: String) {
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = id,
result = data,
),
),
onError = {},
onSuccess = {},
)
}
override fun updateSessions() {
updateSessionsInternal()
}
private fun updateSessionsInternal(): Job = scope.launch {
val availableSessions = WalletKit.getListOfActiveSessions()
.map {
WalletConnectSession(
topic = it.topic,
icon = it.metaData?.icons?.firstOrNull(),
name = it.metaData?.name,
url = it.metaData?.url,
)
}
Timber.i("Available sessions: $availableSessions")
currentSessions = 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())
}
private fun removeMissingNetworks(
namespaces: Map<String, Wallet.Model.Namespace.Proposal>,
userNamespaces: Map<NetworkNamespace, List<Account>>,
): Collection<String> {
val wcProvidedChains = namespaces.values.flatMap { it.chains ?: emptyList() }
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return wcProvidedChains.intersect(userChains.toSet())
}
}

View file

@ -1,57 +0,0 @@
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.files.FileReader
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.model.legacy.Session
import timber.log.Timber
internal class DefaultWalletConnectSessionsRepository(
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.d(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

@ -1,110 +0,0 @@
package com.tangem.tap.domain.walletconnect2.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
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.DefaultLegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepositoryFacade
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.data.LegacyWalletConnectRepositoryStub
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.DefaultWalletConnectFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object WalletConnectInteractorModule {
@Provides
@Singleton
fun provideWalletConnectInteractor(
wcRepository: LegacyWalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(),
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = coroutineDispatcherProvider,
walletConnectFeatureToggles = walletConnectFeatureToggles,
)
}
}
@Module
@InstallIn(SingletonComponent::class)
internal object WalletConnectModule {
@Provides
@Singleton
fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles {
return DefaultWalletConnectFeatureToggles(featureTogglesManager)
}
@Provides
@Singleton
fun provideWalletConnectRepository(
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
wcInitializeUseCase: WcInitializeUseCase,
wcPairService: WcPairService,
): LegacyWalletConnectRepository {
val legacy = DefaultLegacyWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
)
val stub = LegacyWalletConnectRepositoryStub()
return DefaultLegacyWalletConnectRepositoryFacade(
stub,
legacy,
walletConnectFeatureToggles,
wcInitializeUseCase,
wcPairService,
)
}
@Provides
@Singleton
fun provideWalletConnectSessionsRepository(
@SdkMoshi moshi: Moshi,
fileReader: FileReader,
): WalletConnectSessionsRepository {
return DefaultWalletConnectSessionsRepository(
moshi = moshi,
fileReader = fileReader,
)
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.flow.Flow
interface LegacyWalletConnectRepository {
val events: Flow<WalletConnectEvents>
val activeSessions: Flow<List<WalletConnectSession>>
val currentSessions: List<WalletConnectSession>
fun init(projectId: String)
fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>)
fun updateSessions()
fun pair(userWalletId: UserWalletId, uri: String, source: SourceType)
fun disconnect(topic: String)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>)
fun reject()
fun sendRequest(requestData: RequestData, result: String)
fun rejectRequest(requestData: RequestData, error: WalletConnectError)
fun cancelRequest(topic: String, id: Long, message: String = "")
}

View file

@ -1,21 +0,0 @@
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()
fun onPairConnectError(error: Throwable)
}

View file

@ -1,457 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import arrow.core.flatten
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.util.Stack
@Suppress("LargeClass", "LongParameterList")
class WalletConnectInteractor(
private val handler: WalletConnectEventsHandler,
private val walletConnectRepository: LegacyWalletConnectRepository,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
val blockchainHelper: WcBlockchainHelper,
) {
private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled }
private var isWalletConnectReadyForDeepLinks = false
private val wcScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
},
)
private val listenerScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
},
)
/** Stack of deeplinks to handle if user wallet is not selected or cryptocurrency statuses are not available */
private val deeplinkStack: Stack<String> = Stack()
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,
)
init {
getSelectedWalletUseCase().onRight { userWalletFlow ->
userWalletFlow
.conflate()
.distinctUntilChanged()
.onEach(::initWithWallet)
.flowOn(dispatchers.io)
.launchIn(wcScope)
}
}
private fun initWithWallet(userWallet: UserWallet) {
if (isNewWc) return
if (userWallet.isMultiCurrency) {
Timber.i("WalletConnect: initialize and setup networks for ${userWallet.walletId}")
startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet))
subscribeOnCurrenciesUpdates(userWallet)
}
}
private fun subscribeOnCurrenciesUpdates(userWallet: UserWallet) {
currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWallet.walletId)
.conflate()
.distinctUntilChanged()
.onEach { currencies ->
setupUserChains(userWallet, currencies)
}
.flowOn(dispatchers.io)
.launchIn(wcScope)
}
private suspend fun setupUserChains(userWallet: UserWallet, currencies: List<CryptoCurrency>) {
val accounts = getAccountsForWc(
userWallet = userWallet,
networks = currencies.map { it.network }.distinct(),
)
setUserChains(accounts)
handleDeeplinkStack(accounts)
}
private fun startListeningWc(userWalletId: String, cardId: String?) {
this.userWalletId = userWalletId
this.cardId = cardId
listenerScope.coroutineContext.cancelChildren()
listenerScope.launch {
launch { subscribeToEvents() }
launch { subscribeToSessions() }
walletConnectRepository.updateSessions()
}
}
private fun setUserChains(accounts: List<Account>) {
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
.groupBy { account ->
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
?.let { NetworkNamespace(it) }
}.filterNotNull()
walletConnectRepository.setUserNamespaces(userNamespaces)
}
private fun handleDeeplinkStack(accounts: List<Account>) {
runCatching {
if (accounts.isEmpty()) return
isWalletConnectReadyForDeepLinks = true
if (deeplinkStack.empty()) return
val lastDeeplink = deeplinkStack.pop()
val action = WalletConnectAction
.OpenSession(
wcUri = lastDeeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
}.onFailure {
Timber.e("WC deeplink handling failed. $it")
}
}
private suspend fun subscribeToEvents() {
events
.onEach { wcEvent ->
Timber.i("WalletConnect: event: $wcEvent")
when (wcEvent) {
is WalletConnectEvents.SessionProposal -> {
val unsupportedNetworks = wcEvent.requiredChainIds
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
if (unsupportedNetworks.isNotEmpty()) {
val error = WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks)
handler.onSessionRejected(error)
return@onEach
}
val networksFormatted = (wcEvent.requiredChainIds + wcEvent.optionalChainIds)
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.distinct()
.toString()
handler.onProposalReceived(proposal = wcEvent, networksFormatted = networksFormatted)
}
is WalletConnectEvents.SessionApprovalError -> {
val error = when (wcEvent.error) {
is WalletConnectError.ApprovalErrorMissingNetworks -> {
val missingNetworks = wcEvent.error.missingChains.mapNotNull {
blockchainHelper.chainIdToMissingNetworkNameOrNull(it)
}
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks)
}
else -> wcEvent.error
}
handler.onSessionRejected(error)
}
is WalletConnectEvents.SessionApprovalSuccess -> {
sessionsRepository.saveSession(
userWallet = userWalletId,
session = Session(
accounts = wcEvent.accounts,
topic = wcEvent.topic,
),
)
walletConnectRepository.updateSessions()
handler.onSessionEstablished()
}
is WalletConnectEvents.SessionDeleted -> {
sessionsRepository.removeSession(userWalletId, wcEvent.topic)
}
is WalletConnectEvents.SessionRequest -> {
handleRequest(wcEvent)
}
is WalletConnectEvents.PairConnectError -> {
handler.onPairConnectError(wcEvent.error)
}
}
}
.flowOn(dispatchers.io)
.collect()
}
private suspend fun subscribeToSessions() {
sessions
.onEach { listOfSessions ->
val relevantTopics = getTopicsForUserWallet(userWalletId, sessionsRepository)
val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics)
handler.onListOfSessionsUpdated(filteredSessions)
}
.flowOn(dispatchers.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>) {
if (isNewWc) return
Timber.i("Approve session proposal: $accounts")
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
.groupBy { account ->
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
?.let { NetworkNamespace(it) }
}.filterNotNull()
val blockchainNames = blockchainHelper.chainIdsToBlockchains(userNamespaces.values.flatten().map { it.chainId })
.map { it.fullName }
walletConnectRepository.approve(
userNamespaces = userNamespaces,
blockchainNames = blockchainNames,
)
}
fun rejectSessionProposal() {
if (isNewWc) return
Timber.i("Reject session proposal")
walletConnectRepository.reject()
}
fun disconnectSession(topic: String) {
if (isNewWc) return
Timber.i("Disconnect session: $topic")
walletConnectRepository.disconnect(topic)
}
fun cancelRequest(topic: String, id: Long) {
if (isNewWc) return
Timber.i("Cancel request: $topic, $id")
walletConnectRepository.cancelRequest(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
}
}
val networkId = sessionRequest.chainId?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: ""
val requestData = RequestData(
topic = sessionRequest.topic,
requestId = sessionRequest.id,
blockchain = networkId,
method = sessionRequest.method,
)
if (error != null) {
walletConnectRepository.rejectRequest(requestData, error)
return
}
when (sessionRequest.request) {
is WcRequest.BnbCancel -> Unit
is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest(
requestData = requestData,
result = "",
)
else -> {
currentRequest = sessionRequest
val data = prepareRequestData(sessionRequest).getOrElse { e ->
val wrappedError = e as? WalletConnectError ?: WalletConnectError.UnknownError(
message = e.localizedMessage ?: "Unknown error",
)
walletConnectRepository.rejectRequest(requestData, wrappedError)
handler.onSessionRejected(wrappedError)
return
}
handler.onSessionRequest(data)
}
}
}
suspend fun continueWithRequest(request: WcPreparedRequest) {
if (isNewWc) return
val currentRequest = this.currentRequest
if (currentRequest == null || request.topic != currentRequest.topic) return
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return
val signingResultData = 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,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
is WcPreparedRequest.SolanaSignTransaction -> sdkHelper.signTransaction(
hashToSign = request.preparedRequestData.hashToSign,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
is WcPreparedRequest.SolanaSignMultipleTransactions -> sdkHelper.signTransactions(
hashesToSign = request.preparedRequestData.hashesToSign,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
}
val requestData = RequestData(
topic = request.topic,
requestId = request.requestId,
blockchain = networkId,
method = currentRequest.method,
)
if (signingResultData == null) {
walletConnectRepository.rejectRequest(requestData, WalletConnectError.SigningError)
} else {
walletConnectRepository.sendRequest(
requestData = requestData,
result = signingResultData,
)
}
}
/**
* Handles Wallet Connect deep links.
* If wallet connect is able to handle the deeplink, session is started with deeplink.
* Otherwise, deeplink is stored until wallet connect is ready to handle it.
*
* @param deeplink deeplink to handle
*/
fun addDeeplink(deeplink: String) {
if (isNewWc) return
val deeplinkRegex = Regex(WC_PARAM_REGEX)
val matched = deeplinkRegex.findAll(deeplink)
val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull()
val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session ->
session.topic == sessionTopic
}
if (isAlreadyActiveSessionTopic && sessionTopic != null) {
Timber.i("WC already has an active session topic: $deeplink")
return
}
if (isWalletConnectReadyForDeepLinks) {
val action = WalletConnectAction.OpenSession(
wcUri = deeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
} else {
deeplinkStack.push(deeplink)
}
}
private fun getCardId(userWallet: UserWallet): String? {
return if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
}
private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List<Network>): List<Account> {
val walletManagers = networks.mapNotNull {
val blockchain = it.toBlockchain()
walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = it.derivationPath.value,
)
}
return walletManagers.flatMap {
val wallet = it.wallet
val chainIds = blockchainHelper.networkIdToChainIdOrNull(wallet.blockchain.toNetworkId())
blockchainHelper.chainIdsToAccounts(
walletAddress = wallet.address,
chainIds = chainIds,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
)
}
}
private suspend fun prepareRequestData(
sessionRequest: WalletConnectEvents.SessionRequest,
): Result<WcPreparedRequest> {
return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId)
}
private companion object {
const val WC_TOPIC_QUERY_NAME = "sessionTopic"
const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)"
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.walletconnect.model.legacy.Account
interface WcBlockchainHelper {
fun chainIdToNetworkIdOrNull(chainId: String): String?
fun chainIdToMissingNetworkNameOrNull(chainId: String): String?
fun networkIdToChainIdOrNull(networkId: String): List<String>
fun getNamespaceFromFullChainIdOrNull(chainId: String): String?
fun chainIdToFullNameOrNull(chainId: String): String?
fun chainIdsToAccounts(walletAddress: String, chainIds: List<String>, derivationPath: String?): List<Account>
fun chainIdsToBlockchains(chainIds: List<String>): List<Blockchain>
}

View file

@ -1,297 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.squareup.moshi.*
import com.tangem.datasource.di.SdkMoshi
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 com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
internal 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"),
WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
SOLANA_SIGN_TX("solana_signTransaction"),
SOLANA_SIGN_MESSAGE("solana_signMessage"),
SOLANA_SIGN_ALL_TX("solana_signAllTransactions"),
;
companion object {
fun fromCode(code: String): WcJrpcMethods? = values().firstOrNull { it.code == code }
}
}
@JsonClass(generateAdapter = true)
data class WcSignTransaction(
@Json(name = "network")
val network: Int,
@Json(name = "transaction")
val transaction: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcSignMessage(
@Json(name = "raw")
val raw: List<String>,
@Json(name = "type")
val type: WCSignType,
) : WcRequestData {
@JsonClass(generateAdapter = false)
enum class WCSignType {
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_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]`
* - SOLANA_MESSAGE: `[publicKey (address), message]`
*
* 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 WcSignMessageData(
@Json(name = "address")
val address: String,
@Json(name = "message")
val message: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumTransaction(
@Json(name = "from")
val from: String,
@Json(name = "to")
val to: String?,
@Json(name = "nonce")
val nonce: String?,
@Json(name = "gasPrice")
val gasPrice: String?,
@Json(name = "maxFeePerGas")
val maxFeePerGas: String?,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: String?,
@Json(name = "gas")
val gas: String?,
@Json(name = "gasLimit")
val gasLimit: String?,
@Json(name = "value")
val value: String?,
@Json(name = "data")
val data: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class SolanaTransactionsRequest(
@Json(name = "transactions")
val transactions: List<String>,
) : WcRequestData
interface WcRequestData
data class WcCustomRequestData(val data: String) : WcRequestData
sealed class WcRequest(open val data: WcRequestData) {
data class EthSign(override val data: WcSignMessage) : 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 AddChain(override val data: WcAddChain) : WcRequest(data)
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data)
data class SolanaSignTransactions(override val data: SolanaTransactionsRequest) : WcRequest(data)
}
@Singleton
internal 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 = WcSignMessage(
raw = deserializedParams,
type = WcSignMessage.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 = WcSignMessage(
raw = deserializedParams,
type = WcSignMessage.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 = WcSignMessage(
deserializedParams,
WcSignMessage.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)
}
WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> {
val deserializedParams: WcAddChain = moshi.adapter<List<WcAddChain>>(
Types.newParameterizedType(List::class.java, WcAddChain::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.AddChain(data = deserializedParams)
}
WcJrpcMethods.SOLANA_SIGN_TX -> {
val tx = moshi.adapter(SolanaTransactionRequest::class.java)
.fromJsonOrNull(params)
?: return customRequest
WcRequest.SolanaSignRequest(data = tx)
}
WcJrpcMethods.SOLANA_SIGN_MESSAGE -> {
val signMessage = moshi.adapter(SolanaSignMessage::class.java)
.fromJsonOrNull(params)
?: return customRequest
val data = WcSignMessage(
raw = listOf(signMessage.publicKey, signMessage.message),
type = WcSignMessage.WCSignType.SOLANA_MESSAGE,
)
WcRequest.EthSign(data = data)
}
WcJrpcMethods.SOLANA_SIGN_ALL_TX -> {
val transactionsToSign = moshi.adapter(SolanaTransactionsRequest::class.java)
.fromJsonOrNull(params) ?: return customRequest
WcRequest.SolanaSignTransactions(transactionsToSign)
}
}
}
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

@ -1,68 +0,0 @@
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)
class SolanaSignTransaction(
override val preparedRequestData: GenericTransactionData.SingleHash,
topic: String,
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
class SolanaSignMultipleTransactions(
override val preparedRequestData: GenericTransactionData.MultipleHashes,
topic: String,
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
}
sealed interface GenericTransactionData {
val dAppName: String
val type: TransactionType
data class SingleHash(
val hashToSign: ByteArray,
override val dAppName: String,
override val type: TransactionType,
) : GenericTransactionData
data class MultipleHashes(
val hashesToSign: List<ByteArray>,
override val dAppName: String,
override val type: TransactionType,
) : GenericTransactionData
}
enum class TransactionType {
SOLANA_TX,
}

View file

@ -1,184 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
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.WalletConnectError
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
import okio.ByteString.Companion.decodeBase64
internal class WcSessionRequestConverter(
private val blockchainHelper: WcBlockchainHelper,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
) {
@Suppress("LongMethod")
suspend fun prepareRequest(
sessionRequest: WalletConnectEvents.SessionRequest,
userWalletId: String,
): Result<WcPreparedRequest> = runCatching {
val networkId = requireNotNull(blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId.orEmpty())) {
"Failed to get network ID for chain ID: ${sessionRequest.chainId}"
}
val derivationPath = getDerivationPath(
sessionsRepository = sessionsRepository,
sessionRequest = sessionRequest,
userWalletId = userWalletId,
walletAddress = getWalletAddress(sessionRequest.request),
)
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,
),
)
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,
),
)
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,
)
}
is WcRequest.SolanaSignRequest -> {
val transaction = request.data.transaction
WcPreparedRequest.SolanaSignTransaction(
preparedRequestData = GenericTransactionData.SingleHash(
hashToSign = transaction.prepareSolanaTransaction(),
dAppName = sessionRequest.metaName,
type = TransactionType.SOLANA_TX,
),
topic = sessionRequest.topic,
requestId = sessionRequest.id,
derivationPath = derivationPath,
)
}
is WcRequest.SolanaSignTransactions -> {
val transactions = request.data.transactions
WcPreparedRequest.SolanaSignMultipleTransactions(
preparedRequestData = GenericTransactionData.MultipleHashes(
hashesToSign = transactions.map { it.prepareSolanaTransaction() }, //
dAppName = sessionRequest.metaName,
type = TransactionType.SOLANA_TX,
),
topic = sessionRequest.topic,
requestId = sessionRequest.id,
derivationPath = derivationPath,
)
}
else -> throw WalletConnectError.UnsupportedMethod
}
}
/**
* Input transaction in Base64 string
*/
private fun String.prepareSolanaTransaction(): ByteArray {
val transaction = this.decodeBase64()?.toByteArray() ?: ByteArray(0)
return SolanaTransactionHelper.removeSignaturesPlaceholders(transaction)
}
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
is WcRequest.SolanaSignRequest -> request.data.feePayer
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 {
// if walletAddress == null take first account
it.chainId == sessionRequest.chainId &&
(walletAddress == null || it.walletAddress.lowercase() == walletAddress.lowercase())
}?.derivationPath
}
}

View file

@ -1,10 +0,0 @@
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,
)

Some files were not shown because too many files have changed in this diff Show more