Updated on 2026-08-14
This commit is contained in:
commit
dbb63a396f
448 changed files with 12230 additions and 2075 deletions
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.api.common.blockaid
|
||||
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface BlockAidApi {
|
||||
|
||||
@POST("site/scan")
|
||||
suspend fun scanDomain(@Body request: DomainScanRequest): DomainScanResponse
|
||||
|
||||
@POST("evm/json-rpc/scan")
|
||||
suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse
|
||||
|
||||
@POST("solana/message/scan")
|
||||
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DomainScanRequest(
|
||||
@Json(name = "url") val url: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EvmTransactionScanRequest(
|
||||
@Json(name = "chain") val chain: String,
|
||||
@Json(name = "account_address") val accountAddress: String,
|
||||
@Json(name = "method") val method: String,
|
||||
@Json(name = "data") val data: RpcData,
|
||||
@Json(name = "options") val options: List<String> = listOf("simulation", "validation"),
|
||||
@Json(name = "metadata") val metadata: TransactionMetadata,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RpcData(
|
||||
@Json(name = "jsonrpc") val jsonrpc: String = "2.0",
|
||||
@Json(name = "method") val method: String,
|
||||
@Json(name = "params") val params: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SolanaTransactionScanRequest(
|
||||
@Json(name = "encoding") val encoding: String = "base64",
|
||||
@Json(name = "chain") val chain: String,
|
||||
@Json(name = "method") val method: String,
|
||||
@Json(name = "options") val options: List<String> = listOf("simulation, validation"),
|
||||
@Json(name = "metadata") val metadata: TransactionMetadata,
|
||||
@Json(name = "account_address") val accountAddress: String,
|
||||
@Json(name = "transactions") val transactions: List<String>,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountSummaryResponse(
|
||||
@Json(name = "assets_diffs") val assetsDiffs: List<AssetDiff>,
|
||||
@Json(name = "exposures") val exposures: List<Exposure>,
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AssetDiff(
|
||||
@Json(name = "asset_type") val assetType: String,
|
||||
@Json(name = "asset") val asset: Asset,
|
||||
@Json(name = "in") val inTransfer: List<Transfer>? = null,
|
||||
@Json(name = "out") val outTransfer: List<Transfer>? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Asset(
|
||||
@Json(name = "chain_id") val chainId: Int? = null,
|
||||
@Json(name = "logo_url") val logoUrl: String? = null,
|
||||
@Json(name = "symbol") val symbol: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Transfer(
|
||||
@Json(name = "value") val value: String,
|
||||
@Json(name = "raw_value") val rawValue: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DomainScanResponse(
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "is_malicious") val isMalicious: Boolean?,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Exposure(
|
||||
@Json(name = "asset") val asset: Asset,
|
||||
@Json(name = "spenders") val spenders: Map<String, SpenderDetails>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SpenderDetails(
|
||||
@Json(name = "exposure") val exposure: List<ExposureDetail>,
|
||||
@Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExposureDetail(
|
||||
@Json(name = "value") val value: String,
|
||||
@Json(name = "raw_value") val rawValue: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SimulationResponse(
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "account_summary") val accountSummary: AccountSummaryResponse,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TransactionMetadata(
|
||||
@Json(name = "domain") val domain: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TransactionScanResponse(
|
||||
@Json(name = "validation") val validation: ValidationResponse,
|
||||
@Json(name = "simulation") val simulation: SimulationResponse,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ValidationResponse(
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "result_type") val resultType: String,
|
||||
)
|
||||
|
|
@ -27,6 +27,7 @@ sealed class ApiConfig {
|
|||
TangemVisaAuth,
|
||||
TangemVisa,
|
||||
TangemCardSdk,
|
||||
BlockAid,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -37,6 +38,7 @@ sealed class ApiConfig {
|
|||
is TangemVisaAuth -> ID.TangemVisaAuth
|
||||
is TangemVisa -> ID.TangemVisa
|
||||
is TangemCardSdk -> ID.TangemCardSdk
|
||||
is BlockAid -> ID.BlockAid
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +46,7 @@ sealed class ApiConfig {
|
|||
internal const val DEBUG_BUILD_TYPE = "debug"
|
||||
internal const val INTERNAL_BUILD_TYPE = "internal"
|
||||
internal const val MOCKED_BUILD_TYPE = "mocked"
|
||||
internal const val EXTERNAL_BUILD_TYPE = "external"
|
||||
internal const val RELEASE_BUILD_TYPE = "release"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
internal class BlockAid(
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.blockaid.io/v0/",
|
||||
headers = buildMap {
|
||||
environmentConfigStorage.getConfigSync().blockAidApiKey?.let { apiKey ->
|
||||
put("X-API-KEY", ProviderSuspend { apiKey })
|
||||
}
|
||||
put("accept", ProviderSuspend { "application/json" })
|
||||
put("content-type", ProviderSuspend { "application/json" })
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -73,6 +73,7 @@ internal class Express(
|
|||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.STAGE
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
|
|
|
|||
|
|
@ -140,6 +140,40 @@ interface TangemTechApi {
|
|||
@GET("stories/{story_id}")
|
||||
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
|
||||
|
||||
// region push notifications
|
||||
@GET("notification/push_notifications_eligible_networks")
|
||||
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>
|
||||
|
||||
@POST("user-wallets/applications/")
|
||||
suspend fun createApplicationId(
|
||||
@Body
|
||||
body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<NotificationApplicationIdResponse>
|
||||
|
||||
@PATCH("user-wallets/applications/{application_id}")
|
||||
suspend fun updatePushTokenForApplicationId(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<String>
|
||||
|
||||
@PATCH("user-wallets/wallets/{wallet_id}/notify")
|
||||
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
// endregion
|
||||
|
||||
// region wallets
|
||||
@PATCH("user-wallets/wallets/{wallet_id}")
|
||||
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
|
||||
@POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
|
||||
suspend fun associateApplicationIdWithWallets(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: List<WalletIdBody>,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("user-wallets/wallets/{wallet_id}")
|
||||
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
val marketsQuoteFields = listOf(
|
||||
"price",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CryptoNetworkResponse(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "networkId") val networkId: String,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NotificationApplicationCreateBody(
|
||||
@Json(name = "pushToken") val pushToken: String,
|
||||
@Json(name = "platform") val platform: String,
|
||||
@Json(name = "device") val device: String,
|
||||
@Json(name = "systemVersion") val systemVersion: String,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "timezone") val timezone: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NotificationApplicationIdResponse(
|
||||
@Json(name = "uid") val appId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NotificationApplicationUpdateBody(
|
||||
@Json(name = "pushToken") val pushToken: String,
|
||||
@Json(name = "systemVersion") val systemVersion: String? = null,
|
||||
@Json(name = "language") val language: String? = null,
|
||||
@Json(name = "timezone") val timezone: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletBody(
|
||||
@Json(name = "notifyStatus") val notifyStatus: String? = null,
|
||||
@Json(name = "name") val name: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletIdBody(
|
||||
@Json(name = "id") val walletId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletResponse(
|
||||
@Json(name = "notifyStatus") val notifyStatus: String? = null,
|
||||
@Json(name = "name") val name: String? = null,
|
||||
@Json(name = "id") val id: String,
|
||||
)
|
||||
|
|
@ -7,10 +7,13 @@ import com.squareup.moshi.JsonClass
|
|||
data class CardActivationRemoteStateResponse(
|
||||
@Json(name = "activation_status") val status: String,
|
||||
@Json(name = "activation_order") val activationOrder: ActivationOrder?,
|
||||
@Json(name = "stepChangeCode") val stepChangeCode: Int?,
|
||||
@Json(name = "updatedAt") val updatedAt: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActivationOrder(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.appcurrency
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store of app currency data model [CurrenciesResponse.Currency]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AppCurrencyResponseStore {
|
||||
|
||||
/** Get flow of [CurrenciesResponse.Currency] */
|
||||
fun get(): Flow<CurrenciesResponse.Currency?>
|
||||
|
||||
/** Get [CurrenciesResponse.Currency] synchronously or null */
|
||||
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.datasource.appcurrency
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Default implementation of [AppCurrencyResponseStore]
|
||||
*
|
||||
* @property appPreferencesStore app preferences store
|
||||
*/
|
||||
internal class DefaultAppCurrencyResponseStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : AppCurrencyResponseStore {
|
||||
|
||||
override fun get(): Flow<CurrenciesResponse.Currency?> {
|
||||
return appPreferencesStore.getObject(PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(): CurrenciesResponse.Currency? {
|
||||
return appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
|
||||
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@ package com.tangem.datasource.asset.loader
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.adapter
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -23,69 +25,63 @@ import javax.inject.Singleton
|
|||
class AssetLoader @Inject constructor(
|
||||
val assetReader: AssetReader,
|
||||
@NetworkMoshi val moshi: Moshi,
|
||||
val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/** 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")
|
||||
suspend inline fun <reified Content> load(fileName: String): Content? = runCatching(dispatchers.io) {
|
||||
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
|
||||
},
|
||||
)
|
||||
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")
|
||||
suspend inline fun <reified V> loadList(fileName: String): List<V> = runCatching(dispatchers.io) {
|
||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||
|
||||
val type = Types.newParameterizedType(List::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<List<V>>(type)
|
||||
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()
|
||||
},
|
||||
)
|
||||
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")
|
||||
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> = runCatching(dispatchers.io) {
|
||||
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)
|
||||
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()
|
||||
},
|
||||
)
|
||||
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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,23 +1,19 @@
|
|||
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()
|
||||
override suspend fun read(fullFileName: String): String {
|
||||
return assetManager.open(fullFileName).bufferedReader()
|
||||
.use(BufferedReader::readText)
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,12 @@ internal object ApiConfigsModule {
|
|||
@IntoSet
|
||||
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
|
||||
return BlockAid(environmentConfigStorage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemCardSdkConfig(): ApiConfig = TangemCardSdk()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.appcurrency.DefaultAppCurrencyResponseStore
|
||||
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -18,4 +21,10 @@ internal object AppCurrencyDataModule {
|
|||
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
|
||||
return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppCurrencyResponseStore(appPreferencesStore: AppPreferencesStore): AppCurrencyResponseStore {
|
||||
return DefaultAppCurrencyResponseStore(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.datasource.di
|
|||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.*
|
||||
import com.tangem.datasource.local.preferences.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -26,6 +25,7 @@ internal object AppPreferencesStoreModule {
|
|||
return AppPreferencesStore(
|
||||
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
|
||||
moshi = moshi,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ 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
|
||||
|
|
@ -17,10 +16,7 @@ internal object AssetReaderModule {
|
|||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAsserReader(
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AssetReader {
|
||||
return AndroidAssetReader(context.assets, dispatchers)
|
||||
fun providesAsserReader(@ApplicationContext context: Context): AssetReader {
|
||||
return AndroidAssetReader(context.assets)
|
||||
}
|
||||
}
|
||||
|
|
@ -51,12 +51,14 @@ class MoshiModule {
|
|||
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
|
||||
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
|
||||
.withSubtype(NFTCollection.Identifier.TON::class.java, "ton")
|
||||
.withSubtype(NFTCollection.Identifier.Solana::class.java, "sol")
|
||||
.withDefaultValue(NFTCollection.Identifier.Unknown),
|
||||
)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(NFTAsset.Identifier::class.java, "bc")
|
||||
.withSubtype(NFTAsset.Identifier.EVM::class.java, "evm")
|
||||
.withSubtype(NFTAsset.Identifier.TON::class.java, "ton")
|
||||
.withSubtype(NFTAsset.Identifier.Solana::class.java, "sol")
|
||||
.withDefaultValue(NFTAsset.Identifier.Unknown),
|
||||
)
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
|
|
@ -282,6 +283,29 @@ internal object NetworkModule {
|
|||
.create(T::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockAidApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
appLogsStore: AppLogsStore,
|
||||
): BlockAidApi {
|
||||
return createApi<BlockAidApi>(
|
||||
id = ApiConfig.ID.BlockAid,
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
).applyTimeoutAnnotations()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T> createApi(
|
||||
id: ApiConfig.ID,
|
||||
moshi: Moshi,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
|
|
@ -26,21 +27,27 @@ internal object QuotesStoreModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQuotesStore(
|
||||
fun providePersistenceQuotesStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): QuotesStore {
|
||||
return DefaultQuotesStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
): DataStore<Map<String, QuotesResponse.Quote>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQuotesStore(persistenceStore: DataStore<Map<String, QuotesResponse.Quote>>): QuotesStore {
|
||||
return DefaultQuotesStore(
|
||||
persistenceStore = persistenceStore,
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ data class EnvironmentConfig(
|
|||
val express: ExpressModel? = null,
|
||||
val devExpress: ExpressModel? = null,
|
||||
val stakeKitApiKey: String? = null,
|
||||
val blockAidApiKey: String? = null,
|
||||
)
|
||||
|
|
@ -23,6 +23,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?,
|
||||
@Json(name = "moralisApiKey") val moralisApiKey: String?,
|
||||
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
|
||||
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -37,8 +37,12 @@ class AppLogsStore @Inject constructor(
|
|||
private val mutex = Mutex()
|
||||
private val zipMutex = Mutex()
|
||||
|
||||
private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
|
||||
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
|
||||
private val logFile by lazy {
|
||||
File(applicationContext.filesDir, PERMITTED_FILE_NAME)
|
||||
}
|
||||
private val logFileZip by lazy {
|
||||
File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
|
||||
}
|
||||
|
||||
private val formatter = DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(2)
|
||||
|
|
@ -55,12 +59,12 @@ class AppLogsStore @Inject constructor(
|
|||
.toFormatter()
|
||||
|
||||
/** Get log file */
|
||||
fun getFile(): File? = if (file.exists()) file else null
|
||||
fun getFile(): File? = if (logFile.exists()) logFile else null
|
||||
|
||||
suspend fun getZipFile(): File? {
|
||||
return zipMutex.withLock {
|
||||
if (file.exists()) {
|
||||
zip(listOf(file), fileZip)
|
||||
if (logFile.exists()) {
|
||||
zip(listOf(logFile), logFileZip)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -98,8 +102,8 @@ class AppLogsStore @Inject constructor(
|
|||
/** Delete deprecated logs if file size exceeds [maxSize] */
|
||||
fun deleteDeprecatedLogs(maxSize: Int) {
|
||||
launchWithLock {
|
||||
if (file.exists() && file.length() > maxSize) {
|
||||
file.delete()
|
||||
if (logFile.exists() && logFile.length() > maxSize) {
|
||||
logFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +121,7 @@ class AppLogsStore @Inject constructor(
|
|||
}
|
||||
|
||||
private fun writeMessage(tag: String, vararg messages: String) {
|
||||
BufferedWriter(FileWriter(file, true)).use { writer ->
|
||||
BufferedWriter(FileWriter(logFile, true)).use { writer ->
|
||||
writer.append(formatter.print(DateTime.now()))
|
||||
writer.append(": $tag ")
|
||||
messages.forEach(writer::append)
|
||||
|
|
@ -126,8 +130,8 @@ class AppLogsStore @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createFileIfNotExist() {
|
||||
if (!file.exists()) {
|
||||
runCatching { file.createNewFile() }
|
||||
if (!logFile.exists()) {
|
||||
runCatching { logFile.createNewFile() }
|
||||
.onFailure(Timber::e)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter<SdkNFTAsset.Identifier,
|
|||
is SdkNFTAsset.Identifier.TON -> NFTAsset.Identifier.TON(
|
||||
tokenAddress = value.tokenAddress,
|
||||
)
|
||||
is SdkNFTAsset.Identifier.Solana -> NFTAsset.Identifier.Solana(
|
||||
tokenAddress = value.tokenAddress,
|
||||
cnft = value.cnft,
|
||||
)
|
||||
is SdkNFTAsset.Identifier.Unknown -> NFTAsset.Identifier.Unknown
|
||||
}
|
||||
|
||||
|
|
@ -24,6 +28,10 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter<SdkNFTAsset.Identifier,
|
|||
is NFTAsset.Identifier.TON -> SdkNFTAsset.Identifier.TON(
|
||||
tokenAddress = value.tokenAddress,
|
||||
)
|
||||
is NFTAsset.Identifier.Solana -> SdkNFTAsset.Identifier.Solana(
|
||||
tokenAddress = value.tokenAddress,
|
||||
cnft = value.cnft,
|
||||
)
|
||||
is NFTAsset.Identifier.Unknown -> SdkNFTAsset.Identifier.Unknown
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
|
|||
is SdkNFTCollection.Identifier.TON -> NFTCollection.Identifier.TON(
|
||||
contractAddress = value.contractAddress,
|
||||
)
|
||||
is SdkNFTCollection.Identifier.Solana -> NFTCollection.Identifier.Solana(
|
||||
collection = value.collection,
|
||||
)
|
||||
is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown
|
||||
}
|
||||
|
||||
|
|
@ -22,6 +25,9 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
|
|||
is NFTCollection.Identifier.TON -> SdkNFTCollection.Identifier.TON(
|
||||
contractAddress = value.contractAddress,
|
||||
)
|
||||
is NFTCollection.Identifier.Solana -> SdkNFTCollection.Identifier.Solana(
|
||||
collection = value.collection,
|
||||
)
|
||||
is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import androidx.datastore.preferences.core.edit
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
/**
|
||||
* Application preferences store.
|
||||
|
|
@ -19,6 +20,7 @@ import com.squareup.moshi.Types
|
|||
*/
|
||||
class AppPreferencesStore(
|
||||
val moshi: Moshi,
|
||||
val dispatchers: CoroutineDispatcherProvider,
|
||||
private val preferencesDataStore: DataStore<Preferences>,
|
||||
) : DataStore<Preferences> by preferencesDataStore {
|
||||
|
||||
|
|
|
|||
|
|
@ -5,23 +5,25 @@ import androidx.datastore.preferences.core.edit
|
|||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** Get flow of nullable data [T] by string [key] */
|
||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.map { preferences ->
|
||||
preferences[key]?.let {
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
} catch (e: JsonDataException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
return flow {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
emitAll(
|
||||
data.map { preferences ->
|
||||
preferences[key]?.let {
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
} catch (e: JsonDataException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -32,16 +34,19 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
|
|||
* @see getObjectList
|
||||
* */
|
||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
|
||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||
return data.map {
|
||||
try {
|
||||
it[key]?.let(adapter::fromJson) ?: default
|
||||
} catch (e: JsonDataException) {
|
||||
default
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
return flow {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
emitAll(
|
||||
data.map {
|
||||
try {
|
||||
it[key]?.let(adapter::fromJson) ?: default
|
||||
} catch (e: JsonDataException) {
|
||||
default
|
||||
}
|
||||
}.distinctUntilChanged(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nullable data [T] by string [key]
|
||||
*
|
||||
|
|
@ -49,26 +54,27 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
|
|||
*
|
||||
* @see getObjectListSync
|
||||
* */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? {
|
||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let {
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
} catch (e: JsonDataException) {
|
||||
null
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? =
|
||||
withContext(dispatchers.io) {
|
||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let {
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
} catch (e: JsonDataException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get data [T] by string [key]. If data is not found, it returns [default] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
||||
key: Preferences.Key<String>,
|
||||
default: T,
|
||||
): T {
|
||||
): T = withContext(dispatchers.io) {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.firstOrNull()
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let {
|
||||
try {
|
||||
|
|
@ -87,37 +93,47 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
|||
*
|
||||
* @see storeObjectList
|
||||
* */
|
||||
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
|
||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
@Suppress("OptionalUnit")
|
||||
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T): Unit =
|
||||
withContext(dispatchers.io) {
|
||||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
|
||||
/** Store list of data [value] by string [key] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) =
|
||||
withContext(dispatchers.io) {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
|
||||
/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */
|
||||
inline fun <reified T> AppPreferencesStore.getObjectList(key: Preferences.Key<String>): Flow<List<T>?> {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged()
|
||||
return flow {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
emitAll(
|
||||
data.map {
|
||||
it[key]?.let(adapter::fromJson)
|
||||
}.distinctUntilChanged(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get list of data [T] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> =
|
||||
withContext(dispatchers.io) {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/** Store map with [String] key and value [V] by string [key] */
|
||||
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
|
||||
key: Preferences.Key<String>,
|
||||
value: Map<String, V>,
|
||||
) {
|
||||
) = withContext(dispatchers.io) {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
|
|
@ -125,37 +141,47 @@ suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
|
|||
}
|
||||
|
||||
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> =
|
||||
withContext(dispatchers.io) {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */
|
||||
inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Flow<Map<String, V>> {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
return flow {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() }
|
||||
emitAll(
|
||||
data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get set of data [T] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> =
|
||||
withContext(dispatchers.io) {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/** Get flow of set of [T] by string [key], or empty if data is not found */
|
||||
inline fun <reified T> AppPreferencesStore.getObjectSet(key: Preferences.Key<String>): Flow<Set<T>> {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
return data.map {
|
||||
it[key]?.let(adapter::fromJson) ?: emptySet()
|
||||
return flow {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
emitAll(
|
||||
data.map {
|
||||
it[key]?.let(adapter::fromJson) ?: emptySet()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,9 @@ internal class SharedPreferencesKeyMigration(
|
|||
private val keyName: String,
|
||||
) : DataMigration<Preferences> {
|
||||
|
||||
private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
|
||||
private val legacyPrefs by lazy {
|
||||
context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
override suspend fun cleanUp() {
|
||||
val sharedPrefsEditor = legacyPrefs.edit()
|
||||
|
|
|
|||
|
|
@ -10,13 +10,24 @@ import com.tangem.utils.extensions.orZero
|
|||
/**
|
||||
* Converter from [QuotesResponse.Quote] to [Quote.Value]
|
||||
*
|
||||
* @property isCached flag that determines whether the quote is a cache
|
||||
* @property source status source
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class QuoteConverter(private val isCached: Boolean) :
|
||||
class QuoteConverter(
|
||||
private val source: StatusSource,
|
||||
) :
|
||||
Converter<Map.Entry<String, QuotesResponse.Quote>, Quote.Value> {
|
||||
|
||||
/**
|
||||
* Secondary constructor
|
||||
*
|
||||
* @param isCached flag that determines whether the quote is a cache
|
||||
*/
|
||||
constructor(isCached: Boolean) : this(
|
||||
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
|
||||
)
|
||||
|
||||
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): Quote.Value {
|
||||
val (currencyId, quote) = value
|
||||
|
||||
|
|
@ -24,7 +35,7 @@ internal class QuoteConverter(private val isCached: Boolean) :
|
|||
rawCurrencyId = CryptoCurrency.RawID(currencyId),
|
||||
fiatRate = quote.price.orZero(),
|
||||
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
|
||||
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance
|
|||
import com.tangem.domain.staking.model.stakekit.YieldBalanceItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class YieldBalanceConverter(
|
||||
class YieldBalanceConverter(
|
||||
private val source: StatusSource,
|
||||
) : Converter<YieldBalanceWrapperDTO, YieldBalance> {
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.datasource.BuildConfig
|
|||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
|
|
@ -90,6 +91,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
is TangemVisaAuth -> createVisaAuthModel()
|
||||
is TangemVisa -> createVisaModel()
|
||||
is TangemCardSdk -> createTangemCardSdkModel()
|
||||
is BlockAid -> createBlockAidSdkModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +102,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.STAGE
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
|
|
@ -115,6 +118,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> "[REDACTED_ENV_URL]"
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> "https://express.tangem.com/v1/"
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
|
|
@ -215,6 +219,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createBlockAidSdkModel(): Model {
|
||||
return Model(
|
||||
id = ApiConfig.ID.BlockAid,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.blockaid.io/v0/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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 com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.every
|
||||
|
|
@ -22,7 +23,11 @@ class AssetLoaderTest {
|
|||
|
||||
private val assetReader = mockk<AssetReader>()
|
||||
private val moshi = mockk<Moshi>()
|
||||
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
|
||||
private val assetLoader = AssetLoader(
|
||||
assetReader = assetReader,
|
||||
moshi = moshi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun load() = runTest {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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
|
||||
|
|
@ -15,7 +14,7 @@ import java.io.IOException
|
|||
internal class AndroidAssetReaderTest {
|
||||
|
||||
private val assetManager = mockk<AssetManager>()
|
||||
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
|
||||
private val assetReader = AndroidAssetReader(assetManager)
|
||||
|
||||
@Test
|
||||
fun read_content() = runTest {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue