Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-21 14:32:52 +04:00
parent b12b46479c
commit 17008240b9
16 changed files with 123 additions and 76 deletions

View file

@ -38,4 +38,5 @@ dependencies {
testImplementation(deps.test.junit) testImplementation(deps.test.junit)
testImplementation(deps.test.mockk) testImplementation(deps.test.mockk)
testImplementation(deps.test.turbine) testImplementation(deps.test.turbine)
testImplementation(deps.test.truth)
} }

View file

@ -10,13 +10,12 @@ import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionSc
import com.tangem.datasource.api.common.blockaid.models.request.RpcData import com.tangem.datasource.api.common.blockaid.models.request.RpcData
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.* import com.tangem.datasource.api.common.blockaid.models.response.*
import javax.inject.Inject
private const val SUCCESS_STATUS = "Success" private const val SUCCESS_STATUS = "Success"
private const val DOMAIN_CHECKED_STATUS = "hit" private const val DOMAIN_CHECKED_STATUS = "hit"
private const val VALIDATION_SAFE_STATUS = "Benign" private const val VALIDATION_SAFE_STATUS = "Benign"
internal class BlockAidMapper @Inject constructor() { internal class BlockAidMapper {
fun mapToDomain(from: DomainScanResponse): CheckDAppResult { fun mapToDomain(from: DomainScanResponse): CheckDAppResult {
return when { return when {

View file

@ -9,9 +9,8 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject
internal class DefaultBlockAidRepository @Inject constructor( internal class DefaultBlockAidRepository(
private val api: BlockAidApi, private val api: BlockAidApi,
private val dispatcherProvider: CoroutineDispatcherProvider, private val dispatcherProvider: CoroutineDispatcherProvider,
private val mapper: BlockAidMapper, private val mapper: BlockAidMapper,

View file

@ -1,18 +1,27 @@
package com.tangem.data.blockaid.di package com.tangem.data.blockaid.di
import com.tangem.data.blockaid.BlockAidMapper
import com.tangem.data.blockaid.BlockAidRepository import com.tangem.data.blockaid.BlockAidRepository
import com.tangem.data.blockaid.DefaultBlockAidRepository import com.tangem.data.blockaid.DefaultBlockAidRepository
import dagger.Binds import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal interface BlockAidDataInternalModule { internal object BlockAidDataInternalModule {
@Binds @Provides
@Singleton @Singleton
fun bindRepository(repository: DefaultBlockAidRepository): BlockAidRepository fun provideRepository(api: BlockAidApi, dispatcherProvider: CoroutineDispatcherProvider): BlockAidRepository {
return DefaultBlockAidRepository(
api = api,
dispatcherProvider = dispatcherProvider,
mapper = BlockAidMapper(),
)
}
} }

View file

@ -4,8 +4,8 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.ValidationResult
import com.domain.blockaid.models.transaction.simultation.SimulationData import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.google.common.truth.Truth
import com.tangem.datasource.api.common.blockaid.models.response.* import com.tangem.datasource.api.common.blockaid.models.response.*
import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import java.math.BigDecimal import java.math.BigDecimal
@ -14,28 +14,28 @@ class BlockAidMapperTest {
private val mapper = BlockAidMapper() private val mapper = BlockAidMapper()
@Test @Test
fun whenStatusHitAndIsMaliciousFalseThenMapToDomainReturnsSafe() { fun `when status hit and is malicious false then map to domain returns safe`() {
val response = DomainScanResponse(status = "hit", isMalicious = false) val response = DomainScanResponse(status = "hit", isMalicious = false)
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(CheckDAppResult.SAFE, result) Truth.assertThat(result).isEqualTo(CheckDAppResult.SAFE)
} }
@Test @Test
fun whenStatusHitAndIsMaliciousTrueThenMapToDomainReturnsUnsafe() { fun `when status hit and is malicious true then map to domain returns unsafe`() {
val response = DomainScanResponse(status = "hit", isMalicious = true) val response = DomainScanResponse(status = "hit", isMalicious = true)
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(CheckDAppResult.UNSAFE, result) Truth.assertThat(result).isEqualTo(CheckDAppResult.UNSAFE)
} }
@Test @Test
fun whenStatusNotHitThenMapToDomainReturnsFailedToVerify() { fun `when status not hit then map to domain returns failed to verify`() {
val response = DomainScanResponse(status = "miss", isMalicious = false) val response = DomainScanResponse(status = "miss", isMalicious = false)
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(CheckDAppResult.FAILED_TO_VERIFY, result) Truth.assertThat(result).isEqualTo(CheckDAppResult.FAILED_TO_VERIFY)
} }
@Test @Test
fun whenResponseBenignValidationThenReturnsSafeValidation() { fun `when response benign validation then returns safe validation`() {
val spenderDetails = SpenderDetails( val spenderDetails = SpenderDetails(
isApprovedForAll = true, isApprovedForAll = true,
exposure = listOf(ExposureDetail(value = "1000.0", rawValue = "0x123")), exposure = listOf(ExposureDetail(value = "1000.0", rawValue = "0x123")),
@ -53,20 +53,20 @@ class BlockAidMapperTest {
) )
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(ValidationResult.SAFE, result.validation) Truth.assertThat(result.validation).isEqualTo(ValidationResult.SAFE)
val simulation = result.simulation as? SimulationResult.Success val simulation = result.simulation as? SimulationResult.Success
assertNotNull(simulation) Truth.assertThat(simulation).isNotNull()
val approve = simulation?.data as? SimulationData.Approve val approve = simulation?.data as? SimulationData.Approve
assertNotNull(approve) Truth.assertThat(approve).isNotNull()
assertEquals(1, approve?.approvedAmounts?.size) Truth.assertThat(approve?.approvedAmounts?.size).isEqualTo(1)
assertEquals(BigDecimal("1000.0"), approve?.approvedAmounts?.first()?.approvedAmount) Truth.assertThat(approve?.approvedAmounts?.first()?.approvedAmount).isEqualTo(BigDecimal("1000.0"))
assertTrue(approve?.approvedAmounts?.first()?.isUnlimited == true) Truth.assertThat(approve?.approvedAmounts?.first()?.isUnlimited).isTrue()
} }
@Test @Test
fun whenResponseBenignValidationAndSuccessSimulationThenReturnsSendReceiveResult() { fun `when response benign validation and success simulation then returns send receive result`() {
val assetDiff = AssetDiff( val assetDiff = AssetDiff(
assetType = "ERC20", assetType = "ERC20",
asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH"), asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH"),
@ -82,19 +82,19 @@ class BlockAidMapperTest {
) )
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(ValidationResult.SAFE, result.validation) Truth.assertThat(result.validation).isEqualTo(ValidationResult.SAFE)
val simulation = result.simulation as? SimulationResult.Success val simulation = result.simulation as? SimulationResult.Success
assertNotNull(simulation) Truth.assertThat(simulation).isNotNull()
val data = simulation?.data as? SimulationData.SendAndReceive val data = simulation?.data as? SimulationData.SendAndReceive
assertNotNull(data) Truth.assertThat(data).isNotNull()
assertEquals(BigDecimal("1.5"), data?.send?.first()?.amount) Truth.assertThat(data?.send?.first()?.amount).isEqualTo(BigDecimal("1.5"))
assertEquals(BigDecimal("2.0"), data?.receive?.first()?.amount) Truth.assertThat(data?.receive?.first()?.amount).isEqualTo(BigDecimal("2.0"))
} }
@Test @Test
fun whenResponseErrorValidationThenReturnsFailedToValidate() { fun `when response error validation rhen returns failed to validate`() {
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Error", resultType = "Benign"), validation = ValidationResponse(status = "Error", resultType = "Benign"),
simulation = SimulationResponse( simulation = SimulationResponse(
@ -104,12 +104,12 @@ class BlockAidMapperTest {
) )
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(ValidationResult.FAILED_TO_VALIDATE, result.validation) Truth.assertThat(result.validation).isEqualTo(ValidationResult.FAILED_TO_VALIDATE)
assertTrue(result.simulation is SimulationResult.FailedToSimulate) Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue()
} }
@Test @Test
fun whenResponseNotBenignThenReturnsValidationUnsafe() { fun `when response not benign then returns validation unsafe`() {
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Phishing"), validation = ValidationResponse(status = "Success", resultType = "Phishing"),
simulation = SimulationResponse( simulation = SimulationResponse(
@ -119,11 +119,11 @@ class BlockAidMapperTest {
) )
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertEquals(ValidationResult.UNSAFE, result.validation) Truth.assertThat(result.validation).isEqualTo(ValidationResult.UNSAFE)
} }
@Test @Test
fun whenResponseSimulationNotSuccessThenReturnsSimulationFailedToSimulate() { fun `when response simulation not success then returns simulation failed ro simulate`() {
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign"), validation = ValidationResponse(status = "Success", resultType = "Benign"),
simulation = SimulationResponse( simulation = SimulationResponse(
@ -133,11 +133,11 @@ class BlockAidMapperTest {
) )
val result = mapper.mapToDomain(response) val result = mapper.mapToDomain(response)
assertTrue(result.simulation is SimulationResult.FailedToSimulate) Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue()
} }
@Test @Test
fun whenResponseSimulationIsEmptyThenReturnsFailedToSimulate() { fun `when response simulation is empty then returns failed to simulate`() {
val txResponse = TransactionScanResponse( val txResponse = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign"), validation = ValidationResponse(status = "Success", resultType = "Benign"),
simulation = SimulationResponse( simulation = SimulationResponse(
@ -150,6 +150,6 @@ class BlockAidMapperTest {
) )
val result = mapper.mapToDomain(txResponse) val result = mapper.mapToDomain(txResponse)
assertTrue(result.simulation is SimulationResult.FailedToSimulate) Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue()
} }
} }

View file

@ -5,24 +5,20 @@ import com.domain.blockaid.models.dapp.DAppData
import com.domain.blockaid.models.transaction.CheckTransactionResult import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.domain.blockaid.models.transaction.TransactionData import com.domain.blockaid.models.transaction.TransactionData
import com.domain.blockaid.models.transaction.TransactionParams import com.domain.blockaid.models.transaction.TransactionParams
import com.google.common.truth.Truth
import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.* import io.mockk.*
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultBlockAidRepositoryTest { class DefaultBlockAidRepositoryTest {
@MockK @MockK
@ -31,28 +27,18 @@ class DefaultBlockAidRepositoryTest {
@MockK @MockK
private lateinit var mapper: BlockAidMapper private lateinit var mapper: BlockAidMapper
@MockK
private lateinit var dispatcherProvider: CoroutineDispatcherProvider
private lateinit var repository: DefaultBlockAidRepository private lateinit var repository: DefaultBlockAidRepository
private val testDispatcher = StandardTestDispatcher() private val testDispatcherProvider = TestingCoroutineDispatcherProvider()
@Before @Before
fun setup() { fun setup() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
Dispatchers.setMain(testDispatcher) repository = DefaultBlockAidRepository(api, testDispatcherProvider, mapper)
every { dispatcherProvider.io } returns testDispatcher
repository = DefaultBlockAidRepository(api, dispatcherProvider, mapper)
}
@After
fun tearDown() {
Dispatchers.resetMain()
} }
@Test @Test
fun whenVerifyAppDomainThenCallsApiAndMapsResult() = runTest { fun `when verify app domain then calls api and maps result`() = runTest {
val url = "https://example.com" val url = "https://example.com"
val domainData = DAppData(url) val domainData = DAppData(url)
val domainResponse = DomainScanResponse(status = "hit", isMalicious = false) val domainResponse = DomainScanResponse(status = "hit", isMalicious = false)
@ -63,13 +49,13 @@ class DefaultBlockAidRepositoryTest {
val result = repository.verifyDAppDomain(domainData) val result = repository.verifyDAppDomain(domainData)
assertEquals(expectedResult, result) Truth.assertThat(result).isEqualTo(expectedResult)
coVerify { api.scanDomain(DomainScanRequest(url)) } coVerify { api.scanDomain(DomainScanRequest(url)) }
verify { mapper.mapToDomain(domainResponse) } verify { mapper.mapToDomain(domainResponse) }
} }
@Test @Test
fun whenVerifyEvmTransactionThenCallsScanJsonRpcAndMaps() = runTest { fun `when verify evm transaction then calls scan json rpc and maps result`() = runTest {
val data = TransactionData( val data = TransactionData(
chain = "ethereum", chain = "ethereum",
accountAddress = "0xabc", accountAddress = "0xabc",
@ -88,14 +74,14 @@ class DefaultBlockAidRepositoryTest {
val result = repository.verifyTransaction(data) val result = repository.verifyTransaction(data)
assertEquals(expectedResult, result) Truth.assertThat(result).isEqualTo(expectedResult)
coVerify { api.scanJsonRpc(request) } coVerify { api.scanJsonRpc(request) }
verify { mapper.mapToEvmRequest(data) } verify { mapper.mapToEvmRequest(data) }
verify { mapper.mapToDomain(response) } verify { mapper.mapToDomain(response) }
} }
@Test @Test
fun whenVerifySolanaTransactionThenCallsScanSolanaMessageAndMapsResult() = runTest { fun `when verify solana transaction then calls scan solana message and maps result`() = runTest {
val data = TransactionData( val data = TransactionData(
chain = "mainnet", chain = "mainnet",
accountAddress = "/Rd2TLl...", accountAddress = "/Rd2TLl...",
@ -114,7 +100,7 @@ class DefaultBlockAidRepositoryTest {
val result = repository.verifyTransaction(data) val result = repository.verifyTransaction(data)
assertEquals(expectedResult, result) Truth.assertThat(result).isEqualTo(expectedResult)
coVerify { api.scanSolanaMessage(request) } coVerify { api.scanSolanaMessage(request) }
verify { mapper.mapToSolanaRequest(data) } verify { mapper.mapToSolanaRequest(data) }
verify { mapper.mapToDomain(response) } verify { mapper.mapToDomain(response) }

View file

@ -50,6 +50,8 @@ dependencies {
/* Other */ /* Other */
implementation(deps.kotlin.coroutines) implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core) implementation(deps.arrow.core)
implementation(projects.domain.blockaid)
implementation(projects.domain.blockaid.models)
/* Tests */ /* Tests */
testImplementation(projects.common.test) testImplementation(projects.common.test)

View file

@ -20,6 +20,7 @@ import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository
@ -71,11 +72,13 @@ internal object WalletConnectDataModule {
associateNetworksDelegate: AssociateNetworksDelegate, associateNetworksDelegate: AssociateNetworksDelegate,
caipNamespaceDelegate: CaipNamespaceDelegate, caipNamespaceDelegate: CaipNamespaceDelegate,
sdkDelegate: WcPairSdkDelegate, sdkDelegate: WcPairSdkDelegate,
blockAidVerifier: BlockAidVerifier,
): DefaultWcPairUseCase = DefaultWcPairUseCase( ): DefaultWcPairUseCase = DefaultWcPairUseCase(
sessionsManager = sessionsManager, sessionsManager = sessionsManager,
associateNetworksDelegate = associateNetworksDelegate, associateNetworksDelegate = associateNetworksDelegate,
caipNamespaceDelegate = caipNamespaceDelegate, caipNamespaceDelegate = caipNamespaceDelegate,
sdkDelegate = sdkDelegate, sdkDelegate = sdkDelegate,
blockAidVerifier = blockAidVerifier,
) )
@Provides @Provides

View file

@ -1,10 +1,14 @@
package com.tangem.data.walletconnect.pair package com.tangem.data.walletconnect.pair
import arrow.core.Either import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionApprove
@ -28,6 +32,7 @@ internal class DefaultWcPairUseCase(
private val associateNetworksDelegate: AssociateNetworksDelegate, private val associateNetworksDelegate: AssociateNetworksDelegate,
private val caipNamespaceDelegate: CaipNamespaceDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate,
private val sdkDelegate: WcPairSdkDelegate, private val sdkDelegate: WcPairSdkDelegate,
private val blockAidVerifier: BlockAidVerifier,
) : WcPairUseCase { ) : WcPairUseCase {
private val onCallTerminalAction = Channel<TerminalAction>() private val onCallTerminalAction = Channel<TerminalAction>()
@ -71,7 +76,10 @@ internal class DefaultWcPairUseCase(
sessionForApprove = sessionForApprove, sessionForApprove = sessionForApprove,
sdkSessionProposal = sdkSessionProposal, sdkSessionProposal = sdkSessionProposal,
).map { settledSession -> ).map { settledSession ->
val newSession = settledSession.session.toDomain(sessionForApprove.wallet) val newSession = settledSession.session.toDomain(
wallet = sessionForApprove.wallet,
securityStatus = proposalState.dAppSession.securityStatus,
)
sessionsManager.saveSession(newSession) sessionsManager.saveSession(newSession)
newSession newSession
} }
@ -107,6 +115,10 @@ internal class DefaultWcPairUseCase(
sessionProposal: Wallet.Model.SessionProposal, sessionProposal: Wallet.Model.SessionProposal,
): Either<WcPairError, WcPairState.Proposal> = runCatching { ): Either<WcPairError, WcPairState.Proposal> = runCatching {
val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) val proposalNetwork = associateNetworksDelegate.associate(sessionProposal)
val verificationInfo = blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse {
Timber.e("Failed to verify DApp: ${it.localizedMessage}")
CheckDAppResult.FAILED_TO_VERIFY
}
val appMetaData = WcAppMetaData( val appMetaData = WcAppMetaData(
name = sessionProposal.name, name = sessionProposal.name,
description = sessionProposal.description, description = sessionProposal.description,
@ -117,7 +129,7 @@ internal class DefaultWcPairUseCase(
val dAppSession = WcSessionProposal( val dAppSession = WcSessionProposal(
dAppMetaData = appMetaData, dAppMetaData = appMetaData,
proposalNetwork = proposalNetwork, proposalNetwork = proposalNetwork,
securityStatus = Any(), securityStatus = verificationInfo,
) )
WcPairState.Proposal(dAppSession) WcPairState.Proposal(dAppSession)
}.fold(onSuccess = { it.right() }, onFailure = { }.fold(onSuccess = { it.right() }, onFailure = {
@ -127,10 +139,13 @@ internal class DefaultWcPairUseCase(
} }
},) },)
private fun Wallet.Model.Session.toDomain(wallet: UserWallet): WcSession = WcSession( private fun Wallet.Model.Session.toDomain(wallet: UserWallet, securityStatus: CheckDAppResult): WcSession {
wallet = wallet, return WcSession(
sdkModel = WcSdkSessionConverter.convert(this), wallet = wallet,
) sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = securityStatus,
)
}
private sealed interface TerminalAction { private sealed interface TerminalAction {
data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction

View file

@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.sessions
import arrow.core.Either import arrow.core.Either
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkObserver
@ -25,7 +26,7 @@ import timber.log.Timber
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
internal class DefaultWcSessionsManager constructor( internal class DefaultWcSessionsManager(
private val store: WalletConnectStore, private val store: WalletConnectStore,
private val legacyStore: WalletConnectSessionsRepository, private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase, private val getWallets: GetWalletsUseCase,
@ -60,7 +61,7 @@ internal class DefaultWcSessionsManager constructor(
} }
override suspend fun saveSession(session: WcSession) { override suspend fun saveSession(session: WcSession) {
store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId)) store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId, session.securityStatus))
} }
override suspend fun removeSession(session: WcSession): Either<Throwable, Unit> { override suspend fun removeSession(session: WcSession): Either<Throwable, Unit> {
@ -82,13 +83,17 @@ internal class DefaultWcSessionsManager constructor(
} }
override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) {
val storedSessions = sessions.firstOrNull() val storedSession = sessions.firstOrNull()
?.values?.flatten() ?.values?.flatten()
?.firstOrNull { it.sdkModel.topic == topic } ?.firstOrNull { it.sdkModel.topic == topic }
?: return@withContext null ?: return@withContext null
val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null
val wallet = storedSessions.wallet val wallet = storedSession.wallet
WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession)) WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = storedSession.securityStatus,
)
} }
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
@ -104,7 +109,13 @@ internal class DefaultWcSessionsManager constructor(
val walletIds = wallets.map { wallet -> wallet.walletId } val walletIds = wallets.map { wallet -> wallet.walletId }
val inLegacyStore = walletIds val inLegacyStore = walletIds
.map { walletId -> .map { walletId ->
flow { emit(legacyStore.loadSessions(walletId.stringValue).map { WcSessionDTO(it.topic, walletId) }) } flow {
emit(
legacyStore.loadSessions(walletId.stringValue).map {
WcSessionDTO(it.topic, walletId, CheckDAppResult.FAILED_TO_VERIFY)
},
)
}
} }
.merge() .merge()
.reduce { accumulator, value -> accumulator.plus(value) } .reduce { accumulator, value -> accumulator.plus(value) }
@ -124,7 +135,7 @@ internal class DefaultWcSessionsManager constructor(
val wcSessions = inStore.mapNotNull { session -> val wcSessions = inStore.mapNotNull { session ->
val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null
WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession)) WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), session.securityStatus)
} }
return wcSessions return wcSessions
} }

View file

@ -1,8 +1,11 @@
package com.tangem.domain.walletconnect package com.tangem.domain.walletconnect
import app.cash.turbine.test import app.cash.turbine.test
import arrow.core.Either
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet
import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate
@ -10,6 +13,7 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionApprove
@ -30,6 +34,7 @@ internal class DefaultWcPairUseCaseTest {
private val associateNetworksDelegate: AssociateNetworksDelegate = mockk<AssociateNetworksDelegate>() private val associateNetworksDelegate: AssociateNetworksDelegate = mockk<AssociateNetworksDelegate>()
private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk<CaipNamespaceDelegate>() private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk<CaipNamespaceDelegate>()
private val sdkDelegate: WcPairSdkDelegate = mockk<WcPairSdkDelegate>() private val sdkDelegate: WcPairSdkDelegate = mockk<WcPairSdkDelegate>()
private val blockAidVerifier: BlockAidVerifier = mockk<BlockAidVerifier>()
private val url = "testUrl" private val url = "testUrl"
private val source = WcPairUseCase.Source.QR private val source = WcPairUseCase.Source.QR
@ -85,6 +90,7 @@ internal class DefaultWcPairUseCaseTest {
get() = WcSession( get() = WcSession(
wallet = sessionForApprove.wallet, wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(this), sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = CheckDAppResult.SAFE,
) )
private val useCase = DefaultWcPairUseCase( private val useCase = DefaultWcPairUseCase(
@ -92,6 +98,7 @@ internal class DefaultWcPairUseCaseTest {
associateNetworksDelegate = associateNetworksDelegate, associateNetworksDelegate = associateNetworksDelegate,
caipNamespaceDelegate = caipNamespaceDelegate, caipNamespaceDelegate = caipNamespaceDelegate,
sdkDelegate = sdkDelegate, sdkDelegate = sdkDelegate,
blockAidVerifier = blockAidVerifier,
) )
@Before @Before
@ -109,11 +116,13 @@ internal class DefaultWcPairUseCaseTest {
@Test @Test
fun `pair, emmit proposal state and wait actions`() = runTest { fun `pair, emmit proposal state and wait actions`() = runTest {
coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right()
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
useCase.pairFlow(url, source).test { useCase.pairFlow(url, source).test {
assertEquals(loading, awaitItem()) assertEquals(loading, awaitItem())
coVerifyOrder { coVerifyOrder {
sdkDelegate.pair(url) sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
} }
assert(awaitItem() is WcPairState.Proposal) assert(awaitItem() is WcPairState.Proposal)
expectNoEvents() expectNoEvents()
@ -129,11 +138,13 @@ internal class DefaultWcPairUseCaseTest {
coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right()
coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right() coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right()
coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
useCase.pairFlow(url, source).test { useCase.pairFlow(url, source).test {
assertEquals(loading, awaitItem()) assertEquals(loading, awaitItem())
coVerifyOrder { coVerifyOrder {
sdkDelegate.pair(url) sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
} }
assert(awaitItem() is WcPairState.Proposal) assert(awaitItem() is WcPairState.Proposal)
useCase.approve(sessionForApprove) useCase.approve(sessionForApprove)
@ -156,11 +167,13 @@ internal class DefaultWcPairUseCaseTest {
coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right()
coEvery { sdkDelegate.rejectSession(proposerPublicKey) } returns Unit coEvery { sdkDelegate.rejectSession(proposerPublicKey) } returns Unit
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
useCase.pairFlow(url, source).test { useCase.pairFlow(url, source).test {
assertEquals(loading, awaitItem()) assertEquals(loading, awaitItem())
coVerifyOrder { coVerifyOrder {
sdkDelegate.pair(url) sdkDelegate.pair(url)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
} }
assert(awaitItem() is WcPairState.Proposal) assert(awaitItem() is WcPairState.Proposal)
useCase.reject() useCase.reject()
@ -208,6 +221,7 @@ internal class DefaultWcPairUseCaseTest {
val error = WcPairError.ExternalApprovalError("error").left() val error = WcPairError.ExternalApprovalError("error").left()
coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right()
coEvery { sdkDelegate.approve(sdkApprove) } returns error coEvery { sdkDelegate.approve(sdkApprove) } returns error
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
val errorResult = WcPairState.Approving.Result(sessionForApprove, error) val errorResult = WcPairState.Approving.Result(sessionForApprove, error)
@ -216,6 +230,7 @@ internal class DefaultWcPairUseCaseTest {
coVerifyOrder { coVerifyOrder {
sdkDelegate.pair(url) sdkDelegate.pair(url)
associateNetworksDelegate.associate(sdkProposal) associateNetworksDelegate.associate(sdkProposal)
blockAidVerifier.verifyDApp(DAppData(sdkProposal.url))
} }
assert(awaitItem() is WcPairState.Proposal) assert(awaitItem() is WcPairState.Proposal)
useCase.approve(sessionForApprove) useCase.approve(sessionForApprove)

View file

@ -14,6 +14,7 @@ dependencies {
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.walletConnect.models) implementation(projects.domain.walletConnect.models)
implementation(projects.domain.blockaid.models)
/* Other */ /* Other */
implementation(deps.moshi.adapters) implementation(deps.moshi.adapters)

View file

@ -8,6 +8,7 @@ dependencies {
/* Domain */ /* Domain */
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)
implementation(projects.domain.blockaid.models)
/* Other */ /* Other */
implementation(deps.moshi) implementation(deps.moshi)

View file

@ -1,9 +1,11 @@
package com.tangem.domain.walletconnect.model package com.tangem.domain.walletconnect.model
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
data class WcSession( data class WcSession(
val wallet: UserWallet, val wallet: UserWallet,
val sdkModel: WcSdkSession, val sdkModel: WcSdkSession,
val securityStatus: CheckDAppResult,
) )

View file

@ -1,5 +1,6 @@
package com.tangem.domain.walletconnect.model package com.tangem.domain.walletconnect.model
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
@ -7,4 +8,5 @@ import com.tangem.domain.wallets.models.UserWalletId
data class WcSessionDTO( data class WcSessionDTO(
val topic: String, val topic: String,
val walletId: UserWalletId, val walletId: UserWalletId,
val securityStatus: CheckDAppResult,
) )

View file

@ -1,5 +1,6 @@
package com.tangem.domain.walletconnect.model package com.tangem.domain.walletconnect.model
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
@ -7,7 +8,7 @@ import com.tangem.domain.wallets.models.UserWallet
data class WcSessionProposal( data class WcSessionProposal(
val dAppMetaData: WcAppMetaData, val dAppMetaData: WcAppMetaData,
val proposalNetwork: Map<UserWallet, ProposalNetwork>, val proposalNetwork: Map<UserWallet, ProposalNetwork>,
val securityStatus: Any, val securityStatus: CheckDAppResult,
) { ) {
data class ProposalNetwork( data class ProposalNetwork(