Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-14 11:48:39 +03:00
parent 6b770f5d00
commit f1a4be49b3
13 changed files with 337 additions and 103 deletions

View file

@ -34,6 +34,7 @@ object TestConstants {
const val TERRA_RECIPIENT_ADDRESS = "terra148dmp5ccazcwdmrcpvqz5rprnn886kemqen3tj"
const val POLYGON_RECIPIENT_ADDRESS = "0x742d35cc6634c0532925a3b844bc9e7595f2bd18"
const val WAIT_UNTIL_TIMEOUT_SHORT = 5_000L
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
const val WAIT_UNTIL_TIMEOUT_VERY_LONG = 60_000L

View file

@ -3,6 +3,9 @@ package com.tangem.common.extensions
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.components.buttons.actions.HasBadgeKey
import com.tangem.core.ui.components.buttons.actions.IsDimmedKey
@ -10,6 +13,15 @@ import io.github.kakaocup.compose.node.element.KNode
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
fun BaseTestCase.assertSnackbarWithText(text: String, timeoutMs: Long = WAIT_UNTIL_TIMEOUT) {
composeTestRule.waitUntil(timeoutMillis = timeoutMs) {
composeTestRule
.onAllNodesWithText(text, substring = true)
.fetchSemanticsNodes()
.isNotEmpty()
}
}
fun assertElementDoesNotExist(
elementProvider: () -> KNode,
elementDescription: String,

View file

@ -1,5 +1,6 @@
package com.tangem.common.utils
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
@ -21,9 +22,18 @@ private fun redactWcSecrets(text: String): String =
*/
fun getWcUri(
network: String = "ethereum",
dAppUrl: String? = null,
dAppName: String? = null,
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
val url = "$baseUrl/wc_uri?network=$network"
val url = "$baseUrl/wc_uri".toHttpUrl().newBuilder()
.addQueryParameter("network", network)
.apply {
if (dAppUrl != null) addQueryParameter("dappUrl", dAppUrl)
if (dAppName != null) addQueryParameter("dappName", dAppName)
}
.build()
.toString()
TangemLogger.i("getWcUri: requesting $url")
val client = OkHttpClient.Builder()

View file

@ -1,9 +1,19 @@
package com.tangem.scenarios
import android.content.Context
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_SHORT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.setClipboardText
import com.tangem.core.ui.R
import com.tangem.screens.onScanQrScreen
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
import com.tangem.screens.onWarningBottomSheet
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkWalletConnectBottomSheet() {
@ -17,9 +27,6 @@ fun BaseTestCase.checkWalletConnectBottomSheet() {
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() }
}
@ -82,9 +89,6 @@ fun BaseTestCase.checkWalletConnectScreen(withConnections: Boolean) {
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() }
}
@ -117,6 +121,73 @@ fun BaseTestCase.checkWalletConnectScreen(withConnections: Boolean) {
}
fun BaseTestCase.establishAndDisconnectWcSession(
context: Context,
deepLinkUri: String?,
dAppName: String,
) {
step("Set URI to clipboard") {
setClipboardText(context, deepLinkUri)
}
step("Create connection via 'Paste from clipboard' button") {
createConnectionViaPasteFromClipboardButton()
}
step("Check 'Wallet Connect' bottom sheet") {
composeTestRule.waitUntil(timeoutMillis = TestConstants.WAIT_UNTIL_TIMEOUT) {
runCatching { checkWalletConnectBottomSheet() }.isSuccess
}
}
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen(withConnections = true)
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
}
/**
* Clicks 'Connect' in the WalletConnect bottom sheet and dismisses the 'Unknown domain' security
* alert if it appears.
*
* qa-tools URIs are not registered with Reown Verify API, so Reown returns validation=UNKNOWN
* after the production change in DefaultWcPairUseCase that maps UNKNOWN to FAILED_TO_VERIFY, the
* app shows a Security Alert before establishing the session. Tests that drive qa-tools URIs go
* through this helper to consistently accept the warning.
*/
fun BaseTestCase.confirmWcConnection() {
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
waitForIdle()
val alertText = getResourceString(R.string.wc_alert_connect_anyway)
val alertAppeared = runCatching {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_SHORT) {
composeTestRule.onAllNodesWithText(alertText).fetchSemanticsNodes().isNotEmpty()
}
}.isSuccess
if (alertAppeared) {
step("Click on 'Connect anyway' button") {
onWarningBottomSheet { connectAnywayButton.clickWithAssertion() }
}
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
waitForIdle()
step("Assert connection details title is displayed") {
@ -134,9 +205,6 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
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() }
}
@ -158,4 +226,13 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
step("Assert 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.assertIsDisplayed() }
}
}
fun BaseTestCase.createConnectionViaPasteFromClipboardButton() {
step("Click on 'New connection' button") {
onWalletConnectScreen { newConnectionButton.performClick() }
}
step("Click on 'Paste from clipboard' button") {
onScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
}
}

View file

@ -30,11 +30,29 @@ class WarningBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsP
useUnmergedTree = true
}
val gotItButton: KNode = child {
val okGotItButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.warning_button_ok))
useUnmergedTree = true
}
val gotItButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_got_it))
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_cancel))
useUnmergedTree = true
}
val connectAnywayButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.wc_alert_connect_anyway))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWarningBottomSheet(function: WarningBottomSheetPageObject.() -> Unit) =

View file

@ -75,7 +75,7 @@ class SendViaSwapTest : BaseTestCase() {
onWarningBottomSheet { message(warningMessage).assertIsDisplayed() }
}
step("Click on 'Ok, Got it!' button") {
onWarningBottomSheet { gotItButton.performClick() }
onWarningBottomSheet { okGotItButton.performClick() }
}
}
}

View file

@ -3,17 +3,9 @@ package com.tangem.tests.walletConnect
import android.Manifest
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.getWcUri
import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.checkWalletConnectBottomSheet
import com.tangem.scenarios.checkWalletConnectDetailsBottomSheet
import com.tangem.scenarios.checkWalletConnectScreen
import com.tangem.scenarios.openAppByDeepLink
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openWalletConnectScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onScanQrScreen
import com.tangem.scenarios.*
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
@ -51,13 +43,8 @@ class EthereumWalletConnectTest : BaseTestCase() {
step("Assert 'Connect' button is enabled") {
onWalletConnectBottomSheet { connectButton.assertIsEnabled() }
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
@ -108,13 +95,8 @@ class EthereumWalletConnectTest : BaseTestCase() {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Check 'Wallet Connect' screen with connections") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
@ -164,11 +146,8 @@ class EthereumWalletConnectTest : BaseTestCase() {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
@ -218,11 +197,8 @@ class EthereumWalletConnectTest : BaseTestCase() {
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Click 'New connection' button") {
onWalletConnectScreen { newConnectionButton.performClick() }
}
step("CLick 'Paste from clipboard' button") {
onScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
step("Create connection via 'Paste from clipboard' button") {
createConnectionViaPasteFromClipboardButton()
}
step("Check 'Wallet Connect' bottom sheet") {
waitForIdle()
@ -230,13 +206,8 @@ class EthereumWalletConnectTest : BaseTestCase() {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen(withConnections = true)

View file

@ -4,19 +4,11 @@ import android.Manifest
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.getWcUri
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setClipboardText
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkWalletConnectBottomSheet
import com.tangem.scenarios.checkWalletConnectDetailsBottomSheet
import com.tangem.scenarios.checkWalletConnectScreen
import com.tangem.scenarios.openAppByDeepLink
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openWalletConnectScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onScanQrScreen
import com.tangem.scenarios.*
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
@ -64,13 +56,8 @@ class SolanaWalletConnectTest : BaseTestCase() {
step("Assert 'Connect' button is enabled") {
onWalletConnectBottomSheet { connectButton.assertIsEnabled() }
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
@ -131,13 +118,8 @@ class SolanaWalletConnectTest : BaseTestCase() {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Check 'Wallet Connect' screen with connections") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
@ -197,11 +179,8 @@ class SolanaWalletConnectTest : BaseTestCase() {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
@ -261,11 +240,8 @@ class SolanaWalletConnectTest : BaseTestCase() {
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Click 'New connection' button") {
onWalletConnectScreen { newConnectionButton.performClick() }
}
step("CLick 'Paste from clipboard' button") {
onScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
step("Create connection via 'Paste from clipboard' button") {
createConnectionViaPasteFromClipboardButton()
}
step("Check 'Wallet Connect' bottom sheet") {
waitForIdle()
@ -273,13 +249,8 @@ class SolanaWalletConnectTest : BaseTestCase() {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
step("Click on 'Connect' button and dismiss 'Unknown domain' alert if shown") {
confirmWcConnection()
}
step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen(withConnections = true)

View file

@ -0,0 +1,143 @@
package com.tangem.tests.walletConnect
import android.Manifest
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants
import com.tangem.common.extensions.assertSnackbarWithText
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.getWcUri
import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.*
import com.tangem.screens.onWarningBottomSheet
import com.tangem.wallet.BuildConfig
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 WalletConnectTest : BaseTestCase() {
@AllureId("9037")
@DisplayName("WC: invalid wallet connect link")
@Test
fun invalidWalletConnectLinkTest() {
val context = device.context
val deepLinkUri = "wc:384617d590a47f11c26311b5cf2418859682920aa0ad52"
val packageName = BuildConfig.APPLICATION_ID
val permissionName = Manifest.permission.CAMERA
setupHooks(
additionalBeforeSection = {
device.uiDevice.executeShellCommand("pm grant $packageName $permissionName")
},
).run {
step("Set URI to clipboard") {
setClipboardText(context, deepLinkUri)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Create connection via 'Paste from clipboard' button") {
createConnectionViaPasteFromClipboardButton()
}
step("Assert error snackbar about invalid WC URI is displayed") {
assertSnackbarWithText("getUserInfo")
}
}
}
@AllureId("9040")
@DisplayName("WC (React App): repeat open/close session")
@Test
fun repeatedConnectByWalletConnectDeeplinkScreenTest() {
val dAppName = "Tangem QA Tools"
val context = device.context
val packageName = BuildConfig.APPLICATION_ID
val permissionName = Manifest.permission.CAMERA
val sessionsCount = 3
setupHooks(
additionalBeforeSection = {
device.uiDevice.executeShellCommand("pm grant $packageName $permissionName")
},
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
repeat(sessionsCount) { iteration ->
step("Session #${iteration + 1}: connect and disconnect") {
establishAndDisconnectWcSession(
context = context,
deepLinkUri = getWcUri(),
dAppName = dAppName,
)
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
}
}
}
}
@AllureId("9066")
@DisplayName("WC: connect to unsupported dApp shows error")
@Test
fun connectToUnsupportedDAppShowsUnsupportedErrorTest() {
val unsupportedDAppUrl = "https://dydx.trade/test"
val dAppName = "dYdX"
val context = device.context
val packageName = BuildConfig.APPLICATION_ID
val permissionName = Manifest.permission.CAMERA
setupHooks(
additionalBeforeSection = {
device.uiDevice.executeShellCommand("pm grant $packageName $permissionName")
},
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Set unsupported dApp URI to clipboard") {
setClipboardText(
context = context,
text = getWcUri(dAppUrl = unsupportedDAppUrl, dAppName = dAppName),
)
}
step("Create connection via 'Paste from clipboard' button") {
createConnectionViaPasteFromClipboardButton()
}
step("Wait for unsupported dApp error bottom sheet") {
composeTestRule.waitUntil(timeoutMillis = TestConstants.WAIT_UNTIL_TIMEOUT) {
runCatching {
onWarningBottomSheet { gotItButton.assertIsDisplayed() }
}.isSuccess
}
}
step("Click on 'Got it' button") {
onWarningBottomSheet { gotItButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
}
}
}
}

View file

@ -200,20 +200,33 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
verifyContext: Wallet.Model.VerifyContext,
): Either<WcPairError, WcPairState.Proposal> = runCatching {
val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal)
// Display URL: shown to the user and logged to analytics. Reown's verified origin when
// present, otherwise its `verify.walletconnect.org` fallback. NOT trustworthy for
// security checks: when validation is INVALID, getDappOriginUrl returns the dApp-claimed
// origin (so the UI can show what was claimed), which a scam dApp can spoof.
val displayUrl = verifyContext.getDappOriginUrl()
val verificationInfo = when {
verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE
verifyContext.isScam == true -> CheckDAppResult.UNSAFE
else -> blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { error ->
TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error)
CheckDAppResult.FAILED_TO_VERIFY
// BlockAid is scanned only against the Reown-verified origin (validation == VALID
// guarantees Reown confirmed origin matches the dApp's registered domain).
// For UNKNOWN we have no trustworthy URL: passing a dApp-claimed URL would let an
// impersonator (e.g. a scam claiming metadata.url=dydx.trade) inherit its target's
// BlockAid verdict.
verifyContext.validation == Wallet.Model.Validation.VALID -> {
blockAidVerifier.verifyDApp(DAppData(verifyContext.origin)).getOrElse { error ->
TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error)
CheckDAppResult.FAILED_TO_VERIFY
}
}
else -> CheckDAppResult.FAILED_TO_VERIFY
}
val requestedNetworks = proposalAccountNetwork
.values.map { it.available.plus(it.required) }.flatten().toSet()
analytics.send(
WcAnalyticEvents.PairRequested(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
dAppUrl = displayUrl,
network = requestedNetworks,
domainVerification = verificationInfo,
),
@ -221,7 +234,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
val appMetaData = WcAppMetaData(
name = sessionProposal.name,
description = sessionProposal.description,
url = sessionProposal.url,
url = displayUrl,
icons = sessionProposal.icons.map { it.toString() },
redirect = sessionProposal.redirect,
)

View file

@ -7,7 +7,6 @@ import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.getDappOriginUrl
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed
@ -110,9 +109,10 @@ internal class WcPairSdkDelegate(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
val sessionProposalWithRealUrl = sessionProposal.copy(url = verifyContext.getDappOriginUrl())
// Triggered when wallet receives the session proposal sent by a Dapp
onSessionProposal.trySend(sessionProposalWithRealUrl to verifyContext)
// Triggered when wallet receives the session proposal sent by a Dapp.
// Pass the proposal through unchanged so consumers can decide between the dApp-claimed
// metadata url (sessionProposal.url) and the Verify-API origin (verifyContext.getDappOriginUrl()).
onSessionProposal.trySend(sessionProposal to verifyContext)
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {

View file

@ -147,6 +147,25 @@ internal class DefaultWcPairUseCaseTest {
}
}
@Test
fun `verifyDApp uses verifyContext origin when sessionProposal url is spoofed`() = runTest {
val spoofedProposal = sdkProposal.copy(url = "https://evil-spoofed.example/")
coEvery { sdkDelegate.pair(url) } returns (spoofedProposal to sdkVerifyContext).right()
coEvery { associateNetworksDelegate.associateAccounts(spoofedProposal) } returns mapOf()
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
val useCase = useCaseFactory()
useCase.invoke().test {
assertEquals(loading, awaitItem())
coVerifyOrder {
sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
}
assert(awaitItem() is WcPairState.Proposal)
expectNoEvents()
}
}
@Test
fun `success pair and approve flow`() = runTest {
val approveLoading = WcPairState.Approving.Loading(sessionForApprove)

View file

@ -20,7 +20,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
@ -338,7 +337,7 @@ internal class WcPairModel @Inject constructor(
is WcPairError.UriAlreadyUsed -> WcAppInfoRoutes.Alert.UriAlreadyUsed
is WcPairError.TimeoutException -> WcAppInfoRoutes.Alert.TimeoutException
else -> {
messageSender.send(ToastMessage(message = stringReference(error.message)))
messageSender.send(SnackbarMessage(message = stringReference(error.message)))
router.pop()
null
}