Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-10 18:41:26 +01:00
commit 3fba2ae059
872 changed files with 17188 additions and 8108 deletions

View file

@ -3,46 +3,46 @@ package com.tangem.core.analytics.models
sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom Token")
object BlockchainError : CardBalanceState("Blockchain Error")
object NoRate : CardBalanceState("No Rate")
data object Empty : CardBalanceState("Empty")
data object Full : CardBalanceState("Full")
data object CustomToken : CardBalanceState("Custom Token")
data object BlockchainError : CardBalanceState("Blockchain Error")
data object NoRate : CardBalanceState("No Rate")
companion object
}
sealed class TokenBalanceState(val value: String) {
object Empty : TokenBalanceState("Empty")
object Full : TokenBalanceState("Full")
data object Empty : TokenBalanceState("Empty")
data object Full : TokenBalanceState("Full")
}
sealed class RateApp(val value: String) {
object Liked : RateApp("Liked")
object Disliked : RateApp("Disliked")
object Closed : RateApp("Close")
data object Liked : RateApp("Liked")
data object Disliked : RateApp("Disliked")
data object Closed : RateApp("Close")
}
sealed class OnOffState(val value: String) {
object On : OnOffState("On")
object Off : OnOffState("Off")
data object On : OnOffState("On")
data object Off : OnOffState("Off")
}
sealed class OrganizeSortType(val value: String) {
object ByBalance : OrganizeSortType("By Balance")
object Manually : OrganizeSortType("Manually")
data object ByBalance : OrganizeSortType("By Balance")
data object Manually : OrganizeSortType("Manually")
}
sealed class UserCode(val value: String) {
object AccessCode : UserCode("Access Code")
object Passcode : UserCode("Passcode")
data object AccessCode : UserCode("Access Code")
data object Passcode : UserCode("Passcode")
}
sealed class AccessCodeRecoveryStatus(val value: String) {
val key: String = "Status"
object Enabled : AccessCodeRecoveryStatus("Enabled")
object Disabled : AccessCodeRecoveryStatus("Disabled")
data object Enabled : AccessCodeRecoveryStatus("Enabled")
data object Disabled : AccessCodeRecoveryStatus("Disabled")
companion object {
fun from(enabled: Boolean): AccessCodeRecoveryStatus {
@ -52,9 +52,9 @@ sealed class AnalyticsParam {
}
sealed class Error(val value: String) {
object App : Error("App Error")
object CardSdk : Error("Card Sdk Error")
object BlockchainSdk : Error("Blockchain Sdk Error")
data object App : Error("App Error")
data object CardSdk : Error("Card Sdk Error")
data object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class ScreensSources(val value: String) {
@ -86,8 +86,8 @@ sealed class AnalyticsParam {
val permissionType: String,
) : TxSentFrom("Approve"), TxData
object WalletConnect : TxSentFrom("WalletConnect")
object Sell : TxSentFrom("Sell")
data object WalletConnect : TxSentFrom("WalletConnect")
data object Sell : TxSentFrom("Sell")
}
sealed interface TxData {
@ -117,13 +117,13 @@ sealed class AnalyticsParam {
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType("Private key")
object NewSeed : WalletCreationType("New seed")
object SeedImport : WalletCreationType("Seed import")
data object PrivateKey : WalletCreationType("Private key")
data object NewSeed : WalletCreationType("New seed")
data object SeedImport : WalletCreationType("Seed import")
}
sealed class WalletType(val value: String) {
object MultiCurrency : WalletType(value = "Multicurrency")
data object MultiCurrency : WalletType(value = "Multicurrency")
class SingleCurrency(currencyName: String) : WalletType(currencyName)
}

View file

@ -3,11 +3,16 @@ plugins {
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
alias(deps.plugins.room)
id("configuration")
}
android {
namespace = "com.tangem.datasource"
room {
schemaDirectory("$projectDir/schemas")
}
}
dependencies {
@ -39,6 +44,7 @@ dependencies {
/** Network */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.moshi.adapters)
implementation(deps.okHttp)
implementation(deps.okHttp.prettyLogging)
implementation(deps.retrofit)
@ -53,10 +59,19 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)
/** Local storages */
implementation(deps.androidx.datastore)
implementation(deps.room.runtime)
implementation(deps.room.ktx)
kapt(deps.room.compiler)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -0,0 +1,304 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "a8a710af25033ee27e5043d001385234",
"entities": [
{
"tableName": "UserWalletEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `artworkUrl` TEXT NOT NULL, `isMultiCurrency` INTEGER NOT NULL, `hasBackupError` INTEGER NOT NULL, `cardsInWallet` TEXT NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artworkUrl",
"columnName": "artworkUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isMultiCurrency",
"columnName": "isMultiCurrency",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasBackupError",
"columnName": "hasBackupError",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cardsInWallet",
"columnName": "cardsInWallet",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinalNumber",
"columnName": "ordinalNumber",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_UserWalletEntity_id",
"unique": true,
"columnNames": [
"id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name": "index_UserWalletEntity_ordinalNumber",
"unique": true,
"columnNames": [
"ordinalNumber"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_ordinalNumber` ON `${TABLE_NAME}` (`ordinalNumber`)"
}
],
"foreignKeys": []
},
{
"tableName": "CryptoCurrenciesAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `title` TEXT NOT NULL, `currenciesCount` INTEGER NOT NULL, `isArchived` INTEGER NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userWalletId",
"columnName": "userWalletId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currenciesCount",
"columnName": "currenciesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ordinalNumber",
"columnName": "ordinalNumber",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_CryptoCurrenciesAccountEntity_id",
"unique": false,
"columnNames": [
"id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name": "index_CryptoCurrenciesAccountEntity_userWalletId",
"unique": false,
"columnNames": [
"userWalletId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)"
}
],
"foreignKeys": [
{
"table": "UserWalletEntity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"userWalletId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "CryptoCurrencyEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `currencyBackendId` TEXT, `networkId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `contractAddress` TEXT, `derivationPath` TEXT, FOREIGN KEY(`accountId`) REFERENCES `CryptoCurrenciesAccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currencyBackendId",
"columnName": "currencyBackendId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "networkId",
"columnName": "networkId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userWalletId",
"columnName": "userWalletId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "symbol",
"columnName": "symbol",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "decimals",
"columnName": "decimals",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contractAddress",
"columnName": "contractAddress",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "derivationPath",
"columnName": "derivationPath",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_CryptoCurrencyEntity_currencyBackendId",
"unique": false,
"columnNames": [
"currencyBackendId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_currencyBackendId` ON `${TABLE_NAME}` (`currencyBackendId`)"
},
{
"name": "index_CryptoCurrencyEntity_networkId",
"unique": false,
"columnNames": [
"networkId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_networkId` ON `${TABLE_NAME}` (`networkId`)"
},
{
"name": "index_CryptoCurrencyEntity_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_accountId` ON `${TABLE_NAME}` (`accountId`)"
},
{
"name": "index_CryptoCurrencyEntity_userWalletId",
"unique": false,
"columnNames": [
"userWalletId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)"
}
],
"foreignKeys": [
{
"table": "CryptoCurrenciesAccountEntity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
},
{
"table": "UserWalletEntity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"userWalletId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a8a710af25033ee27e5043d001385234')"
]
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.common
/**
* Provides auth for tangemTech API
*/
interface AuthProvider {
/**
* Returns authToken for tangem tech api
*/
fun getCardPublicKey(): String
fun getCardId(): String
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
import retrofit2.converter.moshi.MoshiConverterFactory
/**

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.common
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.common
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.*
import org.joda.time.DateTime

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.common
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.*
import org.joda.time.LocalDate

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.api.common.adapter
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

@ -2,6 +2,7 @@ package com.tangem.datasource.api.express
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.*
import retrofit2.http.Body
@ -42,6 +43,7 @@ interface TangemExpressApi {
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@Query("fromAddress") fromAddress: String,
@Query("toNetwork") toNetwork: String,
@Query("fromAmount") fromAmount: String,
@Query("fromDecimals") fromDecimals: Int,
@ -56,4 +58,7 @@ interface TangemExpressApi {
@GET("exchange-status")
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
@POST("exchange-sent")
suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse<ExchangeSentResponseBody>
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.api.express.models.request
import com.squareup.moshi.Json
data class ExchangeSentRequestBody(
@Json(name = "txId")
val txId: String,
@Json(name = "fromNetwork")
val fromNetwork: String,
@Json(name = "fromAddress")
val fromAddress: String,
@Json(name = "payinAddress")
val payinAddress: String,
@Json(name = "payinExtraId")
val payinExtraId: String?,
@Json(name = "txHash")
val txHash: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
data class ExchangeSentResponseBody(
@Json(name = "txId")
val txId: String,
@Json(name = "status")
val status: String,
)

View file

@ -10,8 +10,8 @@ data class ExchangeStatusResponse(
@Json(name = "externalTxId")
val externalTxId: String,
@Json(name = "externalTxStatus")
val externalStatus: ExchangeStatus,
@Json(name = "status")
val status: ExchangeStatus,
@Json(name = "externalTxUrl")
val externalTxUrl: String,

View file

@ -0,0 +1,48 @@
package com.tangem.datasource.api.stakekit
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
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.Yield
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
@Suppress("LongParameterList")
interface StakeKitApi {
@GET("yields/enabled")
suspend fun getMultipleYields(
@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/{integrationId}")
suspend fun getSingleYield(
@Path("integrationId") integrationId: String,
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false,
): ApiResponse<Yield>
@GET("yields/balances")
suspend fun getMultipleYieldBalances(
@Body body: List<YieldBalanceRequestBody>,
): ApiResponse<List<YieldBalanceWrapper>>
@GET("yields/{integrationId}/balances")
suspend fun getSingleYieldBalance(
@Path("integrationId") integrationId: String,
@Body body: YieldBalanceRequestBody,
): ApiResponse<YieldBalanceWrapper>
@GET("tokens")
suspend fun getTokens(): ApiResponse<List<TokenWithYield>>
}

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,31 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
data class YieldBalanceRequestBody(
@Json(name = "addresses") val addresses: Address,
@Json(name = "args") val args: YieldBalanceRequestArgs,
@Json(name = "integrationId") val integrationId: String? = null,
) {
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 YieldBalanceRequestArgs(
@Json(name = "validatorAddresses") val validatorAddresses: List<String>,
)
}

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-c")
AVALANCHE_C,
@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,146 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@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: BigDecimal,
@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 YieldBalanceWrapper(
@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

@ -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

@ -10,6 +10,7 @@ import retrofit2.http.*
*
[REDACTED_AUTHOR]
*/
@Suppress("TooManyFunctions")
interface TangemTechApi {
@GET("coins")
@ -76,6 +77,21 @@ interface TangemTechApi {
@GET("promotion")
suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse<PromotionInfoResponse>
@GET("settings/{wallet_id}")
suspend fun getUserTokensSettings(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("wallet_id") walletId: String,
): ApiResponse<UserTokensSettingsResponse>
@PUT("settings/{wallet_id}")
suspend fun saveUserTokensSettings(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("wallet_id") walletId: String,
@Body userTokensSettings: UserTokensSettingsResponse,
): ApiResponse<Unit>
@POST("user-network-account")
suspend fun createUserNetworkAccount(
@Header("card_public_key") cardPublicKey: String,
@ -83,6 +99,35 @@ interface TangemTechApi {
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
@POST("account")
suspend fun createUserTokensAccount(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Body body: CreateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("account/{account_id}")
suspend fun updateUserTokensAccount(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("account_id") accountId: Int,
@Body body: UpdateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("account/{account_id}/archive")
suspend fun archiveUserTokensAccount(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("account_id") accountId: Int,
): ApiResponse<UserTokensAccountResponse>
@PUT("account/{account_id}/unarchive")
suspend fun restoreUserTokensAccount(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("account_id") accountId: Int,
): ApiResponse<UserTokensAccountResponse>
@GET("features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.v2.UserTokensResponseV2
import retrofit2.http.*
interface TangemTechApiV2 {
@GET("user-tokens/{wallet_id}")
suspend fun getUserTokens(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("wallet_id") walletId: String,
): ApiResponse<UserTokensResponseV2>
@PUT("user-tokens/{wallet_id}")
suspend fun saveUserTokens(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Path("wallet_id") walletId: String,
@Body userTokens: UserTokensResponseV2,
): ApiResponse<Unit>
}

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.config.models.ProviderModel
import retrofit2.http.GET
import retrofit2.http.Header
/**
* Tangem Tech API for app services
*
[REDACTED_AUTHOR]
*/
interface TangemTechServiceApi {
@GET("networks/providers")
suspend fun getBlockchainProviders(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
): Map<String, List<ProviderModel>>
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
data class CreateUserTokensAccountBody(
@Json(name = "accountId")
val id: Int,
@Json(name = "accountTitle")
val title: String,
)

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
data class UpdateUserTokensAccountBody(
@Json(name = "accountTitle")
val title: String,
)

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
data class UserTokensAccountResponse(
@Json(name = "accountId")
val id: Int,
@Json(name = "accountTitle")
val title: String,
@Json(name = "archived")
val isArchived: Boolean,
@Json(name = "tokensCount")
val tokensCount: Int? = null,
@Json(name = "tokens")
val tokens: List<UserTokensResponse.Token>? = null,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
data class UserTokensSettingsResponse(
@Json(name = "group")
val group: UserTokensResponse.GroupType,
@Json(name = "sort")
val sort: UserTokensResponse.SortType,
)

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.tangemTech.models.v2
import com.squareup.moshi.Json
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
data class UserTokensResponseV2(
@Json(name = "accounts")
val accounts: List<TokensAccount>,
) {
data class TokensAccount(
@Json(name = "id")
val id: Int,
@Json(name = "title")
val title: String,
@Json(name = "tokens")
val tokens: List<UserTokensResponse.Token>? = null,
@Json(name = "tokensCount")
val tokensCount: Int? = null,
@Json(name = "archived")
val isArchived: Boolean,
)
}

View file

@ -1,28 +0,0 @@
package com.tangem.datasource.asset
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
import java.io.InputStream
import javax.inject.Inject
/**
* Implementation of asset file reader
*
* @property context application context
*/
internal class AndroidAssetReader @Inject constructor(
@ApplicationContext private val context: Context,
) : AssetReader {
override fun readJson(fileName: String): String {
return openFile("$fileName.json")
.bufferedReader()
.use(BufferedReader::readText)
}
override fun openFile(fileName: String): InputStream {
return context.assets
.open(fileName)
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.datasource.asset
import java.io.InputStream
/**
* Asset file reader
*
[REDACTED_AUTHOR]
*/
interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
/** Open a file [file] from asset as InputStream */
fun openFile(file: String): InputStream
}

View file

@ -0,0 +1,91 @@
package com.tangem.datasource.asset.loader
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.NetworkMoshi
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Asset file loader
*
* @property assetReader asset reader
* @property moshi moshi
*
* @see <a href = "https://www.notion.so/tangem/Assets-e045dd890413413faf34ce07ae47ff56">Documentation</a>
*
[REDACTED_AUTHOR]
*/
@Singleton
class AssetLoader @Inject constructor(
val assetReader: AssetReader,
@NetworkMoshi val moshi: Moshi,
) {
/** Load content [Content] of asset file [fileName] */
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun <reified Content> load(fileName: String): Content? {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
moshi.adapter<Content>().fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
null
},
)
}
/** Load list [V] values of asset file [fileName] */
suspend inline fun <reified V> loadList(fileName: String): List<V> {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyList()
},
)
}
/** Load map [String] keys and [V] values of asset file [fileName] */
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyMap()
},
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.io.BufferedReader
/**
* Implementation of asset file reader
*
* @property assetManager asset manager
* @property dispatchers dispatchers
*/
internal class AndroidAssetReader(
private val assetManager: AssetManager,
private val dispatchers: CoroutineDispatcherProvider,
) : AssetReader {
override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) {
assetManager.open(fullFileName).bufferedReader()
.use(BufferedReader::readText)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.asset.reader
/**
* Asset file reader
*
* @see <a href = "https://www.notion.so/tangem/Assets-e045dd890413413faf34ce07ae47ff56">Documentation</a>
*
[REDACTED_AUTHOR]
*/
interface AssetReader {
/**
* Read content of file from assets
*
* @param fullFileName name of file with extension. Example: file.json
*/
suspend fun read(fullFileName: String): String
}

View file

@ -10,7 +10,7 @@ interface ConfigManager {
val config: Config
fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null)
suspend fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null)
fun turnOff(name: String)

View file

@ -19,7 +19,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
private var defaultConfig = Config()
override fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)?) {
override suspend fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)?) {
configLoader.load { configModel ->
setupFeature(configModel.features)
setupConfigValues(configModel.configValues)
@ -99,13 +99,16 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
),
chiaFireAcademyApiKey = configValues.chiaFireAcademyApiKey,
chiaTangemApiKey = configValues.chiaTangemApiKey,
polygonScanApiKey = configValues.polygonScanApiKey,
bittensorDwellirApiKey = configValues.bittensorDwellirApiKey,
bittensorOnfinalityApiKey = configValues.bittensorOnfinalityKey,
),
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = configValues.amplitudeApiKey,
sprinklr = configValues.sprinklr,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
express = if (BuildConfig.ENVIRONMENT == "dev") configValues.devExpress else configValues.express,
stakeKitApiKey = configValues.stakeKitApiKey,
)
}
@ -146,8 +149,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
blockBookRest = accessTokens.bitcoin?.blockBookRest,
),
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
)
}

View file

@ -1,38 +1,25 @@
package com.tangem.datasource.config
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class FeaturesLocalLoader(
private val assetReader: AssetReader,
private val moshi: Moshi,
private val assetLoader: AssetLoader,
buildEnvironment: String,
) : Loader<ConfigModel> {
private val featuresName = "features_$buildEnvironment"
private val configValuesName = "tangem-app-config/config_$buildEnvironment"
override fun load(onComplete: (ConfigModel) -> Unit) {
val config = try {
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = assetReader.readJson(featuresName)
val jsonConfigValues = assetReader.readJson(configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {
Timber.e(ex)
ConfigModel.empty()
}
onComplete(config)
override suspend fun load(onComplete: (ConfigModel) -> Unit) {
ConfigModel(
features = assetLoader.load<FeatureModel>(fileName = featuresName),
configValues = assetLoader.load<ConfigValueModel>(fileName = configValuesName),
).also(onComplete)
}
}

View file

@ -4,5 +4,5 @@ package com.tangem.datasource.config
[REDACTED_AUTHOR]
*/
interface Loader<T> {
fun load(onComplete: (T) -> Unit)
suspend fun load(onComplete: (T) -> Unit)
}

View file

@ -8,7 +8,6 @@ data class Config(
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val mercuryoWidgetId: String = "",
val mercuryoSecret: String = "",
val appsFlyerDevKey: String = "",
val amplitudeApiKey: String = "",
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isTopUpEnabled: Boolean = false,
@ -18,4 +17,5 @@ data class Config(
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
val express: ExpressModel? = null,
val stakeKitApiKey: String? = null,
)

View file

@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass
[REDACTED_AUTHOR]
*/
// TODO remove
class FeatureModel(
val isTopUpEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
@ -30,7 +31,6 @@ class ConfigValueModel(
@Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyer: AppsFlyer,
val sprinklr: SprinklrConfig?,
val tronGridApiKey: String,
val amplitudeApiKey: String,
@ -41,6 +41,10 @@ class ConfigValueModel(
val chiaTangemApiKey: String?,
val devExpress: ExpressModel?,
val express: ExpressModel?,
val polygonScanApiKey: String?,
val stakeKitApiKey: String?,
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
)
@JsonClass(generateAdapter = true)
@ -80,11 +84,6 @@ data class GetBlockToken(
@Json(name = "rosetta") val rosetta: String?,
)
data class AppsFlyer(
val appsFlyerDevKey: String,
val appsFlyerAppID: String,
)
class ConfigModel(
val features: FeatureModel?,
val configValues: ConfigValueModel?,

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.config.models
import com.squareup.moshi.Json
/** Config provider model */
sealed class ProviderModel {
/**
* Example,
* {
* "type": "public",
* "url": "https://example.com"
* }
*/
data class Public(
@Json(name = "url") val url: String,
) : ProviderModel()
/**
* Example,
* {
* "type": "private",
* "name": "nownodes"
* }
*/
data class Private(
@Json(name = "name") val name: String,
) : ProviderModel()
/** Unsupported type */
data object UnsupportedType : ProviderModel()
}

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.AccountCreator
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.blockchain.DefaultAccountCreator
import com.tangem.lib.auth.AuthProvider
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 AccountCreatorModule {
@Provides
@Singleton
fun provideAccountCreator(authProvider: AuthProvider, tangemTechApi: TangemTechApi): AccountCreator {
return DefaultAccountCreator(authProvider, tangemTechApi)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.asset.AndroidAssetReader
import com.tangem.datasource.asset.AssetReader
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 AssetModule {
@Binds
@Singleton
fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader
}

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AssetReaderModule {
@Singleton
@Provides
fun providesAsserReader(
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): AssetReader {
return AndroidAssetReader(context.assets, dispatchers)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
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 BlockchainDataStorageModule {
@Provides
@Singleton
fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage {
return DefaultBlockchainDataStorage(appPreferencesStore)
}
}

View file

@ -1,11 +1,15 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
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.common.adapter.BigDecimalAdapter
import com.tangem.datasource.api.common.adapter.DateTimeAdapter
import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter
import com.tangem.datasource.api.stakekit.models.response.model.Token
import com.tangem.datasource.config.models.ProviderModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -21,10 +25,20 @@ class MoshiModule {
@NetworkMoshi
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
.withSubtype(ProviderModel.Public::class.java, "public")
.withSubtype(ProviderModel.Private::class.java, "private")
.withDefaultValue(ProviderModel.UnsupportedType),
)
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(KotlinJsonAdapterFactory())
.add(
Token.NetworkType::class.java,
UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN),
)
.build()
}

View file

@ -5,12 +5,17 @@ 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
import com.tangem.datasource.utils.RequestHeader
import com.tangem.datasource.utils.RequestHeader.*
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 com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,6 +24,7 @@ import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@ -34,7 +40,7 @@ class NetworkModule {
appVersionProvider: AppVersionProvider,
): TangemExpressApi {
val url = if (BuildConfig.ENVIRONMENT == "dev") {
DEV_EXPRESS_BASE_URL
STAGE_EXPRESS_BASE_URL
} else {
PROD_EXPRESS_BASE_URL
}
@ -55,55 +61,117 @@ class NetworkModule {
@Provides
@Singleton
fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi {
fun provideStakeKitApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
stakeKitAuthProvider: StakeKitAuthProvider,
): StakeKitApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
.baseUrl(STAKEKIT_BASE_URL)
.client(
OkHttpClient.Builder()
.addHeaders(
CacheControlHeader,
// TODO("refactor header init") get auth data after biometric auth to avoid race condition
// AuthenticationHeader(authProvider),
)
.addHeaders(StakeKit(stakeKitAuthProvider))
.addLoggers(context)
.build(),
)
.build()
.create(TangemTechApi::class.java)
.create(StakeKitApi::class.java)
}
@Provides
@Singleton
fun provideTangemTechApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
): TangemTechApi {
return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V1_TANGEM_TECH_BASE_URL)
}
@Provides
@Singleton
fun provideTangemTechApiV2(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
): TangemTechApiV2 {
return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V2_TANGEM_TECH_BASE_URL)
}
@Provides
@DevTangemApi
@Singleton
fun provideTangemTechDevApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi {
fun provideTangemTechDevApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
): TangemTechApi {
return provideTangemTechApiInternal(moshi, context, appVersionProvider, DEV_V1_TANGEM_TECH_BASE_URL)
}
@Provides
@Singleton
fun provideTangemTechServiceApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
): TangemTechServiceApi {
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
baseUrl = PROD_V1_TANGEM_TECH_BASE_URL,
timeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS,
requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)),
)
}
private inline fun <reified T> provideTangemTechApiInternal(
moshi: Moshi,
context: Context,
appVersionProvider: AppVersionProvider,
baseUrl: String,
timeoutSeconds: Long? = null,
requestHeaders: List<RequestHeader> = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)),
): T {
val client = OkHttpClient.Builder()
.let { builder ->
if (timeoutSeconds != null) {
builder.callTimeout(timeoutSeconds, TimeUnit.SECONDS)
} else {
builder
}
}
.addHeaders(
*requestHeaders.toTypedArray(),
// TODO("refactor header init") get auth data after biometric auth to avoid race condition
// AuthenticationHeader(authProvider),
)
.addLoggers(context)
.build()
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(DEV_TANGEM_TECH_BASE_URL)
.client(
OkHttpClient.Builder()
.addHeaders(
CacheControlHeader,
// TODO("refactor header init") get auth data after biometric auth to avoid race condition
// AuthenticationHeader(authProvider),
)
.addLoggers(context)
.build(),
)
.baseUrl(baseUrl)
.client(client)
.build()
.create(TangemTechApi::class.java)
.create(T::class.java)
}
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 STAGE_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
const val API_ONE_INCH_TIMEOUT_MS = 5000L
const val PROD_V1_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import dagger.Module
@ -16,12 +14,11 @@ import javax.inject.Singleton
*/
@Module
@InstallIn(SingletonComponent::class)
object TestnetTokensStorageModule {
internal object TestnetTokensStorageModule {
@OptIn(ExperimentalStdlibApi::class)
@Provides
@Singleton
fun providesTestnetTokensStorage(assetReader: AssetReader, @SdkMoshi moshi: Moshi): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetReader = assetReader, adapter = moshi.adapter())
fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
return DefaultTestnetTokensStorage(assetLoader)
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.datasource.local.blockchain
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
import com.tangem.lib.auth.AuthProvider
internal class DefaultAccountCreator(
private val authProvider: AuthProvider,
private val tangemTechApi: TangemTechApi,
) : AccountCreator {
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString())
return try {
val response = tangemTechApi.createUserNetworkAccount(
cardPublicKey = authProvider.getCardPublicKey(),
cardId = authProvider.getCardId(),
body = request,
).getOrThrow()
Result.Success(response.data.accountId)
} catch (e: Exception) {
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
}
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.datasource.local.blockchain
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
/**
* [BlockchainDataStorage] implementation
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultBlockchainDataStorage(
private val appPreferencesStore: AppPreferencesStore,
) : BlockchainDataStorage {
override suspend fun getOrNull(key: String): String? {
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
}
override suspend fun store(key: String, value: String) {
appPreferencesStore.edit {
it[stringPreferencesKey(key)] = value
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.datasource.local.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.tangem.datasource.local.db.dao.CryptoCurrenciesAccountDao
import com.tangem.datasource.local.db.dao.CryptoCurrencyDao
import com.tangem.datasource.local.db.dao.UserWalletDao
import com.tangem.datasource.local.db.entity.CryptoCurrenciesAccountEntity
import com.tangem.datasource.local.db.entity.CryptoCurrencyEntity
import com.tangem.datasource.local.db.entity.UserWalletEntity
import com.tangem.datasource.local.db.utils.Converters
@Database(
entities = [
UserWalletEntity::class,
CryptoCurrenciesAccountEntity::class,
CryptoCurrencyEntity::class,
],
version = 1,
)
@TypeConverters(Converters::class)
abstract class TangemDatabase : RoomDatabase() {
abstract fun userWalletDao(): UserWalletDao
abstract fun cryptoCurrencyDao(): CryptoCurrencyDao
abstract fun cryptoCurrenciesAccountDao(): CryptoCurrenciesAccountDao
}

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.local.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import com.tangem.datasource.local.db.entity.CryptoCurrenciesAccountEntity
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@Dao
interface CryptoCurrenciesAccountDao {
@Insert
suspend fun insert(account: CryptoCurrenciesAccountEntity)
@Update
suspend fun update(account: CryptoCurrenciesAccountEntity)
@Query("SELECT * FROM CryptoCurrenciesAccountEntity WHERE userWalletId = :userWalletId")
fun observeByUserWalletId(userWalletId: UserWalletId): Flow<List<CryptoCurrenciesAccountEntity>>
@Query("SELECT * FROM CryptoCurrenciesAccountEntity WHERE userWalletId = :userWalletId")
suspend fun selectByUserWalletId(userWalletId: UserWalletId): List<CryptoCurrenciesAccountEntity>
}

View file

@ -0,0 +1,39 @@
package com.tangem.datasource.local.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.tangem.datasource.local.db.entity.CryptoCurrencyEntity
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@Dao
interface CryptoCurrencyDao {
@Insert
suspend fun insert(currencies: List<CryptoCurrencyEntity>)
@Query(
"""
SELECT * FROM CryptoCurrencyEntity
WHERE userWalletId = :userWalletId AND accountId = :accountId
""",
)
suspend fun selectByAccountId(userWalletId: UserWalletId, accountId: Int): List<CryptoCurrencyEntity>
@Query(
"""
SELECT * FROM CryptoCurrencyEntity
WHERE userWalletId = :userWalletId AND accountId = :accountId
""",
)
fun observeByAccountId(userWalletId: UserWalletId, accountId: Int): Flow<List<CryptoCurrencyEntity>>
@Query(
"""
SELECT COUNT(*) FROM CryptoCurrencyEntity
WHERE userWalletId = :userWalletId
""",
)
suspend fun countByUserWalletId(userWalletId: UserWalletId): Int
}

View file

@ -0,0 +1,30 @@
package com.tangem.datasource.local.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import com.tangem.datasource.local.db.entity.UserWalletEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface UserWalletDao {
@Insert
suspend fun insert(vararg wallets: UserWalletEntity)
@Update
suspend fun update(wallet: UserWalletEntity)
@Query("SELECT * FROM UserWalletEntity")
suspend fun selectAll(): List<UserWalletEntity>
@Query("SELECT * FROM UserWalletEntity")
fun observeAll(): Flow<List<UserWalletEntity>>
@Query("SELECT * FROM UserWalletEntity WHERE id = :id")
suspend fun selectById(id: String): UserWalletEntity?
@Query("SELECT * FROM UserWalletEntity WHERE id = :id")
fun observeById(id: String): Flow<UserWalletEntity>
}

View file

@ -0,0 +1,28 @@
package com.tangem.datasource.local.db.di
import android.content.Context
import androidx.room.Room
import com.tangem.datasource.local.db.TangemDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TangemDatabaseModule {
private const val DATABASE_NAME = "tangem_database.db"
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): TangemDatabase {
return Room.databaseBuilder(
context,
TangemDatabase::class.java,
DATABASE_NAME,
).build()
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.datasource.local.db.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = UserWalletEntity::class,
parentColumns = ["id"],
childColumns = ["userWalletId"],
onDelete = ForeignKey.CASCADE,
),
],
indices = [
Index(value = ["id"]),
Index(value = ["userWalletId"]),
],
)
data class CryptoCurrenciesAccountEntity(
@PrimaryKey
val id: Int,
val userWalletId: String,
val title: String,
val currenciesCount: Int,
val isArchived: Boolean,
val ordinalNumber: Int,
)

View file

@ -0,0 +1,42 @@
package com.tangem.datasource.local.db.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = CryptoCurrenciesAccountEntity::class,
parentColumns = ["id"],
childColumns = ["accountId"],
onDelete = ForeignKey.CASCADE,
),
ForeignKey(
entity = UserWalletEntity::class,
parentColumns = ["id"],
childColumns = ["userWalletId"],
onDelete = ForeignKey.CASCADE,
),
],
indices = [
Index(value = ["currencyBackendId"]),
Index(value = ["networkId"]),
Index(value = ["accountId"]),
Index(value = ["userWalletId"]),
],
)
data class CryptoCurrencyEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val currencyBackendId: String?,
val networkId: String,
val accountId: Int,
val userWalletId: String,
val name: String,
val symbol: String,
val decimals: Int,
val contractAddress: String?,
val derivationPath: String?,
)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.local.db.entity
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
indices = [
Index(value = ["id"], unique = true),
Index(value = ["ordinalNumber"], unique = true),
],
)
data class UserWalletEntity(
@PrimaryKey
val id: String,
val name: String,
val artworkUrl: String,
val isMultiCurrency: Boolean,
val hasBackupError: Boolean,
val cardsInWallet: Set<String>,
val ordinalNumber: Int,
)

View file

@ -0,0 +1,27 @@
package com.tangem.datasource.local.db.utils
import androidx.room.TypeConverter
import com.tangem.domain.wallets.models.UserWalletId
internal class Converters {
@TypeConverter
fun setFromString(value: String): Set<String> {
return value.split(",").toSet()
}
@TypeConverter
fun setToString(set: Set<Any>): String {
return set.joinToString(",")
}
@TypeConverter
fun userWalletIdFromString(value: String): UserWalletId {
return UserWalletId(value)
}
@TypeConverter
fun userWalletIdToString(userWalletId: UserWalletId): String {
return userWalletId.stringValue
}
}

View file

@ -5,9 +5,13 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_
import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_APPLICATION_STOPPED_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN
/**
@ -19,6 +23,8 @@ object PreferencesKeys {
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") }
@ -79,6 +85,14 @@ object PreferencesKeys {
val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") }
val WAS_APPLICATION_STOPPED_KEY by lazy { booleanPreferencesKey(name = "applicationStopped") }
val SHOULD_OPEN_WELCOME_ON_RESUME_KEY by lazy { booleanPreferencesKey(name = "openWelcomeOnResume") }
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
}
@ -86,6 +100,7 @@ object PreferencesKeys {
internal fun getTapPrefKeysToMigrate(): Set<String> {
return setOf(
SAVE_USER_WALLETS_KEY,
SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
APP_LAUNCH_COUNT_KEY,
SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
FUNDS_FOUND_DATE_KEY,
@ -93,6 +108,9 @@ internal fun getTapPrefKeysToMigrate(): Set<String> {
USED_CARDS_INFO_KEY,
WAS_TWINS_ONBOARDING_SHOWN,
IS_TANGEM_TOS_ACCEPTED_KEY,
WAS_APPLICATION_STOPPED_KEY,
SHOULD_OPEN_WELCOME_ON_RESUME_KEY,
SHOULD_SAVE_ACCESS_CODES_KEY,
)
.map(Preferences.Key<*>::name)
.toSet()

View file

@ -1,28 +1,21 @@
package com.tangem.datasource.local.testnet
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
/**
* Default implementation for storing testnet tokens data
*
* @property assetReader file reader from assets
* @property adapter adapter for parsing testnet tokens config
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class DefaultTestnetTokensStorage(
private val assetReader: AssetReader,
private val adapter: JsonAdapter<TestnetTokensConfig>,
private val assetLoader: AssetLoader,
) : TestnetTokensStorage {
override fun getConfig(): TestnetTokensConfig {
return requireNotNull(
value = adapter.fromJson(
assetReader.readJson(fileName = LOCAL_CONFIG_PATH),
),
)
override suspend fun getConfig(): TestnetTokensConfig {
return requireNotNull(assetLoader.load<TestnetTokensConfig>(fileName = LOCAL_CONFIG_PATH))
}
private companion object {

View file

@ -10,5 +10,5 @@ import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
interface TestnetTokensStorage {
/** Get a testnet tokens data */
fun getConfig(): TestnetTokensConfig
suspend fun getConfig(): TestnetTokensConfig
}

View file

@ -1,9 +1,9 @@
package com.tangem.datasource.utils
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.AuthBearerProvider
import com.tangem.lib.auth.AuthProvider
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.version.AppVersionProvider
/**
* Presentation of request header
@ -15,7 +15,7 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
/** Header list */
val values: List<Pair<String, () -> String>> = pairs.toList()
object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
data object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
"card_id" to { authProvider.getCardId() },
@ -28,12 +28,12 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
"session-id" to { expressAuthProvider.getSessionId() },
)
class AuthBearerHeader(authBearerProvider: AuthBearerProvider) : RequestHeader(
"Authorization" to { "Bearer " + authBearerProvider.getApiKey() },
)
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
"version" to { appVersionProvider.getAppVersion() },
"version" to { appVersionProvider.versionName },
"platform" to { "android" },
)
class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader(
"X-API-KEY" to { stakeKitAuthProvider.getApiKey() },
)
}

View file

@ -0,0 +1,119 @@
package com.tangem.datasource.asset.loader
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.asset.reader.AssetReader
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalStdlibApi::class)
class AssetLoaderTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
@Test
fun load() = runTest {
everyReadingJson() returns assetObjectJson
val jsonAdapter = mockk<JsonAdapter<Asset>>()
every { moshi.adapter<Asset>() } returns jsonAdapter
every { jsonAdapter.fromJson(assetObjectJson) } returns assetObject
val actual = assetLoader.load<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetObjectJson)
}
Truth.assertThat(actual).isEqualTo(assetObject)
}
@Test
fun loadList() = runTest {
everyReadingJson() returns assetListJson
val jsonAdapter = mockk<JsonAdapter<List<Asset>>>()
val types = Types.newParameterizedType(List::class.java, Asset::class.java)
every { moshi.adapter<List<Asset>>(types) } returns jsonAdapter
every { jsonAdapter.fromJson(assetListJson) } returns assetList
val actual = assetLoader.loadList<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetListJson)
}
Truth.assertThat(actual).isEqualTo(assetList)
}
@Test
fun loadMap() = runTest {
everyReadingJson() returns assetMapJson
val jsonAdapter = mockk<JsonAdapter<Map<String, Asset>>>()
val types = Types.newParameterizedType(Map::class.java, String::class.java, Asset::class.java)
every { moshi.adapter<Map<String, Asset>>(types) } returns jsonAdapter
every { jsonAdapter.fromJson(assetMapJson) } returns assetMap
val actual = assetLoader.loadMap<Asset>(fileName = JSON_FILE_NAME)
coVerifyOrder {
assetReader.read("$JSON_FILE_NAME.json")
jsonAdapter.fromJson(assetMapJson)
}
Truth.assertThat(actual).isEqualTo(assetMap)
}
private fun everyReadingJson() = coEvery { assetReader.read("$JSON_FILE_NAME.json") }
private companion object {
const val JSON_FILE_NAME = "config"
val assetObject = Asset(
contractAddress = "0x0000000000000000000000000000000000000000",
network = "Network",
exchangeAvailable = true,
)
val assetList = listOf(assetObject, assetObject)
val assetMap = mapOf("key1" to assetObject, "key2" to assetObject)
val assetObjectJson = """
{
"contractAddress": "${assetObject.contractAddress}",
"network": "${assetObject.network}",
"exchangeAvailable": ${assetObject.exchangeAvailable}
}
""".trimIndent()
val assetListJson = """
$assetObjectJson,
$assetObjectJson
""".trimIndent()
val assetMapJson = """
{
"key1": $assetObjectJson,
"key2": $assetObjectJson
}
""".trimIndent()
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.google.common.truth.Truth
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.IOException
/**
[REDACTED_AUTHOR]
*/
internal class AndroidAssetReaderTest {
private val assetManager = mockk<AssetManager>()
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
@Test
fun read_content() = runTest {
every { assetManager.open(FILE_NAME) } returns json.byteInputStream()
val actual = assetReader.read(fullFileName = FILE_NAME)
Truth.assertThat(actual).isEqualTo(json)
}
@Test
fun read_error() = runTest {
val exception = IOException("Error")
every { assetManager.open(FILE_NAME) } throws exception
runCatching { assetReader.read(fullFileName = FILE_NAME) }
.onSuccess { throw IllegalStateException("Error should be thrown") }
.onFailure {
Truth.assertThat(it).isEqualTo(exception)
}
}
private companion object {
const val FILE_NAME = "file.json"
val json = """
{
"key": "value"
}
""".trimIndent()
}
}

1
core/decompose/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
dependencies {
api(projects.core.utils)
api(deps.decompose)
implementation(deps.kotlin.coroutines)
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,23 @@
package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.tangem.core.decompose.di.HiltComponentBuilderOwner
import com.tangem.core.decompose.navigation.NavigationOwner
import com.tangem.core.decompose.ui.UiMessageSenderOwner
import com.tangem.core.decompose.utils.ComponentScopeOwner
import com.tangem.core.decompose.utils.DispatchersOwner
import com.tangem.core.decompose.utils.TagsOwner
/**
* Interface for the application component context.
*
* It combines several other interfaces related to navigation, dispatching, UI messaging, etc.
*/
interface AppComponentContext :
ComponentContext,
NavigationOwner,
ComponentScopeOwner,
DispatchersOwner,
UiMessageSenderOwner,
HiltComponentBuilderOwner,
TagsOwner

View file

@ -0,0 +1,73 @@
package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.tangem.core.decompose.di.HiltComponentBuilderOwner
import com.tangem.core.decompose.navigation.NavigationOwner
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.decompose.ui.UiMessageSenderOwner
import com.tangem.core.decompose.utils.ComponentCoroutineScope
import com.tangem.core.decompose.utils.DispatchersOwner
import kotlinx.coroutines.CoroutineScope
/**
* Creates a new child [AppComponentContext] with the provided [key] and optional [lifecycle].
*
* @param key The key to use.
* @param lifecycle The [Lifecycle] to use. If not provided, the parent's lifecycle will be used.
* @param router The [Router] to use in the child. If not provided, the parent's router will be used.
* @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will
* be used.
*
* @see childByContext
* */
fun AppComponentContext.child(
key: String,
lifecycle: Lifecycle? = null,
router: Router? = null,
messageHandler: UiMessageHandler? = null,
): AppComponentContext = childByContext(
componentContext = childContext(key, lifecycle),
router = router,
messageHandler = messageHandler,
)
/**
* Creates a new child [AppComponentContext] with the provided [componentContext].
*
* @param componentContext The [ComponentContext] to use.
* @param router The [Router] to use in the child. If not provided, the parent's router will be used.
* @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will
* be used.
*
* @see child
* */
fun AppComponentContext.childByContext(
componentContext: ComponentContext,
router: Router? = null,
messageHandler: UiMessageHandler? = null,
): AppComponentContext = object :
AppComponentContext,
ComponentContext by componentContext,
NavigationOwner by this@childByContext,
UiMessageSenderOwner by this@childByContext,
DispatchersOwner by this@childByContext,
HiltComponentBuilderOwner by this@childByContext {
override val tags: HashMap<String, Any> = HashMap()
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
override val messageSender: UiMessageSender = messageHandler
?.let(::DefaultUiMessageSender)
?: this@childByContext.messageSender
override val router: Router
get() = router ?: this@childByContext.router
}

View file

@ -0,0 +1,35 @@
package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.navigation.AppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultRouter
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.decompose.utils.ComponentCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
class DefaultAppComponentContext(
componentContext: ComponentContext,
messageHandler: UiMessageHandler,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
) : AppComponentContext, ComponentContext by componentContext {
override val tags: HashMap<String, Any> = HashMap()
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler)
override val navigationProvider: AppNavigationProvider
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
override val router: Router
get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
}

View file

@ -0,0 +1,9 @@
package com.tangem.core.decompose.di
/**
* Annotation for marking a dependency as a component scoped.
*
* This means that the lifecycle of the dependency is limited to the lifecycle of the component it is attached to.
*/
@Retention(AnnotationRetention.SOURCE)
annotation class ComponentScoped

View file

@ -0,0 +1,47 @@
package com.tangem.core.decompose.di
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import dagger.BindsInstance
import dagger.hilt.DefineComponent
import dagger.hilt.components.SingletonComponent
/**
* Interface for the Decompose component.
*
* It is annotated as [ComponentScoped], meaning it has a lifecycle that is scoped to the component.
*/
@ComponentScoped
@DefineComponent(parent = SingletonComponent::class)
interface DecomposeComponent {
/**
* Builder interface for the component.
*/
@DefineComponent.Builder
interface Builder {
/**
* Sets the router for the component.
*
* @param router The router to set.
* @return The builder instance.
*/
fun router(@BindsInstance router: Router): Builder
/**
* Sets the UI message sender for the component.
*
* @param uiMessageSender The UI message sender to set.
* @return The builder instance.
*/
fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder
/**
* Builds the Decompose component.
*
* @return The built Decompose component.
*/
fun build(): DecomposeComponent
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.di
/**
* Interface for owning a Hilt component builder.
*/
interface HiltComponentBuilderOwner {
/**
* Provides access to the Hilt component builder instance.
*/
val hiltComponentBuilder: DecomposeComponent.Builder
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.decompose.di
import javax.inject.Qualifier
@Qualifier
annotation class RootAppComponentContext

View file

@ -0,0 +1,78 @@
package com.tangem.core.decompose.model
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Abstract class for a component's model.
*
* It provides access to the coroutine dispatchers and a coroutine scope which will survive re-creation of component
* and will be destroyed when the component is destroyed.
*
* Also, it can inject and use some component features like [Router] and [UiMessageSender].
*/
abstract class Model : InstanceKeeper.Instance {
/**
* Provides access to the coroutine dispatchers.
*/
protected abstract val dispatchers: CoroutineDispatcherProvider
/**
* The coroutine scope for the model. That will be cancelled when the model is destroyed.
*/
protected val modelScope by lazy {
CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
}
override fun onDestroy() {
runCatching { modelScope.cancel() }
}
/**
* Launches a coroutine in the model's scope and updates the [progressFlow] with the progress state.
*
* @param progressFlow The [SharedFlow] to emit the progress state.
* @param dispatcher The [CoroutineDispatcher] to launch the coroutine. Default is [Dispatchers.Main.immediate].
* @param block The block of code to execute.
*
* @return The [Job] of the launched coroutine.
* */
protected inline fun withProgress(
progressFlow: MutableSharedFlow<Boolean>,
dispatcher: CoroutineDispatcher = dispatchers.mainImmediate,
crossinline block: suspend () -> Unit,
): Job = modelScope.launch(dispatcher) {
progressFlow.emit(value = true)
try {
block()
} finally {
withContext(NonCancellable) {
progressFlow.emit(value = false)
}
}
}
/**
* Converts a cold [Flow] to a hot [SharedFlow] that will be shared in the model's scope.
*
* @param started The [SharingStarted] strategy to start sharing the flow. Default is [SharingStarted.WhileSubscribed].
* @param replay The number of values to replay. Default is `1`.
*
* @return The [SharedFlow] that will be shared in the model's scope.
* @see [Flow.shareIn]
* */
protected fun <T> Flow<T>.share(
started: SharingStarted = SharingStarted.WhileSubscribed(),
replay: Int = 1,
): SharedFlow<T> = shareIn(
scope = modelScope,
started = started,
replay = replay,
)
}

View file

@ -0,0 +1,51 @@
package com.tangem.core.decompose.model
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import javax.inject.Provider
/**
* Entry point for the models in the application.
*
* It provides a map of model providers.
*/
@EntryPoint
@InstallIn(DecomposeComponent::class)
interface ModelsEntryPoint {
fun models(): Map<Class<*>, Provider<Model>>
}
/**
* Gets or creates a component's [Model].
*/
inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M {
val modelKey = "model_${M::class.simpleName}"
val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") {
val hiltComponent = hiltComponentBuilder
.router(router)
.uiMessageSender(messageSender)
.build()
EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java)
}
val model = instanceKeeper.getOrCreate(modelKey) {
requireNotNull(entryPoint.models()[M::class.java]?.get()) {
"Model ${M::class.simpleName} is not provided"
}
}
val isModelExist = tags.getOrElse(modelKey) { false } as Boolean
if (!isModelExist) {
tags[modelKey] = true
}
return model as M
}

View file

@ -0,0 +1,28 @@
@file:Suppress("UNCHECKED_CAST")
package com.tangem.core.decompose.navigation
import com.arkivanov.decompose.router.stack.StackNavigation
/**
* Interface for providing application navigation.
* It provides or creates a StackNavigation instance for the application.
*/
interface AppNavigationProvider {
/**
* Gets or creates a StackNavigation instance.
*
* @return The StackNavigation instance.
*/
fun getOrCreate(): StackNavigation<Route>
}
/**
* Gets or creates a [StackNavigation] instance of a specific type.
*
* @return The [StackNavigation] instance.
*/
fun <R : Route> AppNavigationProvider.getOrCreateTyped(): StackNavigation<R> {
return getOrCreate() as StackNavigation<R>
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.decompose.navigation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
internal class DefaultAppNavigationProvider : AppNavigationProvider, InstanceKeeper.Instance {
private var navigation: StackNavigation<Route>? = null
override fun getOrCreate(): StackNavigation<Route> {
return navigation ?: StackNavigation<Route>().also { navigation = it }
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.decompose.navigation
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.popWhile
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
internal class DefaultRouter(
private val navigationProvider: AppNavigationProvider,
) : Router, InstanceKeeper.Instance {
private val navigation: StackNavigation<Route>
get() = navigationProvider.getOrCreate()
@OptIn(ExperimentalDecomposeApi::class)
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
navigation.pushNew(route, onComplete)
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
navigation.pop(onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
navigation.popWhile(
predicate = { it != route },
onComplete = onComplete,
)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.core.decompose.navigation
/**
* Interface for owning navigation-related properties.
*/
interface NavigationOwner {
/**
* The [Router] instance.
*/
val router: Router
/**
* The [AppNavigationProvider] instance.
*/
val navigationProvider: AppNavigationProvider
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.decompose.navigation
/**
* Interface for a route in the application.
*/
interface Route

View file

@ -0,0 +1,31 @@
package com.tangem.core.decompose.navigation
/**
* Interface for a router in the application.
* It provides methods for navigating through the application.
*/
interface Router {
/**
* Pushes a new route to the navigation stack.
*
* @param route The route to push.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops the top route from the navigation stack.
*
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun pop(onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops routes from the navigation stack until the specified route is found.
*
* @param route The route to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.decompose.ui
internal class DefaultUiMessageSender(
private val handler: UiMessageHandler,
) : UiMessageSender {
override fun send(message: UiMessage) {
handler.handleMessage(message)
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.core.decompose.ui
/**
* Interface for a message that can be sent to a [UiMessageSender] and handled by a [UiMessageHandler].
*
* @see UiMessageSender
* @see UiMessageHandler
*/
interface UiMessage

View file

@ -0,0 +1,17 @@
package com.tangem.core.decompose.ui
/**
* Interface for handling UI messages.
*
* @see UiMessage
* @see UiMessageSender
*/
interface UiMessageHandler {
/**
* Handles the given UI message.
*
* @param message The UI message to handle.
*/
fun handleMessage(message: UiMessage)
}

View file

@ -0,0 +1,17 @@
package com.tangem.core.decompose.ui
/**
* Interface for sending messages to UI.
*
* @see UiMessage
* @see UiMessageHandler
* */
interface UiMessageSender {
/**
* Sends the given UI message.
*
* @param message The UI message to send.
* */
fun send(message: UiMessage)
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.ui
/**
* Interface for owning a [UiMessageSender].
* */
interface UiMessageSenderOwner {
/**
* The [UiMessageSender] instance.
* */
val messageSender: UiMessageSender
}

View file

@ -0,0 +1,20 @@
package com.tangem.core.decompose.utils
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
/**
* [CoroutineDispatcherProvider.mainImmediate] dispatcher.
* */
@Suppress("FunctionName")
internal fun ComponentCoroutineScope(lifecycle: Lifecycle, dispatchers: CoroutineDispatcherProvider): CoroutineScope {
val scope = CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
lifecycle.doOnDestroy(scope::cancel)
return scope
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.decompose.utils
import kotlinx.coroutines.CoroutineScope
/**
* Interface for owning a component scope.
*/
interface ComponentScopeOwner {
/**
* Provides access to the component's [CoroutineScope] instance.
*
* This scope is used for launching coroutines that are bound to the component's lifecycle.
*/
val componentScope: CoroutineScope
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.decompose.utils
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Interface for owning a [CoroutineDispatcherProvider].
*/
interface DispatchersOwner {
/**
* Provides access to the [CoroutineDispatcherProvider] instance.
*/
val dispatchers: CoroutineDispatcherProvider
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.utils
/**
* Interface for owning tags.
*/
interface TagsOwner {
/**
* Provides access to the tags map instance.
*/
val tags: HashMap<String, Any>
}

View file

@ -26,6 +26,7 @@ dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)

View file

@ -11,20 +11,36 @@
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "5.10.0"
},
{
"name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED",
"version": "5.8.0"
},
{
"name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.12.0"
"version": "5.13.0"
},
{
"name": "GENERATE_XPUB_ENABLED",
"version": "undefined"
"version": "5.12.0"
},
{
"name": "WC_SOLANA_TX_SIGN_ENABLED",
"version": "undefined"
},
{
"name": "TOKEN_LIST_LCE_ENABLED",
"version": "5.12.0"
},
{
"name": "CARDANO_TOKENS_SUPPORT_ENABLED",
"version": "5.12.0"
},
{
"name": "STAKING_ENABLED",
"version": "undefined"
},
{
"name": "FULL_RESET_ENABLED",
"version": "5.12.0"
},
{
"name": "DETAILS_REDESIGN_ENABLED",
"version": "undefined"
}
]

View file

@ -1,16 +1,13 @@
package com.tangem.core.featuretoggle.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
@ -25,17 +22,12 @@ internal object FeatureTogglesManagerModule {
@Provides
@Singleton
@OptIn(ExperimentalStdlibApi::class)
fun provideFeatureTogglesManager(
@ApplicationContext context: Context,
assetReader: AssetReader,
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
assetReader = assetReader,
jsonAdapter = moshi.adapter(),
)
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context)
return if (BuildConfig.TESTER_MENU_ENABLED) {

View file

@ -1,41 +1,28 @@
package com.tangem.core.featuretoggle.storage
import androidx.annotation.VisibleForTesting
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
import com.tangem.datasource.asset.AssetReader
import timber.log.Timber
import com.tangem.datasource.asset.loader.AssetLoader
import kotlin.properties.Delegates
/**
* Storage implementation for storing local feature toggles.
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
*
* @property assetReader asset reader
* @property jsonAdapter adapter for parsing local json config
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class LocalFeatureTogglesStorage(
private val assetReader: AssetReader,
private val jsonAdapter: JsonAdapter<List<FeatureToggle>>,
private val assetLoader: AssetLoader,
) : FeatureTogglesStorage {
override var featureToggles: List<FeatureToggle> by Delegates.notNull()
private set
override suspend fun init() {
runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) }
.onSuccess { featureToggles = it }
.onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") }
featureToggles = assetLoader.loadList<FeatureToggle>(LOCAL_CONFIG_PATH)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getConfigPath() = LOCAL_CONFIG_PATH
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getFeatureToggles(): Iterable<FeatureToggle> = featureToggles
private companion object {
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
}

Some files were not shown because too many files have changed in this diff Show more