Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-29 12:37:11 +02:00
parent c849c203b1
commit 7ab5f7b547
23 changed files with 762 additions and 17 deletions

View file

@ -15,11 +15,11 @@ internal class DefaultExpressAuthProvider(
private var uuid = AtomicReference(UUID.randomUUID())
override fun getApiKey(): String {
return configManager.config.express?.apiKey ?: ""
return configManager.config.express?.apiKey ?: error("No express api key provided")
}
override fun getUserId(): String {
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: ""
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: error("No user id provided")
}
override fun getSessionId(): String {

View file

@ -0,0 +1,13 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.config.ConfigManager
import com.tangem.lib.auth.StakeKitAuthProvider
internal class DefaultStakeKitAuthProvider(
private val configManager: ConfigManager,
) : StakeKitAuthProvider {
override fun getApiKey(): String {
return configManager.config.express?.apiKey ?: error("No StakeKit api key provided")
}
}

View file

@ -5,9 +5,11 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.tap.network.auth.DefaultAppVersionProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
@ -37,6 +39,12 @@ class AuthModule {
)
}
@Provides
@Singleton
fun provideStakeKitAuthProvider(configManager: ConfigManager): StakeKitAuthProvider {
return DefaultStakeKitAuthProvider(configManager)
}
@Provides
@Singleton
fun provideAppVersionProvider(): AppVersionProvider {

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.api.stakekit
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.stakekit.models.request.MultipleYieldBalancesRequestBody
import com.tangem.datasource.api.stakekit.models.request.RevenueOption
import com.tangem.datasource.api.stakekit.models.request.YieldType
import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalances
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Query
@Suppress("LongParameterList")
interface StakeKitApi {
@GET("yields/enabled")
fun getAllYields(
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean,
@Query("type") type: YieldType,
@Query("revenueOption") revenueOption: RevenueOption,
@Query("page") page: Int,
@Query("network") network: String,
@Query("limit") limit: Int,
): ApiResponse<EnabledYieldsResponse>
@GET("yields/balances")
fun getMultipleYieldBalances(@Body body: List<MultipleYieldBalancesRequestBody>): ApiResponse<List<YieldBalances>>
@GET("tokens")
fun getTokens(): ApiResponse<List<TokenWithYield>>
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.api.stakekit
import com.squareup.moshi.*
import com.squareup.moshi.adapters.EnumJsonAdapter
/**
* Object to create a adapter for enum types with support for unknown enum values.
*/
object UnknownEnumMoshiAdapter {
fun <T : Enum<T>> create(enumType: Class<T>, defaultValue: T): JsonAdapter<T> =
EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue)
}

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
data class MultipleYieldBalancesRequestBody(
@Json(name = "addresses") val addresses: Address,
@Json(name = "args") val args: MultipleYieldBalancesRequestArgs,
@Json(name = "integrationId") val integrationId: String,
) {
data class Address(
@Json(name = "address") val address: String,
@Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null,
@Json(name = "explorerUrl") val explorerUrl: String,
) {
data class AdditionalAddresses(
@Json(name = "cosmosPubKey") val cosmosPubKey: String? = null,
@Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null,
@Json(name = "stakeAccounts") val stakeAccounts: List<String>? = null,
@Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List<String>? = null,
@Json(name = "tezosPubKey") val tezosPubKey: String? = null,
@Json(name = "cAddressBech") val cAddressBech: String? = null,
@Json(name = "pAddressBech") val pAddressBech: String? = null,
)
}
data class MultipleYieldBalancesRequestArgs(
@Json(name = "validatorAddresses") val validatorAddresses: List<String>,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.stakekit.models.request
enum class RevenueOption(val value: String) {
SUPPORTS_FEE("supportsFee"),
SUPPORTS_REV_SHARE("supportsRevShare"),
;
override fun toString(): String {
return value
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.stakekit.models.request
enum class YieldType(private val value: String) {
STAKING("staking"),
LIQUID_STAKING("liquid-staking"),
LENDING("lending"),
RESTAKING("restaking"),
VAULT("vault"),
;
override fun toString(): String {
return value
}
}

View file

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

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AddressArgument(
@Json(name = "required")
val required: Boolean,
@Json(name = "network")
val network: String? = null,
@Json(name = "minimum")
val minimum: Int? = null,
@Json(name = "maximum")
val maximum: Int? = null,
)

View file

@ -0,0 +1,50 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
enum class StakingActionType {
@Json(name = "STAKE")
STAKE,
@Json(name = "UNSTAKE")
UNSTAKE,
@Json(name = "CLAIM_REWARDS")
CLAIM_REWARDS,
@Json(name = "RESTAKE_REWARDS")
RESTAKE_REWARDS,
@Json(name = "WITHDRAW")
WITHDRAW,
@Json(name = "RESTAKE")
RESTAKE,
@Json(name = "CLAIM_UNSTAKED")
CLAIM_UNSTAKED,
@Json(name = "UNLOCK_LOCKED")
UNLOCK_LOCKED,
@Json(name = "STAKE_LOCKED")
STAKE_LOCKED,
@Json(name = "VOTE")
VOTE,
@Json(name = "REVOKE")
REVOKE,
@Json(name = "VOTE_LOCKED")
VOTE_LOCKED,
@Json(name = "REVOTE")
REVOTE,
@Json(name = "REBOND")
REBOND,
@Json(name = "MIGRATE")
MIGRATE,
}

View file

@ -0,0 +1,218 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "name") val name: String,
@Json(name = "network") val network: NetworkType,
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "address") val address: String?,
@Json(name = "coinGeckoId") val coinGeckoId: String?,
@Json(name = "logoURI") val logoURI: String?,
@Json(name = "isPoints") val isPoints: Boolean,
) {
enum class NetworkType {
@Json(name = "avalanche")
AVALANCHE,
@Json(name = "avalanche-atomic")
AVALANCHE_ATOMIC,
@Json(name = "avalanche-p")
AVALANCHE_P,
@Json(name = "arbitrum")
ARBITRUM,
@Json(name = "binance")
BINANCE,
@Json(name = "celo")
CELO,
@Json(name = "ethereum")
ETHEREUM,
@Json(name = "ethereum-goerli")
ETHEREUM_GOERLI,
@Json(name = "ethereum-holesky")
ETHEREUM_HOLESKY,
@Json(name = "fantom")
FANTOM,
@Json(name = "harmony")
HARMONY,
@Json(name = "optimism")
OPTIMISM,
@Json(name = "polygon")
POLYGON,
@Json(name = "gnosis")
GNOSIS,
@Json(name = "moonriver")
MOONRIVER,
@Json(name = "okc")
OKC,
@Json(name = "zksync")
ZKSYNC,
@Json(name = "viction")
VICTION,
@Json(name = "agoric")
AGORIC,
@Json(name = "akash")
AKASH,
@Json(name = "axelar")
AXELAR,
@Json(name = "band-protocol")
BAND_PROTOCOL,
@Json(name = "bitsong")
BITSONG,
@Json(name = "canto")
CANTO,
@Json(name = "chihuahua")
CHIHUAHUA,
@Json(name = "comdex")
COMDEX,
@Json(name = "coreum")
COREUM,
@Json(name = "cosmos")
COSMOS,
@Json(name = "crescent")
CRESCENT,
@Json(name = "cronos")
CRONOS,
@Json(name = "cudos")
CUDOS,
@Json(name = "desmos")
DESMOS,
@Json(name = "dydx")
DYDX,
@Json(name = "evmos")
EVMOS,
@Json(name = "fetch-ai")
FETCH_AI,
@Json(name = "gravity-bridge")
GRAVITY_BRIDGE,
@Json(name = "injective")
INJECTIVE,
@Json(name = "irisnet")
IRISNET,
@Json(name = "juno")
JUNO,
@Json(name = "kava")
KAVA,
@Json(name = "ki-network")
KI_NETWORK,
@Json(name = "mars-protocol")
MARS_PROTOCOL,
@Json(name = "nym")
NYM,
@Json(name = "okex-chain")
OKEX_CHAIN,
@Json(name = "onomy")
ONOMY,
@Json(name = "osmosis")
OSMOSIS,
@Json(name = "persistence")
PERSISTENCE,
@Json(name = "quicksilver")
QUICKSILVER,
@Json(name = "regen")
REGEN,
@Json(name = "secret")
SECRET,
@Json(name = "sentinel")
SENTINEL,
@Json(name = "sommelier")
SOMMELIER,
@Json(name = "stafi")
STAFI,
@Json(name = "stargaze")
STARGAZE,
@Json(name = "stride")
STRIDE,
@Json(name = "teritori")
TERITORI,
@Json(name = "tgrade")
TGRADE,
@Json(name = "umee")
UMEE,
@Json(name = "polkadot")
POLKADOT,
@Json(name = "kusama")
KUSAMA,
@Json(name = "westend")
WESTEND,
@Json(name = "binancebeacon")
BINANCEBEACON,
@Json(name = "near")
NEAR,
@Json(name = "solana")
SOLANA,
@Json(name = "tezos")
TEZOS,
@Json(name = "tron")
TRON,
UNKNOWN,
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TokenWithYield(
@Json(name = "token") val token: Token,
@Json(name = "availableYields") val availableYieldIds: List<String>,
)

View file

@ -0,0 +1,145 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Yield(
@Json(name = "id")
val id: String,
@Json(name = "token")
val token: Token,
@Json(name = "tokens")
val tokens: List<Token>,
@Json(name = "args")
val args: Args,
@Json(name = "status")
val status: Status,
@Json(name = "apy")
val apy: Double,
@Json(name = "rewardRate")
val rewardRate: Double,
@Json(name = "rewardType")
val rewardType: RewardType,
@Json(name = "metadata")
val metadata: Metadata,
@Json(name = "validators")
val validators: List<Validator>,
@Json(name = "isAvailable")
val isAvailable: Boolean,
) {
@JsonClass(generateAdapter = true)
data class Status(
@Json(name = "enter")
val enter: Boolean,
@Json(name = "exit")
val exit: Boolean?,
)
@JsonClass(generateAdapter = true)
data class Args(
@Json(name = "enter")
val enter: Enter,
@Json(name = "exit")
val exit: Enter?,
) {
@JsonClass(generateAdapter = true)
data class Enter(
@Json(name = "addresses")
val addresses: Addresses,
@Json(name = "args")
val args: Map<String, AddressArgument>,
) {
@JsonClass(generateAdapter = true)
data class Addresses(
@Json(name = "address")
val address: AddressArgument,
@Json(name = "additionalAddresses")
val additionalAddresses: Map<String, AddressArgument>? = null,
)
}
}
@JsonClass(generateAdapter = true)
data class Validator(
@Json(name = "address")
val address: String,
@Json(name = "status")
val status: String,
@Json(name = "name")
val name: String,
@Json(name = "image")
val image: String?,
@Json(name = "website")
val website: String?,
@Json(name = "apr")
val apr: Double?,
@Json(name = "commission")
val commission: Double?,
@Json(name = "stakedBalance")
val stakedBalance: String?,
@Json(name = "votingPower")
val votingPower: Double?,
@Json(name = "preferred")
val preferred: Boolean,
)
@JsonClass(generateAdapter = true)
data class Metadata(
@Json(name = "name")
val name: String,
@Json(name = "logoURI")
val logoUri: String,
@Json(name = "description")
val description: String,
@Json(name = "documentation")
val documentation: String?,
@Json(name = "gasFeeToken")
val gasFeeToken: Token,
@Json(name = "token")
val token: Token,
@Json(name = "tokens")
val tokens: List<Token>,
@Json(name = "type")
val type: String,
@Json(name = "rewardSchedule")
val rewardSchedule: String,
@Json(name = "cooldownPeriod")
val cooldownPeriod: Period,
@Json(name = "warmupPeriod")
val warmupPeriod: Period,
@Json(name = "rewardClaiming")
val rewardClaiming: String,
@Json(name = "defaultValidator")
val defaultValidator: String?,
@Json(name = "minimumStake")
val minimumStake: Int,
@Json(name = "supportsMultipleValidators")
val supportsMultipleValidators: Boolean,
@Json(name = "revshare")
val revshare: Enabled,
@Json(name = "fee")
val fee: Enabled,
) {
@JsonClass(generateAdapter = true)
data class Period(
@Json(name = "days")
val days: Int,
)
@JsonClass(generateAdapter = true)
data class Enabled(
@Json(name = "enabled")
val enabled: Boolean,
)
}
enum class RewardType {
@Json(name = "apy")
APY, // auto
@Json(name = "apr")
APR, // manual
}
}

View file

@ -0,0 +1,138 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldBalances(
@Json(name = "balances")
val balances: List<Balance>,
@Json(name = "integrationId")
val integrationId: String,
) {
@JsonClass(generateAdapter = true)
data class Balance(
@Json(name = "groupId")
val groupId: String,
@Json(name = "type")
val type: BalanceType,
@Json(name = "amount")
val amount: BigDecimal,
@Json(name = "date")
val date: DateTime?,
@Json(name = "pricePerShare")
val pricePerShare: BigDecimal,
@Json(name = "pendingActions")
val pendingActions: List<PendingAction>,
@Json(name = "token")
val token: Token,
@Json(name = "validatorAddress")
val validatorAddress: String?,
@Json(name = "validatorAddresses")
val validatorAddresses: List<String>?,
@Json(name = "providerId")
val providerId: String?,
) {
enum class BalanceType {
@Json(name = "available")
AVAILABLE,
@Json(name = "staked")
STAKED,
@Json(name = "unstaking")
UNSTAKING,
@Json(name = "unstaked")
UNSTAKED,
@Json(name = "preparing")
PREPARING,
@Json(name = "rewards")
REWARDS,
@Json(name = "locked")
LOCKED,
@Json(name = "unlocking")
UNLOCKING,
}
@JsonClass(generateAdapter = true)
data class PendingAction(
@Json(name = "type")
val type: StakingActionType,
@Json(name = "passthrough")
val passthrough: String,
@Json(name = "args")
val args: PendingActionArgs?,
) {
@JsonClass(generateAdapter = true)
data class PendingActionArgs(
@Json(name = "amount")
val amount: Amount?,
@Json(name = "duration")
val duration: Duration?,
@Json(name = "validatorAddress")
val validatorAddress: Required?,
@Json(name = "validatorAddresses")
val validatorAddresses: Required?,
@Json(name = "nfts")
val nfts: List<Nft>?,
@Json(name = "tronResource")
val tronResource: TronResource?,
@Json(name = "signatureVerification")
val signatureVerification: Required?,
) {
@JsonClass(generateAdapter = true)
data class Amount(
@Json(name = "required")
val required: Boolean,
@Json(name = "minimum")
val minimum: BigDecimal?,
@Json(name = "maximum")
val maximum: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class Duration(
@Json(name = "required")
val required: Boolean,
@Json(name = "minimum")
val minimum: Int?,
@Json(name = "maximum")
val maximum: Int?,
)
@JsonClass(generateAdapter = true)
data class Nft(
@Json(name = "baycId")
val baycId: Required?,
@Json(name = "maycId")
val maycId: Required?,
@Json(name = "bakcId")
val bakcId: Required?,
)
@JsonClass(generateAdapter = true)
data class TronResource(
@Json(name = "required")
val required: Boolean,
@Json(name = "options")
val options: List<String>,
)
}
}
@JsonClass(generateAdapter = true)
data class Required(
@Json(name = "required")
val required: Boolean,
)
}
}

View file

@ -106,6 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
express = if (BuildConfig.ENVIRONMENT == "dev") configValues.devExpress else configValues.express,
stakeKitApiKey = configValues.stakeKitApiKey,
)
}

View file

@ -17,4 +17,5 @@ data class Config(
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
val express: ExpressModel? = null,
val stakeKitApiKey: String? = null,
)

View file

@ -42,6 +42,7 @@ class ConfigValueModel(
val devExpress: ExpressModel?,
val express: ExpressModel?,
val polygonScanApiKey: String?,
val stakeKitApiKey: String?,
)
@JsonClass(generateAdapter = true)

View file

@ -7,6 +7,8 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.DateTimeAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import com.tangem.datasource.api.stakekit.UnknownEnumMoshiAdapter
import com.tangem.datasource.api.stakekit.models.response.model.Token
import com.tangem.datasource.config.models.ProviderModel
import dagger.Module
import dagger.Provides
@ -33,6 +35,10 @@ class MoshiModule {
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(KotlinJsonAdapterFactory())
.add(
Token.NetworkType::class.java,
UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN),
)
.build()
}

View file

@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
import com.tangem.datasource.api.tangemTech.TangemTechServiceApi
@ -13,6 +14,7 @@ import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -56,6 +58,27 @@ class NetworkModule {
.create(TangemExpressApi::class.java)
}
@Provides
@Singleton
fun provideStakeKitApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
stakeKitAuthProvider: StakeKitAuthProvider,
): StakeKitApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(STAKEKIT_BASE_URL)
.client(
OkHttpClient.Builder()
.addHeaders(StakeKit(stakeKitAuthProvider))
.addLoggers(context)
.build(),
)
.build()
.create(StakeKitApi::class.java)
}
@Provides
@Singleton
fun provideTangemTechApi(
@ -137,6 +160,7 @@ class NetworkModule {
}
private companion object {
const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/"
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
@ -146,8 +170,5 @@ class NetworkModule {
const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L
const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
const val API_ONE_INCH_TIMEOUT_MS = 5000L
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.utils
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
/**
* Presentation of request header
@ -31,4 +32,8 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
"version" to { appVersionProvider.getAppVersion() },
"platform" to { "android" },
)
class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader(
"X-API-KEY" to { stakeKitAuthProvider.getApiKey() },
)
}

View file

@ -1,12 +0,0 @@
package com.tangem.lib.auth
/**
* Provides auth for tangemTech API
*/
interface AuthBearerProvider {
/**
* Returns api-key
*/
fun getApiKey(): String
}

View file

@ -0,0 +1,6 @@
package com.tangem.lib.auth
interface StakeKitAuthProvider {
fun getApiKey(): String
}