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

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