From 2af08f578c803f663221de9a5d553c9dae00ce57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 12:47:25 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../api/common/blockaid/BlockAidApi.kt | 21 +++ .../models/request/DomainScanRequest.kt | 9 + .../request/EvmTransactionScanRequest.kt | 22 +++ .../request/SolanaTransactionScanRequest.kt | 16 ++ .../models/response/AccountSummaryResponse.kt | 10 ++ .../blockaid/models/response/AssetDiff.kt | 25 +++ .../models/response/DomainScanResponse.kt | 10 ++ .../blockaid/models/response/Exposure.kt | 22 +++ .../models/response/SimulationResponse.kt | 10 ++ .../models/response/TransactionMetadata.kt | 9 + .../response/TransactionScanResponse.kt | 10 ++ .../models/response/ValidationResponse.kt | 10 ++ .../datasource/api/common/config/ApiConfig.kt | 2 + .../datasource/api/common/config/BlockAid.kt | 27 +++ .../tangem/datasource/di/ApiConfigsModule.kt | 6 + .../com/tangem/datasource/di/NetworkModule.kt | 24 +++ .../config/environment/EnvironmentConfig.kt | 1 + .../converter/EnvironmentConfigConverter.kt | 1 + .../models/EnvironmentConfigModel.kt | 1 + .../managers/ProdApiConfigsManagerTest.kt | 11 ++ data/blockaid/.gitignore | 1 + data/blockaid/build.gradle.kts | 41 +++++ .../tangem/data/blockaid/BlockAidMapper.kt | 128 +++++++++++++++ .../data/blockaid/BlockAidRepository.kt | 13 ++ .../blockaid/DefaultBlockAidRepository.kt | 40 +++++ .../data/blockaid/DefaultBlockAidVerifier.kt | 31 ++++ .../blockaid/di/BlockAidDataInternalModule.kt | 18 ++ .../data/blockaid/di/BlockAidDataModule.kt | 18 ++ .../data/blockaid/BlockAidMapperTest.kt | 155 ++++++++++++++++++ .../blockaid/DefaultBlockAidRepositoryTest.kt | 122 ++++++++++++++ domain/blockaid/.gitignore | 1 + domain/blockaid/build.gradle.kts | 13 ++ domain/blockaid/models/.gitignore | 1 + domain/blockaid/models/build.gradle.kts | 10 ++ .../blockaid/models/dapp/CheckDAppResult.kt | 22 +++ .../domain/blockaid/models/dapp/DAppData.kt | 11 ++ .../transaction/CheckTransactionResult.kt | 12 ++ .../models/transaction/SimulationResult.kt | 21 +++ .../models/transaction/TransactionData.kt | 36 ++++ .../models/transaction/ValidationResult.kt | 22 +++ .../transaction/simultation/AmountInfo.kt | 8 + .../transaction/simultation/ApprovedAmount.kt | 9 + .../transaction/simultation/SimulationData.kt | 22 +++ .../transaction/simultation/TokenInfo.kt | 7 + .../domain/blockaid/BlockAidVerifier.kt | 23 +++ settings.gradle.kts | 3 + 46 files changed, 1035 insertions(+) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt create mode 100644 data/blockaid/.gitignore create mode 100644 data/blockaid/build.gradle.kts create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt create mode 100644 data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt create mode 100644 data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt create mode 100644 domain/blockaid/.gitignore create mode 100644 domain/blockaid/build.gradle.kts create mode 100644 domain/blockaid/models/.gitignore create mode 100644 domain/blockaid/models/build.gradle.kts create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt create mode 100644 domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt new file mode 100644 index 0000000000..605bc8a391 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.api.common.blockaid + +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.SolanaTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface BlockAidApi { + + @POST("site/scan") + suspend fun scanDomain(@Body request: DomainScanRequest): DomainScanResponse + + @POST("evm/json-rpc/scan") + suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse + + @POST("solana/message/scan") + suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt new file mode 100644 index 0000000000..e88eca28ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DomainScanRequest( + @Json(name = "url") val url: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt new file mode 100644 index 0000000000..fd5aeecfaf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata + +@JsonClass(generateAdapter = true) +data class EvmTransactionScanRequest( + @Json(name = "chain") val chain: String, + @Json(name = "account_address") val accountAddress: String, + @Json(name = "method") val method: String, + @Json(name = "data") val data: RpcData, + @Json(name = "options") val options: List = listOf("simulation", "validation"), + @Json(name = "metadata") val metadata: TransactionMetadata, +) + +@JsonClass(generateAdapter = true) +data class RpcData( + @Json(name = "jsonrpc") val jsonrpc: String = "2.0", + @Json(name = "method") val method: String, + @Json(name = "params") val params: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt new file mode 100644 index 0000000000..72ec506f01 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata + +@JsonClass(generateAdapter = true) +data class SolanaTransactionScanRequest( + @Json(name = "encoding") val encoding: String = "base64", + @Json(name = "chain") val chain: String, + @Json(name = "method") val method: String, + @Json(name = "options") val options: List = listOf("simulation, validation"), + @Json(name = "metadata") val metadata: TransactionMetadata, + @Json(name = "account_address") val accountAddress: String, + @Json(name = "transactions") val transactions: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt new file mode 100644 index 0000000000..e0188fa7dc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AccountSummaryResponse( + @Json(name = "assets_diffs") val assetsDiffs: List, + @Json(name = "exposures") val exposures: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt new file mode 100644 index 0000000000..645409ec89 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AssetDiff( + @Json(name = "asset_type") val assetType: String, + @Json(name = "asset") val asset: Asset, + @Json(name = "in") val inTransfer: List? = null, + @Json(name = "out") val outTransfer: List? = null, +) + +@JsonClass(generateAdapter = true) +data class Asset( + @Json(name = "chain_id") val chainId: Int? = null, + @Json(name = "logo_url") val logoUrl: String? = null, + @Json(name = "symbol") val symbol: String, +) + +@JsonClass(generateAdapter = true) +data class Transfer( + @Json(name = "value") val value: String, + @Json(name = "raw_value") val rawValue: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt new file mode 100644 index 0000000000..b11e7a6c88 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DomainScanResponse( + @Json(name = "status") val status: String, + @Json(name = "is_malicious") val isMalicious: Boolean?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt new file mode 100644 index 0000000000..90038d41c4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Exposure( + @Json(name = "asset") val asset: Asset, + @Json(name = "spenders") val spenders: Map, +) + +@JsonClass(generateAdapter = true) +data class SpenderDetails( + @Json(name = "exposure") val exposure: List, + @Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null, +) + +@JsonClass(generateAdapter = true) +data class ExposureDetail( + @Json(name = "value") val value: String, + @Json(name = "raw_value") val rawValue: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt new file mode 100644 index 0000000000..a62c20d307 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SimulationResponse( + @Json(name = "status") val status: String, + @Json(name = "account_summary") val accountSummary: AccountSummaryResponse, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt new file mode 100644 index 0000000000..13f229ead5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TransactionMetadata( + @Json(name = "domain") val domain: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt new file mode 100644 index 0000000000..9d45809013 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TransactionScanResponse( + @Json(name = "validation") val validation: ValidationResponse, + @Json(name = "simulation") val simulation: SimulationResponse, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt new file mode 100644 index 0000000000..08e86a0697 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ValidationResponse( + @Json(name = "status") val status: String, + @Json(name = "result_type") val resultType: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 88c03b6435..78db8380c3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -27,6 +27,7 @@ sealed class ApiConfig { TangemVisaAuth, TangemVisa, TangemCardSdk, + BlockAid, } private fun initializeId(): ID { @@ -37,6 +38,7 @@ sealed class ApiConfig { is TangemVisaAuth -> ID.TangemVisaAuth is TangemVisa -> ID.TangemVisa is TangemCardSdk -> ID.TangemCardSdk + is BlockAid -> ID.BlockAid } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt new file mode 100644 index 0000000000..008cd9ef09 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.utils.ProviderSuspend + +internal class BlockAid( + private val environmentConfigStorage: EnvironmentConfigStorage, +) : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + + override val environmentConfigs = listOf( + createProdEnvironment(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.blockaid.io/v0/", + headers = buildMap { + environmentConfigStorage.getConfigSync().blockAidApiKey?.let { apiKey -> + put("X-API-KEY", ProviderSuspend { apiKey }) + } + put("accept", ProviderSuspend { "application/json" }) + put("content-type", ProviderSuspend { "application/json" }) + }, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 5035774b7f..1a90974c75 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -46,6 +46,12 @@ internal object ApiConfigsModule { @IntoSet fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) + @Provides + @IntoSet + fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { + return BlockAid(environmentConfigStorage) + } + @Provides @IntoSet fun provideTangemCardSdkConfig(): ApiConfig = TangemCardSdk() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index dd14d5f63a..e05ca4cf1a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -4,6 +4,7 @@ import android.content.Context import com.squareup.moshi.Moshi import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.managers.ApiConfigsManager @@ -282,6 +283,29 @@ internal object NetworkModule { .create(T::class.java) } + @Provides + @Singleton + fun provideBlockAidApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, + apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, + ): BlockAidApi { + return createApi( + id = ApiConfig.ID.BlockAid, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ).applyTimeoutAnnotations() + }, + ) + } + private inline fun createApi( id: ApiConfig.ID, moshi: Moshi, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index d4bc388d6d..9da0b616fc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -14,4 +14,5 @@ data class EnvironmentConfig( val express: ExpressModel? = null, val devExpress: ExpressModel? = null, val stakeKitApiKey: String? = null, + val blockAidApiKey: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt index 02d7967f47..80f41f68b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt @@ -23,6 +23,7 @@ internal object EnvironmentConfigConverter : Converter createVisaAuthModel() is TangemVisa -> createVisaModel() is TangemCardSdk -> createTangemCardSdkModel() + is BlockAid -> createBlockAidSdkModel() } } @@ -218,6 +219,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } + private fun createBlockAidSdkModel(): Model { + return Model( + id = ApiConfig.ID.BlockAid, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.blockaid.io/v0/", + ), + ) + } + private fun String.checkHeaderValueOrEmpty(): String { for (i in this.indices) { val c = this[i] diff --git a/data/blockaid/.gitignore b/data/blockaid/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/blockaid/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/blockaid/build.gradle.kts b/data/blockaid/build.gradle.kts new file mode 100644 index 0000000000..c1855c991d --- /dev/null +++ b/data/blockaid/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.android.library) + id("configuration") +} + +android { + namespace = "com.tangem.data.blockaid" +} + +dependencies { + /* Project - Domain */ + implementation(projects.data.common) + implementation(projects.domain.blockaid) + implementation(projects.domain.blockaid.models) + + /* Project - Data */ + implementation(projects.core.datasource) + + /* Project - Core */ + implementation(projects.core.utils) + + /* DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /* Tangem libraries */ + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + + /* Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + /* Tests */ + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.turbine) +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt new file mode 100644 index 0000000000..5336bf9657 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -0,0 +1,128 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.transaction.* +import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.domain.blockaid.models.transaction.simultation.SimulationData +import com.domain.blockaid.models.transaction.simultation.TokenInfo +import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest +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.response.* +import javax.inject.Inject + +private const val SUCCESS_STATUS = "Success" +private const val DOMAIN_CHECKED_STATUS = "hit" +private const val VALIDATION_SAFE_STATUS = "Benign" + +internal class BlockAidMapper @Inject constructor() { + + fun mapToDomain(from: DomainScanResponse): CheckDAppResult { + return when { + from.status != DOMAIN_CHECKED_STATUS -> CheckDAppResult.FAILED_TO_VERIFY + from.isMalicious == true -> CheckDAppResult.UNSAFE + else -> CheckDAppResult.SAFE + } + } + + fun mapToDomain(from: TransactionScanResponse): CheckTransactionResult { + return CheckTransactionResult( + validation = when { + from.validation.status != SUCCESS_STATUS -> ValidationResult.FAILED_TO_VALIDATE + from.validation.resultType == VALIDATION_SAFE_STATUS -> ValidationResult.SAFE + else -> ValidationResult.UNSAFE + }, + simulation = if (from.simulation.status != SUCCESS_STATUS) { + SimulationResult.FailedToSimulate + } else { + mapSimulationSuccessResult(from.simulation.accountSummary) + }, + ) + } + + fun mapToEvmRequest(from: TransactionData): EvmTransactionScanRequest { + return EvmTransactionScanRequest( + chain = from.chain, + accountAddress = from.accountAddress, + method = from.method, + data = RpcData( + method = from.method, + params = (from.params as TransactionParams.Evm).params, + ), + metadata = TransactionMetadata(from.domainUrl), + ) + } + + fun mapToSolanaRequest(from: TransactionData): SolanaTransactionScanRequest { + return SolanaTransactionScanRequest( + chain = from.chain, + accountAddress = from.accountAddress, + metadata = TransactionMetadata(from.domainUrl), + method = from.method, + transactions = (from.params as TransactionParams.Solana).transactions, + ) + } + + private fun mapSimulationSuccessResult(from: AccountSummaryResponse): SimulationResult { + return when { + from.assetsDiffs.isEmpty() && from.exposures.isNotEmpty() -> mapApproveTransaction(from.exposures) + from.assetsDiffs.isNotEmpty() && from.exposures.isEmpty() -> mapSendReceiveTransaction(from.assetsDiffs) + else -> SimulationResult.FailedToSimulate + } + } + + private fun mapApproveTransaction(exposures: List): SimulationResult { + val amounts = exposures.flatMap { exposure -> + val tokenInfo = TokenInfo( + chainId = exposure.asset.chainId, + logoUrl = exposure.asset.logoUrl, + symbol = exposure.asset.symbol, + ) + exposure.spenders.flatMap { (_, spender) -> + val isUnlimited = spender.isApprovedForAll == true + spender.exposure.mapNotNull { detail -> + ApprovedAmount( + approvedAmount = detail.value.toBigDecimalOrNull() ?: return@mapNotNull null, + isUnlimited = isUnlimited, + tokenInfo = tokenInfo, + ) + } + } + } + return if (amounts.isNotEmpty()) { + SimulationResult.Success(SimulationData.Approve(amounts)) + } else { + SimulationResult.FailedToSimulate + } + } + + private fun mapSendReceiveTransaction(assetDiffs: List): SimulationResult { + val sendInfo = arrayListOf() + val receiveInfo = arrayListOf() + + assetDiffs.forEach { diff -> + val token = TokenInfo( + chainId = diff.asset.chainId, + logoUrl = diff.asset.logoUrl, + symbol = diff.asset.symbol, + ) + diff.outTransfer.orEmpty().forEach { transfer -> + transfer.value.toBigDecimalOrNull()?.let { amount -> + sendInfo.add(AmountInfo(amount = amount, token = token)) + } + } + diff.inTransfer.orEmpty().forEach { transfer -> + transfer.value.toBigDecimalOrNull()?.let { amount -> + receiveInfo.add(AmountInfo(amount = amount, token = token)) + } + } + } + + return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) { + SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo)) + } else { + SimulationResult.FailedToSimulate + } + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt new file mode 100644 index 0000000000..57e8db2559 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData + +interface BlockAidRepository { + + suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult + + suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt new file mode 100644 index 0000000000..837b699192 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -0,0 +1,40 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.domain.blockaid.models.transaction.TransactionParams +import com.tangem.datasource.api.common.blockaid.BlockAidApi +import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultBlockAidRepository @Inject constructor( + private val api: BlockAidApi, + private val dispatcherProvider: CoroutineDispatcherProvider, + private val mapper: BlockAidMapper, +) : BlockAidRepository { + + override suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult { + val response = withContext(dispatcherProvider.io) { + api.scanDomain(DomainScanRequest(data.url)) + } + return mapper.mapToDomain(response) + } + + override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult { + val response = withContext(dispatcherProvider.io) { + when (data.params) { + is TransactionParams.Evm -> { + api.scanJsonRpc(mapper.mapToEvmRequest(data)) + } + is TransactionParams.Solana -> { + api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) + } + } + } + return mapper.mapToDomain(response) + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt new file mode 100644 index 0000000000..95d5070701 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt @@ -0,0 +1,31 @@ +package com.tangem.data.blockaid + +import arrow.core.Either +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.tangem.domain.blockaid.BlockAidVerifier +import javax.inject.Inject + +/** + * Verifies the safety of DApps and WalletConnect transactions + */ +class DefaultBlockAidVerifier @Inject constructor( + private val repository: BlockAidRepository, +) : BlockAidVerifier { + + /** + * Checks if a DApp is safe to use + */ + override suspend fun verifyDApp(data: DAppData): Either { + return Either.catch { repository.verifyDAppDomain(data) } + } + + /** + * Checks the safety of a WalletConnect transaction and provides a simulation result + */ + override suspend fun verifyTransaction(data: TransactionData): Either { + return Either.catch { repository.verifyTransaction(data) } + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt new file mode 100644 index 0000000000..894e9aca12 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.blockaid.di + +import com.tangem.data.blockaid.BlockAidRepository +import com.tangem.data.blockaid.DefaultBlockAidRepository +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 BlockAidDataInternalModule { + + @Binds + @Singleton + fun bindRepository(repository: DefaultBlockAidRepository): BlockAidRepository +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt new file mode 100644 index 0000000000..1efc366513 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.blockaid.di + +import com.tangem.data.blockaid.DefaultBlockAidVerifier +import com.tangem.domain.blockaid.BlockAidVerifier +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface BlockAidDataModule { + + @Binds + @Singleton + fun bindVerifier(verifier: DefaultBlockAidVerifier): BlockAidVerifier +} \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt new file mode 100644 index 0000000000..ac9639b167 --- /dev/null +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -0,0 +1,155 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.transaction.SimulationResult +import com.domain.blockaid.models.transaction.ValidationResult +import com.domain.blockaid.models.transaction.simultation.SimulationData +import com.tangem.datasource.api.common.blockaid.models.response.* +import org.junit.Assert.* +import org.junit.Test +import java.math.BigDecimal + +class BlockAidMapperTest { + + private val mapper = BlockAidMapper() + + @Test + fun whenStatusHitAndIsMaliciousFalseThenMapToDomainReturnsSafe() { + val response = DomainScanResponse(status = "hit", isMalicious = false) + val result = mapper.mapToDomain(response) + assertEquals(CheckDAppResult.SAFE, result) + } + + @Test + fun whenStatusHitAndIsMaliciousTrueThenMapToDomainReturnsUnsafe() { + val response = DomainScanResponse(status = "hit", isMalicious = true) + val result = mapper.mapToDomain(response) + assertEquals(CheckDAppResult.UNSAFE, result) + } + + @Test + fun whenStatusNotHitThenMapToDomainReturnsFailedToVerify() { + val response = DomainScanResponse(status = "miss", isMalicious = false) + val result = mapper.mapToDomain(response) + assertEquals(CheckDAppResult.FAILED_TO_VERIFY, result) + } + + @Test + fun whenResponseBenignValidationThenReturnsSafeValidation() { + val spenderDetails = SpenderDetails( + isApprovedForAll = true, + exposure = listOf(ExposureDetail(value = "1000.0", rawValue = "0x123")), + ) + val exposure = Exposure( + asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE"), + spenders = mapOf("spender" to spenderDetails), + ) + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(assetsDiffs = emptyList(), exposures = listOf(exposure)), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.SAFE, result.validation) + + val simulation = result.simulation as? SimulationResult.Success + assertNotNull(simulation) + + val approve = simulation?.data as? SimulationData.Approve + assertNotNull(approve) + assertEquals(1, approve?.approvedAmounts?.size) + assertEquals(BigDecimal("1000.0"), approve?.approvedAmounts?.first()?.approvedAmount) + assertTrue(approve?.approvedAmounts?.first()?.isUnlimited == true) + } + + @Test + fun whenResponseBenignValidationAndSuccessSimulationThenReturnsSendReceiveResult() { + val assetDiff = AssetDiff( + assetType = "ERC20", + asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH"), + inTransfer = listOf(Transfer(value = "2.0", rawValue = "0x1")), + outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")), + ) + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(exposures = emptyList(), assetsDiffs = listOf(assetDiff)), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.SAFE, result.validation) + + val simulation = result.simulation as? SimulationResult.Success + assertNotNull(simulation) + + val data = simulation?.data as? SimulationData.SendAndReceive + assertNotNull(data) + assertEquals(BigDecimal("1.5"), data?.send?.first()?.amount) + assertEquals(BigDecimal("2.0"), data?.receive?.first()?.amount) + } + + @Test + fun whenResponseErrorValidationThenReturnsFailedToValidate() { + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Error", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(emptyList(), emptyList()), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.FAILED_TO_VALIDATE, result.validation) + assertTrue(result.simulation is SimulationResult.FailedToSimulate) + } + + @Test + fun whenResponseNotBenignThenReturnsValidationUnsafe() { + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Phishing"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(emptyList(), emptyList()), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.UNSAFE, result.validation) + } + + @Test + fun whenResponseSimulationNotSuccessThenReturnsSimulationFailedToSimulate() { + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Error", + accountSummary = AccountSummaryResponse(emptyList(), emptyList()), + ), + ) + + val result = mapper.mapToDomain(response) + assertTrue(result.simulation is SimulationResult.FailedToSimulate) + } + + @Test + fun whenResponseSimulationIsEmptyThenReturnsFailedToSimulate() { + val txResponse = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse( + assetsDiffs = emptyList(), + exposures = emptyList(), + ), + ), + ) + + val result = mapper.mapToDomain(txResponse) + assertTrue(result.simulation is SimulationResult.FailedToSimulate) + } +} \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt new file mode 100644 index 0000000000..bb919dbdb7 --- /dev/null +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -0,0 +1,122 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.domain.blockaid.models.transaction.TransactionParams +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.EvmTransactionScanRequest +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.TransactionScanResponse +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import io.mockk.* +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultBlockAidRepositoryTest { + + @MockK + private lateinit var api: BlockAidApi + + @MockK + private lateinit var mapper: BlockAidMapper + + @MockK + private lateinit var dispatcherProvider: CoroutineDispatcherProvider + + private lateinit var repository: DefaultBlockAidRepository + + private val testDispatcher = StandardTestDispatcher() + + @Before + fun setup() { + MockKAnnotations.init(this) + Dispatchers.setMain(testDispatcher) + every { dispatcherProvider.io } returns testDispatcher + repository = DefaultBlockAidRepository(api, dispatcherProvider, mapper) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun whenVerifyAppDomainThenCallsApiAndMapsResult() = runTest { + val url = "https://example.com" + val domainData = DAppData(url) + val domainResponse = DomainScanResponse(status = "hit", isMalicious = false) + val expectedResult = CheckDAppResult.SAFE + + coEvery { api.scanDomain(DomainScanRequest(url)) } returns domainResponse + every { mapper.mapToDomain(domainResponse) } returns expectedResult + + val result = repository.verifyDAppDomain(domainData) + + assertEquals(expectedResult, result) + coVerify { api.scanDomain(DomainScanRequest(url)) } + verify { mapper.mapToDomain(domainResponse) } + } + + @Test + fun whenVerifyEvmTransactionThenCallsScanJsonRpcAndMaps() = runTest { + val data = TransactionData( + chain = "ethereum", + accountAddress = "0xabc", + domainUrl = "https://uniswap.org", + method = "eth_sendTransaction", + params = TransactionParams.Evm(params = "some-params"), + ) + + val request = mockk() + val response = mockk() + val expectedResult = mockk() + + every { mapper.mapToEvmRequest(data) } returns request + coEvery { api.scanJsonRpc(request) } returns response + every { mapper.mapToDomain(response) } returns expectedResult + + val result = repository.verifyTransaction(data) + + assertEquals(expectedResult, result) + coVerify { api.scanJsonRpc(request) } + verify { mapper.mapToEvmRequest(data) } + verify { mapper.mapToDomain(response) } + } + + @Test + fun whenVerifySolanaTransactionThenCallsScanSolanaMessageAndMapsResult() = runTest { + val data = TransactionData( + chain = "mainnet", + accountAddress = "/Rd2TLl...", + domainUrl = "https://example.com", + method = "signTransaction", + params = TransactionParams.Solana(transactions = listOf("TX_PAYLOAD_BASE64")), + ) + + val request = mockk() + val response = mockk() + val expectedResult = mockk() + + every { mapper.mapToSolanaRequest(data) } returns request + coEvery { api.scanSolanaMessage(request) } returns response + every { mapper.mapToDomain(response) } returns expectedResult + + val result = repository.verifyTransaction(data) + + assertEquals(expectedResult, result) + coVerify { api.scanSolanaMessage(request) } + verify { mapper.mapToSolanaRequest(data) } + verify { mapper.mapToDomain(response) } + } +} \ No newline at end of file diff --git a/domain/blockaid/.gitignore b/domain/blockaid/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/blockaid/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/blockaid/build.gradle.kts b/domain/blockaid/build.gradle.kts new file mode 100644 index 0000000000..9fda0da981 --- /dev/null +++ b/domain/blockaid/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + /* Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.blockaid.models) + + /* Other */ + implementation(deps.moshi.adapters) +} \ No newline at end of file diff --git a/domain/blockaid/models/.gitignore b/domain/blockaid/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/blockaid/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/blockaid/models/build.gradle.kts b/domain/blockaid/models/build.gradle.kts new file mode 100644 index 0000000000..14f878155b --- /dev/null +++ b/domain/blockaid/models/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.ksp) + id("configuration") +} +dependencies { + /* Other */ + implementation(deps.moshi) + ksp(deps.moshi.kotlin.codegen) +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt new file mode 100644 index 0000000000..480b3d8fd9 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt @@ -0,0 +1,22 @@ +package com.domain.blockaid.models.dapp + +/** + * Result of BlockAid's DApp domain check + */ +enum class CheckDAppResult { + + /** + * DApp was confirmed safe + */ + SAFE, + + /** + * DApp was confirmed unsafe (known security risk) + */ + UNSAFE, + + /** + * Check wasn't performed, BlockAid cannot guarantee DApp's safety + */ + FAILED_TO_VERIFY, +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt new file mode 100644 index 0000000000..25d1882031 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt @@ -0,0 +1,11 @@ +package com.domain.blockaid.models.dapp + +/** + * Data BlockAid needs to verify DApp domain + * + * @property url DApp's domain url + */ +@JvmInline +value class DAppData( + val url: String, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt new file mode 100644 index 0000000000..d7244d6023 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt @@ -0,0 +1,12 @@ +package com.domain.blockaid.models.transaction + +/** + * Result of BlockAid's transaction check + * + * @property validation Indicates whether the transaction is considered safe or unsafe + * @property simulation Provides insight into the expected outcome of the transaction + */ +data class CheckTransactionResult( + val validation: ValidationResult, + val simulation: SimulationResult, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt new file mode 100644 index 0000000000..c44441a922 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt @@ -0,0 +1,21 @@ +package com.domain.blockaid.models.transaction + +import com.domain.blockaid.models.transaction.simultation.SimulationData + +/** + * Result of BlockAid's transaction simulation + */ +sealed class SimulationResult { + + /** + * Simulation was successfully performed and returned data + */ + data class Success( + val data: SimulationData, + ) : SimulationResult() + + /** + * Simulation wasn't performed, BlockAid cannot guarantee transaction's behavior + */ + data object FailedToSimulate : SimulationResult() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt new file mode 100644 index 0000000000..01fb3a9321 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt @@ -0,0 +1,36 @@ +package com.domain.blockaid.models.transaction + +/** + * Input data for BlockAid's transaction check + * + * @property chain Chain name, for ex. "ethereum" + * @property accountAddress The address of the account (wallet) received the request in hex string format + * @property method Transaction method, for ex. "eth_signTransaction" + * @property domainUrl Url of the DApp domain + */ +data class TransactionData( + val chain: String, + val accountAddress: String, + val method: String, + val domainUrl: String, + val params: TransactionParams, +) + +sealed class TransactionParams { + + /** + * Parameters for Ethereum based transactions, can be taken from [WcSdkSessionRequest.JSONRPCRequest.params] + */ + data class Evm( + val params: String, + ) : TransactionParams() + + /** + * Parameters for Solana transactions + * + * @property transactions Base64-encoded serialized list of transactions + */ + data class Solana( + val transactions: List, + ) : TransactionParams() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt new file mode 100644 index 0000000000..0adde02aa8 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt @@ -0,0 +1,22 @@ +package com.domain.blockaid.models.transaction + +/** + * Result of BlockAid's transaction validation + */ +enum class ValidationResult { + + /** + * Transaction was confirmed safe + */ + SAFE, + + /** + * Transaction was confirmed unsafe + */ + UNSAFE, + + /** + * Validation wasn't performed, BlockAid cannot guarantee transaction's safety + */ + FAILED_TO_VALIDATE, +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt new file mode 100644 index 0000000000..14e8b4c6d6 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt @@ -0,0 +1,8 @@ +package com.domain.blockaid.models.transaction.simultation + +import java.math.BigDecimal + +data class AmountInfo( + val amount: BigDecimal, + val token: TokenInfo, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt new file mode 100644 index 0000000000..6fdad58ec2 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt @@ -0,0 +1,9 @@ +package com.domain.blockaid.models.transaction.simultation + +import java.math.BigDecimal + +data class ApprovedAmount( + val approvedAmount: BigDecimal, + val isUnlimited: Boolean, + val tokenInfo: TokenInfo, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt new file mode 100644 index 0000000000..afd8fea9e9 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt @@ -0,0 +1,22 @@ +package com.domain.blockaid.models.transaction.simultation + +/** + * Result of a successful transaction simulation. + */ +sealed class SimulationData { + + /** + * Represents a swap/send/sell operations with specified send and receive amounts (can be multiple amounts for NFT) + */ + data class SendAndReceive( + val send: List, + val receive: List, + ) : SimulationData() + + /** + * Represents an approve operation with the specified amount (can be multiple amounts for NFT) + */ + data class Approve( + val approvedAmounts: List, + ) : SimulationData() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt new file mode 100644 index 0000000000..889bcf1d1e --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt @@ -0,0 +1,7 @@ +package com.domain.blockaid.models.transaction.simultation + +data class TokenInfo( + val chainId: Int?, + val logoUrl: String?, + val symbol: String, +) \ No newline at end of file diff --git a/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt b/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt new file mode 100644 index 0000000000..e41f57e783 --- /dev/null +++ b/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.blockaid + +import arrow.core.Either +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData + +/** + * Verifies the safety of DApps and WalletConnect transactions + */ +interface BlockAidVerifier { + + /** + * Checks if a DApp is safe to use + */ + suspend fun verifyDApp(data: DAppData): Either + + /** + * Checks the safety of a WalletConnect transaction and provides a simulation result + */ + suspend fun verifyTransaction(data: TransactionData): Either +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f7384a646..01e4240001 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -289,6 +289,8 @@ include(":domain:nft") include(":domain:nft:models") include(":domain:networks") include(":domain:quotes") +include(":domain:blockaid") +include(":domain:blockaid:models") // endregion Domain modules // region Data modules @@ -316,4 +318,5 @@ include(":data:networks") include(":data:nft") include(":data:onramp") include(":data:quotes") +include(":data:blockaid") // endregion Data modules \ No newline at end of file