Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-05 13:10:02 +03:00
parent d9a539b64d
commit d68ad52a45
16 changed files with 278 additions and 2 deletions

View file

@ -18,6 +18,8 @@ interface AuthProvider {
fun getApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String>
fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String>
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/

View file

@ -31,6 +31,7 @@ sealed class ApiConfig {
YieldSupply,
MoonPay,
News,
GaslessTxService,
}
private fun initializeId(): ID {
@ -45,6 +46,7 @@ sealed class ApiConfig {
is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
is News -> ID.News
is GaslessTxService -> ID.GaslessTxService
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
* Gasless transactions [ApiConfig]
*/
internal class GaslessTxService(
private val authProvider: AuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createDevEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = PROD_BASE_URL,
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = DEV_BASE_URL,
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createHeaders(environment: ApiEnvironment) = buildMap {
putAll(RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
put(
key = "Authorization",
value = ProviderSuspend {
"Bearer ${authProvider.getGaslessServiceApiKey(Provider { environment }).invoke()}"
},
)
}
private companion object {
private const val PROD_BASE_URL = "https://api.tangem.org/"
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.gasless
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.gasless.models.GaslessServiceResponse
import com.tangem.datasource.api.gasless.models.GaslessSupportedTokens
import retrofit2.http.GET
interface GaslessTxServiceApi {
@GET("api/v1/tokens")
suspend fun getSupportedTokens(): ApiResponse<GaslessServiceResponse<GaslessSupportedTokens>>
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessServiceResponse<T>(
@Json(name = "result") val result: T,
@Json(name = "success") val isSuccess: Boolean,
@Json(name = "timestamp") val timestamp: String,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessSupportedTokens(
@Json(name = "tokens") val tokens: List<GaslessTokenDTO>,
)

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessTokenDTO(
@Json(name = "tokenAddress") val tokenAddress: String,
@Json(name = "tokenSymbol") val tokenSymbol: String,
@Json(name = "tokenName") val tokenName: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "chainId") val chainId: Int,
@Json(name = "chain") val chain: String,
)

View file

@ -15,6 +15,7 @@ import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.news.NewsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.TangemPayAuthApi
import com.tangem.datasource.api.stakekit.StakeKitApi
@ -188,4 +189,13 @@ internal object NetworkModule {
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideGaslessTxServiceApi(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.GaslessTxService,
applyTimeoutAnnotations = false,
)
}
}

View file

@ -75,6 +75,11 @@ class ApiConfigTest {
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = mockk())
ApiConfig.ID.GaslessTxService -> GaslessTxService(
authProvider = appAuthProvider,
appVersionProvider = mockk(),
appInfoProvider = mockk(),
)
}
}
}

View file

@ -33,6 +33,7 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
const val BLOCK_AID_API_KEY = "block_aid_api_key"
const val TANGEM_API_KEY = "tangem_api_key"
const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
const val YIELD_MODULE_KEY = "yield_module_api_key"
const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"

View file

@ -12,6 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
@ -45,6 +46,7 @@ internal class ProdApiConfigsManagerTest {
private val appAuthProvider = mockk<AuthProvider>()
private val appInfoProvider = mockk<AppInfoProvider>()
private val tangemApiKeyProvider = mockk<ProviderSuspend<String>>()
private val tangemGaslessApiKeyProvider = mockk<ProviderSuspend<String>>()
private lateinit var manager: ProdApiConfigsManager
@ -63,6 +65,8 @@ internal class ProdApiConfigsManagerTest {
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { p2pEthPoolAuthProvider.getApiKey() } returns P2P_API_KEY
every { appAuthProvider.getApiKey(any()) } returns tangemApiKeyProvider
every { appAuthProvider.getGaslessServiceApiKey(any()) } returns tangemGaslessApiKeyProvider
coEvery { tangemGaslessApiKeyProvider.invoke() } returns TANGEM_GASLESS_API_KEY
coEvery { tangemApiKeyProvider.invoke() } returns TANGEM_API_KEY
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
@ -117,6 +121,11 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = appVersionProvider)
ApiConfig.ID.GaslessTxService -> GaslessTxService(
authProvider = appAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}
}
}
@ -133,6 +142,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.P2PEthPool -> createP2PModel()
ApiConfig.ID.News -> createNewsModel()
ApiConfig.ID.TangemPayAuth -> createTangemPayAuthModel()
ApiConfig.ID.GaslessTxService -> createGaslessTxServiceModel()
}
}
@ -274,6 +284,37 @@ internal class ProdApiConfigsManagerTest {
)
}
private fun createGaslessTxServiceModel(): TestModel {
val (environment, baseUrl) = when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV to "[REDACTED_ENV_URL]"
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD to "https://tangem.com/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
return TestModel(
id = ApiConfig.ID.GaslessTxService,
expected = ApiEnvironmentConfig(
environment = environment,
baseUrl = baseUrl,
headers = mapOf(
"Authorization" to ProviderSuspend { "Bearer $TANGEM_GASLESS_API_KEY" },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
}
private fun createBlockAidSdkModel(): TestModel {
return TestModel(
id = ApiConfig.ID.BlockAid,