Updated on 2026-08-14
This commit is contained in:
parent
d9a539b64d
commit
d68ad52a45
16 changed files with 278 additions and 2 deletions
|
|
@ -59,6 +59,22 @@ internal class DefaultAuthProvider(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String> {
|
||||
return ProviderSuspend {
|
||||
when (apiEnvironment.invoke()) {
|
||||
ApiEnvironment.DEV,
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev // TODO add gaslessTxApiKeyDev
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
-> environmentConfigStorage.getConfigSync().tangemApiKeyStage // TODO add gaslessTxApiKeyStage
|
||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey // TODO add gaslessTxApiKey
|
||||
else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}")
|
||||
} ?: error("No gasless tx api config provided")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getWallets(): List<UserWallet> {
|
||||
return if (shouldUseNewListRepository) {
|
||||
userWalletsListRepository.userWalletsSync()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.transaction.converters.GaslessTokenDtoToCryptoCurrencyConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigInteger
|
||||
|
||||
class DefaultGaslessTransactionRepository(
|
||||
private val gaslessTxServiceApi: GaslessTxServiceApi,
|
||||
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
) : GaslessTransactionRepository {
|
||||
|
||||
private val gaslessTokenDtoToCryptoCurrency = GaslessTokenDtoToCryptoCurrencyConverter()
|
||||
private val supportedTokensState = MutableStateFlow<Set<CryptoCurrency>?>(null)
|
||||
|
||||
override fun isNetworkSupported(network: Network): Boolean {
|
||||
val blockchain = Blockchain.fromNetworkId(network.backendId) ?: return false
|
||||
return SUPPORTED_BLOCKCHAINS.contains(blockchain)
|
||||
}
|
||||
|
||||
override suspend fun getSupportedTokens(): Set<CryptoCurrency> {
|
||||
return withContext(coroutineDispatcherProvider.io) {
|
||||
val storedTokens = supportedTokensState.value
|
||||
if (storedTokens != null) {
|
||||
return@withContext storedTokens
|
||||
}
|
||||
val supportedTokensData = gaslessTxServiceApi.getSupportedTokens().getOrThrow()
|
||||
if (supportedTokensData.isSuccess) {
|
||||
val supportedTokens = supportedTokensData.result.tokens.map {
|
||||
gaslessTokenDtoToCryptoCurrency.convert(it)
|
||||
}.toSet()
|
||||
// update local cache
|
||||
supportedTokensState.update { supportedTokens }
|
||||
return@withContext supportedTokens
|
||||
} else {
|
||||
error("Gasless service returned unsuccessful response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTokenFeeReceiverAddress(): String {
|
||||
return TOKEN_RECEIVER_ADDRESS
|
||||
}
|
||||
|
||||
override fun getBaseGasForTransaction(): BigInteger {
|
||||
return BASE_GAS_FOR_TRANSACTION
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TOKEN_RECEIVER_ADDRESS = "0x"
|
||||
|
||||
val BASE_GAS_FOR_TRANSACTION: BigInteger = BigInteger("100000")
|
||||
val SUPPORTED_BLOCKCHAINS = arrayOf(
|
||||
Blockchain.Ethereum,
|
||||
Blockchain.BSC,
|
||||
Blockchain.Base,
|
||||
Blockchain.Polygon,
|
||||
Blockchain.Arbitrum,
|
||||
Blockchain.XDC,
|
||||
Blockchain.Optimism,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.data.transaction.converters
|
||||
|
||||
import com.tangem.datasource.api.gasless.models.GaslessTokenDTO
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class GaslessTokenDtoToCryptoCurrencyConverter : Converter<GaslessTokenDTO, CryptoCurrency> {
|
||||
|
||||
override fun convert(value: GaslessTokenDTO): CryptoCurrency {
|
||||
TODO("implement conversion logic here")
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ interface GaslessTransactionRepository {
|
|||
|
||||
fun isNetworkSupported(network: Network): Boolean
|
||||
|
||||
fun getSupportedTokens(): Set<CryptoCurrency>
|
||||
suspend fun getSupportedTokens(): Set<CryptoCurrency>
|
||||
|
||||
fun getTokenFeeReceiverAddress(): String
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ class GetAvailableFeeTokensUseCase(
|
|||
} ?: raiseIllegalStateError("no native currency found")
|
||||
}
|
||||
|
||||
private fun getGaslessTokens(userCurrenciesStatuses: List<CryptoCurrencyStatus>): List<CryptoCurrencyStatus> {
|
||||
private suspend fun getGaslessTokens(
|
||||
userCurrenciesStatuses: List<CryptoCurrencyStatus>,
|
||||
): List<CryptoCurrencyStatus> {
|
||||
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens()
|
||||
return userCurrenciesStatuses
|
||||
.asSequence()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue