Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-21 12:09:33 +03:00
commit 85423227c4
390 changed files with 11140 additions and 1722 deletions

View file

@ -7,9 +7,12 @@ internal class DateTimeAdapter : JsonAdapter<DateTime>() {
@FromJson
override fun fromJson(reader: JsonReader): DateTime? {
val dateString = reader.nextString() ?: return null
return DateTime.parse(dateString)
return if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<DateTime>()
} else {
val dateString = reader.nextString()
DateTime.parse(dateString)
}
}
@ToJson

View file

@ -10,8 +10,12 @@ internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
@FromJson
override fun fromJson(reader: JsonReader): LocalDate? {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
return if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<LocalDate>()
} else {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
}
}
@ToJson

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
@ -26,6 +27,8 @@ data class TokenMarketInfoResponse(
val insights: Insights?,
@Json(name = "metrics")
val metrics: Metrics?,
@Json(name = "security_data")
val securityData: SecurityData?,
@Json(name = "links")
val links: Links?,
@Json(name = "price_performance")
@ -144,6 +147,28 @@ data class TokenMarketInfoResponse(
val allTime: Range?,
)
@JsonClass(generateAdapter = true)
data class SecurityData(
@Json(name = "total_security_score")
val totalSecurityScore: Float,
@Json(name = "provider_data")
val providerData: List<ProviderData>,
)
@JsonClass(generateAdapter = true)
data class ProviderData(
@Json(name = "provider_id")
val providerId: String,
@Json(name = "provider_name")
val providerName: String,
@Json(name = "link")
val link: String?,
@Json(name = "security_score")
val securityScore: Float,
@Json(name = "last_audit_date")
val lastAuditDate: DateTime?,
)
@JsonClass(generateAdapter = true)
data class Range(
@Json(name = "low_price")

View file

@ -1,178 +0,0 @@
package com.tangem.datasource.api.onramp
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
internal class MockedOnrampApi : OnrampApi {
override suspend fun getCurrencies(): ApiResponse<List<OnrampCurrencyDTO>> = ApiResponse.Success(
COUNTRIES.map(OnrampCountryDTO::defaultCurrency),
)
override suspend fun getCountries(): ApiResponse<List<OnrampCountryDTO>> = ApiResponse.Success(COUNTRIES + RUSSIA)
override suspend fun getCountryByIp(): ApiResponse<OnrampCountryDTO> = ApiResponse.Success(RUSSIA)
override suspend fun getPaymentMethods(): ApiResponse<List<PaymentMethodDTO>> = ApiResponse.Success(
listOf(
PaymentMethodDTO(id = "google", name = "Google Play", image = ""),
PaymentMethodDTO(id = "apple", name = "Apple Pay", image = ""),
PaymentMethodDTO(id = "card", name = "Card", image = ""),
),
)
override suspend fun getPairs(body: OnrampPairsRequest): ApiResponse<List<OnrampPairDTO>> = ApiResponse.Success(
listOf(
OnrampPairDTO(
fromCurrencyCode = "USD",
to = OnrampDestinationDTO(contractAddress = "0xcontract_address", network = "ethereum"),
providers = listOf(),
),
),
)
override suspend fun getQuote(
fromCurrencyCode: String,
toContractAddress: String,
toNetwork: String,
paymentMethod: String,
countryCode: String,
fromAmount: String,
toDecimals: Int,
providerId: String,
): ApiResponse<OnrampQuoteResponse> {
TODO("Not yet implemented")
}
override suspend fun getData(
fromCurrencyCode: String,
toContractAddress: String,
toNetwork: String,
paymentMethod: String,
countryCode: String,
fromAmount: String,
toDecimals: Int,
providerId: String,
toAddress: String,
redirectUrl: String,
language: String?,
theme: String?,
requestId: String,
): ApiResponse<OnrampDataResponse> {
TODO("Not yet implemented")
}
override suspend fun getStatus(txId: String): ApiResponse<OnrampStatusResponse> {
TODO("Not yet implemented")
}
private companion object {
private val RUSSIA = OnrampCountryDTO(
name = "Russia",
code = "RU",
image = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
alpha3 = "RUS",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Russian ruble",
code = "RUB",
image = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
precision = 2,
),
onrampAvailable = false,
)
private val COUNTRIES = listOf(
OnrampCountryDTO(
name = "United States of America",
code = "USA",
image = "https://hatscripts.github.io/circle-flags/flags/us.svg",
alpha3 = "USA",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "US Dollar",
code = "USD",
image = "https://hatscripts.github.io/circle-flags/flags/us.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Europe Union",
code = "EU",
image = "https://hatscripts.github.io/circle-flags/flags/eu.svg",
alpha3 = "EUR",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Euro",
code = "EUR",
image = "https://hatscripts.github.io/circle-flags/flags/eu.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Great Britain",
code = "GB",
image = "https://hatscripts.github.io/circle-flags/flags/gb.svg",
alpha3 = "GB",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "British Pound Sterling",
code = "GBP",
image = "https://hatscripts.github.io/circle-flags/flags/gb.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "CANADA",
code = "CA",
image = "https://hatscripts.github.io/circle-flags/flags/ca.svg",
alpha3 = "CA",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Canadian Dollar",
code = "CAD",
image = "https://hatscripts.github.io/circle-flags/flags/ca.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Hon Kong",
code = "HK",
image = "https://hatscripts.github.io/circle-flags/flags/hk.svg",
alpha3 = "HK",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Hon Kong Dollar",
code = "HKD",
image = "https://hatscripts.github.io/circle-flags/flags/hk.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Australia",
code = "AU",
image = "https://hatscripts.github.io/circle-flags/flags/au.svg",
alpha3 = "AU",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Australian Dollar",
code = "AUD",
image = "https://hatscripts.github.io/circle-flags/flags/au.svg",
precision = 2,
),
onrampAvailable = true,
),
)
}
}

View file

@ -14,8 +14,8 @@ data class OnrampStatusResponse(
@Json(name = "payoutAddress")
val payoutAddress: String,
// @Json(name = "status")
// val status: ???
@Json(name = "status")
val status: Status,
@Json(name = "failReason")
val failReason: String?,
@ -48,14 +48,46 @@ data class OnrampStatusResponse(
val toDecimals: String,
@Json(name = "toAmount")
val toAmount: String,
val toAmount: String?,
@Json(name = "toActualAmount")
val toActualAmount: String,
val toActualAmount: String?,
@Json(name = "paymentMethod")
val paymentMethod: String,
@Json(name = "countryCode")
val countryCode: String,
)
)
enum class Status {
@Json(name = "created")
Created,
@Json(name = "expired")
Expired,
@Json(name = "waiting-for-payment")
WaitingForPayment,
@Json(name = "payment-processing")
PaymentProcessing,
@Json(name = "verifying")
Verifying,
@Json(name = "failed")
Failed,
@Json(name = "paid")
Paid,
@Json(name = "sending")
Sending,
@Json(name = "finished")
Finished,
@Json(name = "paused")
Paused,
}

View file

@ -11,7 +11,6 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.onramp.MockedOnrampApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -99,24 +98,22 @@ internal object NetworkModule {
@Provides
@Singleton
fun provideOnrampApi(
// @NetworkMoshi moshi: Moshi,
// @ApplicationContext context: Context,
// apiConfigsManager: ApiConfigsManager,
// appLogsStore: AppLogsStore,
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): OnrampApi {
// TODO: Remove when backend will be ready - [REDACTED_TASK_KEY]
return MockedOnrampApi()
// return createApi(
// id = ApiConfig.ID.Express,
// moshi = moshi,
// context = context,
// apiConfigsManager = apiConfigsManager,
// clientBuilder = {
// addInterceptor(
// NetworkLogsSaveInterceptor(appLogsStore),
// )
// },
// )
return createApi(
id = ApiConfig.ID.Express,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
)
},
)
}
@Provides

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.onramp.DefaultOnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object OnrampStoreModule {
@Provides
@Singleton
fun provideOnrampPaymentMethodsStore(): OnrampPaymentMethodsStore {
return DefaultOnrampPaymentMethodsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultSwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
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 ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindSwapServiceLoader(defaultSwapServiceLoader: DefaultSwapServiceLoader): SwapServiceLoader
}

View file

@ -0,0 +1,91 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
/**
* Default implementation of [SwapServiceLoader]
*
* @property tangemExpressApi express api
* @property expressAssetsStore local storage
*
[REDACTED_AUTHOR]
*/
internal class DefaultSwapServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : SwapServiceLoader {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
withContext(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWalletId)
initializationStatus.update { lceLoading() }
try {
val tokensList = userTokens.tokens.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
body = AssetsRequestBody(tokensList = tokensList),
).getOrThrow()
expressAssetsStore.store(userWalletId, response)
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
initializationStatus.update { e.lceError() }
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
}
}
override fun getInitializationStatus(userWalletId: UserWalletId): InitializationStatusFlow {
return getInitializationStatusInternal(userWalletId)
}
private fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
val initializationStatus = initializationStatuses.value.get(key = userWalletId)
if (initializationStatus != null) return initializationStatus
val default: InitializationStatusFlow = MutableStateFlow(value = lceLoading())
initializationStatuses.update {
it.toMutableMap().apply {
put(key = userWalletId, value = default)
}
}
return default
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.StateFlow
/**
* Swap service loader
*
[REDACTED_AUTHOR]
*/
interface SwapServiceLoader {
/** Update service using [userWalletId] and [userTokens] */
suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): StateFlow<Lce<Throwable, List<Asset>>>
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.onramp
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultOnrampPaymentMethodsStore(
dataStore: StringKeyDataStore<List<PaymentMethodDTO>>,
) : OnrampPaymentMethodsStore, StringKeyDataStoreDecorator<String, List<PaymentMethodDTO>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.onramp
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
interface OnrampPaymentMethodsStore {
suspend fun getSyncOrNull(key: String): List<PaymentMethodDTO>?
suspend fun store(key: String, value: List<PaymentMethodDTO>)
suspend fun contains(key: String): Boolean
}

View file

@ -53,10 +53,6 @@ object PreferencesKeys {
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy {
booleanPreferencesKey(name = "isWalletTravalaPromoShown")
}
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
@ -103,6 +99,12 @@ object PreferencesKeys {
val SHOULD_SHOW_RING_PROMO_KEY by lazy { booleanPreferencesKey(name = "shouldShowRingPromo") }
val ONRAMP_DEFAULT_CURRENCY by lazy { stringPreferencesKey(name = "onrampDefaultCurrency") }
val ONRAMP_DEFAULT_COUNTRY by lazy { stringPreferencesKey(name = "onrampDefaultCountry") }
val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") }
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -150,4 +150,12 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Prefere
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Get flow of set of [T] by string [key], or empty if data is not found */
inline fun <reified T> AppPreferencesStore.getObjectSet(key: Preferences.Key<String>): Flow<Set<T>> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.map {
it[key]?.let(adapter::fromJson) ?: emptySet()
}
}