Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-18 12:47:25 +04:00
parent 7a8f55b589
commit 2af08f578c
46 changed files with 1035 additions and 0 deletions

View file

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

View file

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

View file

@ -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<String> = 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,
)

View file

@ -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<String> = listOf("simulation, validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,
@Json(name = "account_address") val accountAddress: String,
@Json(name = "transactions") val transactions: List<String>,
)

View file

@ -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<AssetDiff>,
@Json(name = "exposures") val exposures: List<Exposure>,
)

View file

@ -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<Transfer>? = null,
@Json(name = "out") val outTransfer: List<Transfer>? = 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,
)

View file

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

View file

@ -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<String, SpenderDetails>,
)
@JsonClass(generateAdapter = true)
data class SpenderDetails(
@Json(name = "exposure") val exposure: List<ExposureDetail>,
@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,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" })
},
)
}

View file

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

View file

@ -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<BlockAidApi>(
id = ApiConfig.ID.BlockAid,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
).applyTimeoutAnnotations()
},
)
}
private inline fun <reified T> createApi(
id: ApiConfig.ID,
moshi: Moshi,

View file

@ -14,4 +14,5 @@ data class EnvironmentConfig(
val express: ExpressModel? = null,
val devExpress: ExpressModel? = null,
val stakeKitApiKey: String? = null,
val blockAidApiKey: String? = null,
)

View file

@ -23,6 +23,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
express = value.express,
devExpress = value.devExpress,
stakeKitApiKey = value.stakeKitApiKey,
blockAidApiKey = value.blockaidApiKey,
)
}
}

View file

@ -38,6 +38,7 @@ class EnvironmentConfigModel(
@Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?,
@Json(name = "moralisApiKey") val moralisApiKey: String?,
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
)
@JsonClass(generateAdapter = true)

View file

@ -91,6 +91,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
is TangemVisaAuth -> 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]