Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-27 10:59:46 +02:00
parent 7c5777974a
commit 5579cc1372
16 changed files with 127 additions and 60 deletions

View file

@ -1,5 +1,7 @@
package com.tangem.tap.di
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
@ -8,9 +10,12 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -64,6 +69,21 @@ internal object ActivityModule {
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
@Provides
@Singleton
fun provideExchangeService(
environmentConfigStorage: EnvironmentConfigStorage,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
moonPayApi: MoonPayApi,
): SellService {
return MoonPayService(
api = moonPayApi,
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey },
secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey },
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
@Provides
@Singleton
fun provideGetPolkadotCheckHasResetUseCase(

View file

@ -16,14 +16,11 @@ import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
@ -42,8 +39,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -83,9 +79,9 @@ internal class MainViewModel @Inject constructor(
private val apiConfigsManager: ApiConfigsManager,
private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -195,22 +191,12 @@ internal class MainViewModel @Inject constructor(
private fun initializeOffRamp() {
viewModelScope.launch {
val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync())
appStateHolder.sellService = sellService
sellService.update()
}
}
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
isLogEnabled = LogConfig.network.moonPayService,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}

View file

@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
internal class DefaultRampManager(
private val sellService: Provider<ExchangeService>,
private val sellService: Provider<SellService>,
private val expressServiceFetcher: ExpressServiceFetcher,
private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider,
@ -90,7 +90,7 @@ internal class DefaultRampManager(
return availabilityState.toReason(cryptoCurrency.name)
}
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
override fun getSellInitializationStatus(): Flow<SellServiceInitializationStatus> {
return sellService.invoke().initializationStatus
}
@ -100,7 +100,7 @@ internal class DefaultRampManager(
}
}
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<SellServiceInitializationStatus> {
return expressServiceFetcher.getInitializationStatus(userWalletId)
}

View file

@ -5,11 +5,11 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any>
typealias SellServiceInitializationStatus = Lce<Throwable, Any>
interface ExchangeService {
interface SellService {
val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
val initializationStatus: StateFlow<SellServiceInitializationStatus>
suspend fun update()

View file

@ -5,7 +5,9 @@ import android.util.Base64
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.moonpay.MoonPayCurrencies
import com.tangem.datasource.api.moonpay.MoonPayUserStatus
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
@ -14,9 +16,10 @@ import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@ -24,25 +27,18 @@ import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val isLogEnabled: Boolean,
private val api: MoonPayApi,
private val apiKeyProvider: Provider<String>,
private val secretKeyProvider: Provider<String>,
private val userWalletProvider: () -> UserWallet?,
) : ExchangeService {
) : SellService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
override val initializationStatus: StateFlow<SellServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
private val _initializationStatus: MutableStateFlow<SellServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MoonPayApi by lazy {
createRetrofitInstance(
baseUrl = MoonPayApi.MOOONPAY_BASE_URL,
logEnabled = isLogEnabled,
).create(MoonPayApi::class.java)
}
private var status: MoonPayStatus? = null
override suspend fun update() {
@ -51,7 +47,7 @@ class MoonPayService(
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) {
is Result.Failure -> {
Timber.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError()
@ -60,7 +56,7 @@ class MoonPayService(
is Result.Success -> result.data
}
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) {
is Result.Failure -> {
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
@ -78,7 +74,7 @@ class MoonPayService(
MoonPayAvailableCurrency(
currencyCode = currency.code,
networkCode = currency.metadata?.networkCode ?: return@mapNotNull null,
contractAddress = currency.metadata.contractAddress,
contractAddress = currency.metadata?.contractAddress,
)
}
@ -167,7 +163,7 @@ class MoonPayService(
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("apiKey", apiKeyProvider())
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
@ -191,7 +187,7 @@ class MoonPayService(
private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)

View file

@ -7,7 +7,7 @@ import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.SellService
import org.rekotlin.Action
import org.rekotlin.Store
import javax.inject.Inject
@ -19,7 +19,7 @@ import javax.inject.Inject
class AppStateHolder @Inject constructor() : ReduxStateHolder {
var mainStore: Store<AppState>? = null
var sellService: ExchangeService? = null
var sellService: SellService? = null
override fun dispatch(action: Action) {
mainStore?.dispatch(action)

View file

@ -27,6 +27,7 @@ sealed class ApiConfig {
TangemPay,
BlockAid,
YieldSupply,
MoonPay,
}
private fun initializeId(): ID {
@ -37,6 +38,7 @@ sealed class ApiConfig {
is TangemPay -> ID.TangemPay
is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
/**
* MoonPay [ApiConfig]
*/
internal class MoonPay : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]", // TODO [REDACTED_TASK_KEY]
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.network.exchangeServices.moonpay
package com.tangem.datasource.api.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@ -7,17 +7,11 @@ import retrofit2.http.Query
interface MoonPayApi {
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL)
@GET("v4/ip_address/")
suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL)
@GET("v3/currencies/")
suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
}
@JsonClass(generateAdapter = true)

View file

@ -76,4 +76,10 @@ internal object ApiConfigsModule {
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
}
@Provides
@IntoSet
fun provideMoonPayConfig(): ApiConfig {
return MoonPay()
}
}

View file

@ -5,12 +5,14 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.MoonPay
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
@ -130,4 +132,13 @@ internal object NetworkModule {
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideMoonPayApi(retrofitApiBuilder: RetrofitApiBuilder): MoonPayApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.MoonPay,
applyTimeoutAnnotations = false,
)
}
}

View file

@ -197,6 +197,7 @@ internal class RetrofitApiBuilder @Inject constructor(
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
ApiConfig.ID.MoonPay,
)
}
}

View file

@ -56,6 +56,7 @@ class ApiConfigTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
ApiConfig.ID.MoonPay -> MoonPay()
}
}
}

View file

@ -103,6 +103,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
ApiConfig.ID.MoonPay -> MoonPay()
}
}
}
@ -115,6 +116,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> createStakeKitModel()
ApiConfig.ID.TangemPay -> createTangemPayModel()
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
ApiConfig.ID.MoonPay -> createMoonPayModel()
}
}
@ -257,6 +259,16 @@ internal class ProdApiConfigsManagerTest {
)
}
private fun createMoonPayModel(): TestModel {
return TestModel(
id = ApiConfig.ID.MoonPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]

View file

@ -10,11 +10,6 @@ object LogConfig {
}
object NetworkLogConfig {
const val mercuryoService: Boolean = false
const val moonPayService: Boolean = false
const val utorgService: Boolean = false
val tangemTechService: Boolean = BuildConfig.LOG_ENABLED
val paymentologyApiService: Boolean = BuildConfig.LOG_ENABLED
val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED
}

View file

@ -83,7 +83,7 @@ reownCore = "1.4.11"
reownWeb3 = "1.4.11"
prettyLogger = "2.2.0"
okHttp-prettyLogging = "3.1.0"
chucker = "4.0.0"
chucker = "4.2.0"
mlKit-barcodeScanning = "17.3.0"
androidXCamera = "1.4.2"
listenableFuture = "1.0"