Updated on 2026-08-14
This commit is contained in:
commit
3fba2ae059
872 changed files with 17188 additions and 8108 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
1
core/decompose/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
core/decompose/build.gradle.kts
Normal file
15
core/decompose/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.decompose.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class RootAppComponentContext
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
/**
|
||||
* Interface for a route in the application.
|
||||
*/
|
||||
interface Route
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
/**
|
||||
* Interface for owning a [UiMessageSender].
|
||||
* */
|
||||
interface UiMessageSenderOwner {
|
||||
|
||||
/**
|
||||
* The [UiMessageSender] instance.
|
||||
* */
|
||||
val messageSender: UiMessageSender
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ dependencies {
|
|||
|
||||
/** Core modules */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue