Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-29 14:31:53 +03:00
commit 652159dbce
316 changed files with 6242 additions and 3084 deletions

View file

@ -0,0 +1,178 @@
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

@ -3,7 +3,8 @@ package com.tangem.datasource.api.stakekit
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.stakekit.models.request.*
import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse
import com.tangem.datasource.api.stakekit.models.response.ActionDTO
import com.tangem.datasource.api.stakekit.models.response.GetActionsResponse
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
@ -35,14 +36,23 @@ interface StakeKitApi {
@Body body: YieldBalanceRequestBody,
): ApiResponse<List<BalanceDTO>>
@GET("actions")
suspend fun getActions(
@Query("walletAddress") walletAddress: String,
@Query("network") network: String,
@Query("status") status: String,
@Query("sort") sort: String = "createdAtDesc",
@Query("limit") limit: Int = 50,
): ApiResponse<GetActionsResponse>
@POST("actions/enter")
suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse<EnterActionResponse>
suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse<ActionDTO>
@POST("actions/exit")
suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse<EnterActionResponse>
suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse<ActionDTO>
@POST("actions/pending")
suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse<EnterActionResponse>
suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse<ActionDTO>
@POST("actions/enter/estimate-gas")
suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse<StakingGasEstimateDTO>

View file

@ -9,7 +9,7 @@ import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class EnterActionResponse(
data class ActionDTO(
@Json(name = "id")
val id: String,
@Json(name = "integrationId")

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.stakekit.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetActionsResponse(
@Json(name = "data")
val data: List<ActionDTO>,
@Json(name = "hasNextPage")
val hasNextPage: Boolean,
@Json(name = "limit")
val limit: Int,
@Json(name = "page")
val page: Int,
)

View file

@ -9,31 +9,31 @@ data class YieldDTO(
@Json(name = "id")
val id: String,
@Json(name = "token")
val token: TokenDTO,
val token: TokenDTO?,
@Json(name = "tokens")
val tokens: List<TokenDTO>,
val tokens: List<TokenDTO>?,
@Json(name = "args")
val args: ArgsDTO,
val args: ArgsDTO?,
@Json(name = "status")
val status: StatusDTO,
val status: StatusDTO?,
@Json(name = "apy")
val apy: BigDecimal,
val apy: BigDecimal?,
@Json(name = "rewardRate")
val rewardRate: Double,
val rewardRate: Double?,
@Json(name = "rewardType")
val rewardType: RewardTypeDTO,
val rewardType: RewardTypeDTO?,
@Json(name = "metadata")
val metadata: MetadataDTO,
val metadata: MetadataDTO?,
@Json(name = "validators")
val validators: List<ValidatorDTO>,
val validators: List<ValidatorDTO>?,
@Json(name = "isAvailable")
val isAvailable: Boolean,
val isAvailable: Boolean?,
) {
@JsonClass(generateAdapter = true)
data class StatusDTO(
@Json(name = "enter")
val enter: Boolean,
val enter: Boolean?,
@Json(name = "exit")
val exit: Boolean?,
)
@ -41,21 +41,21 @@ data class YieldDTO(
@JsonClass(generateAdapter = true)
data class ArgsDTO(
@Json(name = "enter")
val enter: Enter,
val enter: Enter?,
@Json(name = "exit")
val exit: Enter?,
) {
@JsonClass(generateAdapter = true)
data class Enter(
@Json(name = "addresses")
val addresses: Addresses,
val addresses: Addresses?,
@Json(name = "args")
val args: Map<String, AddressArgumentDTO>,
val args: Map<String, AddressArgumentDTO>?,
) {
@JsonClass(generateAdapter = true)
data class Addresses(
@Json(name = "address")
val address: AddressArgumentDTO,
val address: AddressArgumentDTO?,
@Json(name = "additionalAddresses")
val additionalAddresses: Map<String, AddressArgumentDTO>? = null,
)
@ -65,7 +65,7 @@ data class YieldDTO(
@JsonClass(generateAdapter = true)
data class ValidatorDTO(
@Json(name = "address")
val address: String,
val address: String?,
@Json(name = "status")
val status: ValidatorStatusDTO,
@Json(name = "name")
@ -106,51 +106,51 @@ data class YieldDTO(
@JsonClass(generateAdapter = true)
data class MetadataDTO(
@Json(name = "name")
val name: String,
val name: String?,
@Json(name = "logoURI")
val logoUri: String,
val logoUri: String?,
@Json(name = "description")
val description: String,
val description: String?,
@Json(name = "documentation")
val documentation: String?,
@Json(name = "gasFeeToken")
val gasFeeTokenDTO: TokenDTO,
val gasFeeTokenDTO: TokenDTO?,
@Json(name = "token")
val tokenDTO: TokenDTO,
val tokenDTO: TokenDTO?,
@Json(name = "tokens")
val tokensDTO: List<TokenDTO>,
val tokensDTO: List<TokenDTO>?,
@Json(name = "type")
val type: String,
val type: String?,
@Json(name = "rewardSchedule")
val rewardSchedule: RewardScheduleDTO,
val rewardSchedule: RewardScheduleDTO?,
@Json(name = "cooldownPeriod")
val cooldownPeriod: PeriodDTO?,
@Json(name = "warmupPeriod")
val warmupPeriod: PeriodDTO,
val warmupPeriod: PeriodDTO?,
@Json(name = "rewardClaiming")
val rewardClaiming: RewardClaimingDTO,
val rewardClaiming: RewardClaimingDTO?,
@Json(name = "defaultValidator")
val defaultValidator: String?,
@Json(name = "minimumStake")
val minimumStake: Int?,
@Json(name = "supportsMultipleValidators")
val supportsMultipleValidators: Boolean,
val supportsMultipleValidators: Boolean?,
@Json(name = "revshare")
val revshare: EnabledDTO,
val revshare: EnabledDTO?,
@Json(name = "fee")
val fee: EnabledDTO,
val fee: EnabledDTO?,
) {
@JsonClass(generateAdapter = true)
data class PeriodDTO(
@Json(name = "days")
val days: Int,
val days: Int?,
)
@JsonClass(generateAdapter = true)
data class EnabledDTO(
@Json(name = "enabled")
val enabled: Boolean,
val enabled: Boolean?,
)
enum class RewardScheduleDTO {

View file

@ -11,6 +11,7 @@ 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
@ -98,22 +99,24 @@ 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 {
return createApi(
id = ApiConfig.ID.Express,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
)
},
)
// 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),
// )
// },
// )
}
@Provides

View file

@ -1,8 +1,9 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.*
import com.tangem.datasource.local.token.DefaultStakingBalanceStore
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -11,11 +12,23 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object StakingBalanceStoreModule {
internal object StakingStoreModule {
@Provides
@Singleton
fun provideStakingTokensStore(): StakingYieldsStore {
return DefaultStakingYieldsStore()
}
@Provides
@Singleton
fun provideStakingBalanceStore(): StakingBalanceStore {
return DefaultStakingBalanceStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideStakingActionsStore(): StakingActionsStore {
return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
import com.tangem.datasource.local.token.StakingYieldsStore
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 StakingTokensStoreModule {
@Provides
@Singleton
fun provideStakingTokensStore(): StakingYieldsStore {
return DefaultStakingYieldsStore()
}
}

View file

@ -4,6 +4,8 @@ import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
import com.tangem.datasource.local.config.providers.DefaultBlockchainProvidersStorage
import com.tangem.datasource.local.config.testnet.DefaultTestnetTokensStorage
@ -30,7 +32,7 @@ internal object ConfigModule {
@Provides
@Singleton
fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
fun provideTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetLoader)
}
@ -42,4 +44,13 @@ internal object ConfigModule {
runtimeStateStore = RuntimeStateStore(defaultValue = emptyMap()),
)
}
@Provides
@Singleton
fun provideIssuersConfigStorage(assetLoader: AssetLoader): IssuersConfigStorage {
return DefaultIssuersConfigStorage(
assetLoader = assetLoader,
runtimeStateStore = RuntimeStateStore(defaultValue = emptyList()),
)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.datasource.local.config.issuers
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.config.issuers.models.Issuer
import com.tangem.datasource.local.datastore.RuntimeStateStore
internal class DefaultIssuersConfigStorage(
private val assetLoader: AssetLoader,
private val runtimeStateStore: RuntimeStateStore<List<Issuer>>,
) : IssuersConfigStorage {
override suspend fun getConfig(): List<Issuer> {
val cachedData = runtimeStateStore.get().value
if (cachedData.isNotEmpty()) return cachedData
val issuers = assetLoader.loadList<Issuer>(fileName = ISSUERS_FILE_NAME)
runtimeStateStore.store(value = issuers)
return issuers
}
private companion object {
const val ISSUERS_FILE_NAME = "tangem-app-config/issuers"
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.local.config.issuers
import com.tangem.datasource.local.config.issuers.models.Issuer
/**
* Storage for list of Twins [Issuer]
*
[REDACTED_AUTHOR]
*/
interface IssuersConfigStorage {
/** Get config */
suspend fun getConfig(): List<Issuer>
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.config.issuers.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Issuer(
@Json(name = "privateKey") val privateKey: String,
@Json(name = "publicKey") val publicKey: String,
)

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.extensions.replaceBy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -27,4 +28,23 @@ internal class DefaultNetworksStatusesStore(
store(key, newValues)
}
}
override suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>) {
mutex.withLock {
val currentValues = getSyncOrNull(key) ?: emptySet()
val updatedValues = currentValues.toMutableSet()
values.forEach { newValue ->
val isReplaced = updatedValues.replaceBy(newValue) {
it.network == newValue.network
}
if (!isReplaced) {
updatedValues.add(newValue)
}
}
store(key, updatedValues)
}
}
}

View file

@ -11,4 +11,6 @@ interface NetworksStatusesStore {
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
suspend fun store(key: UserWalletId, value: NetworkStatus)
suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>)
}

View file

@ -0,0 +1,34 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal class DefaultStakingActionsStore(
private val dataStore: StringKeyDataStore<List<StakingAction>>,
) : StakingActionsStore {
private val mutex = Mutex()
override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<StakingAction>> {
return dataStore.get(composeKey(userWalletId, cryptoCurrencyId))
}
override suspend fun store(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
items: List<StakingAction>,
) {
mutex.withLock {
dataStore.store(composeKey(userWalletId, cryptoCurrencyId), items)
}
}
private fun composeKey(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): String {
return userWalletId.stringValue + cryptoCurrencyId.value
}
}

View file

@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
internal class DefaultStakingYieldsStore : StakingYieldsStore {
private var yields = mutableListOf<YieldDTO>()
private var yields = listOf<YieldDTO>()
override fun get(): List<YieldDTO> {
return yields
}
override fun store(items: List<YieldDTO>) {
yields = items.toMutableList()
yields = items
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.token
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface StakingActionsStore {
fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<StakingAction>>
suspend fun store(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, items: List<StakingAction>)
}