Updated on 2026-08-14
This commit is contained in:
commit
a0c9ace219
1607 changed files with 40580 additions and 16776 deletions
|
|
@ -131,6 +131,12 @@ sealed class AnalyticsParam {
|
|||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Send&Swap"), TxData
|
||||
|
||||
data class Earning(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Earning"), TxData
|
||||
}
|
||||
|
||||
sealed interface TxData {
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ sealed class Basic(
|
|||
batch: String,
|
||||
signInType: SignInType,
|
||||
walletsCount: String,
|
||||
isImported: Boolean,
|
||||
hasBackup: Boolean?,
|
||||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.CURRENCY, currency.value)
|
||||
put(AnalyticsParam.BATCH, batch)
|
||||
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
|
||||
put("Sign in type", signInType.name)
|
||||
put("Wallets Count", walletsCount)
|
||||
if (hasBackup != null) {
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@
|
|||
"version": "5.21.0"
|
||||
},
|
||||
{
|
||||
"name": "scroll",
|
||||
"name": "zklink",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "zklink",
|
||||
"name": "scroll",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
@ -23,26 +23,14 @@
|
|||
"name": "USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "SEND_VIA_SWAP_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_REDESIGN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "SEND_REDESIGN_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NFT_SEND_REDESIGN_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -53,7 +41,7 @@
|
|||
},
|
||||
{
|
||||
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.30.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ internal class DevFeatureTogglesManager(
|
|||
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
private var fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
||||
private val fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
init {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ internal class DefaultVersionProvider @Inject constructor(
|
|||
|
||||
private fun getVersionName(): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager
|
||||
.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.PackageInfoFlags.of(0),
|
||||
)
|
||||
.versionName!!
|
||||
requireNotNull(
|
||||
context
|
||||
.packageManager
|
||||
.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0))
|
||||
.versionName,
|
||||
)
|
||||
} else {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName!!
|
||||
requireNotNull(context.packageManager.getPackageInfo(context.packageName, 0).versionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.walletConnect.models)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.visa.models)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
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.EvmTransactionBulkScanRequest
|
||||
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.GasEstimationResponse
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
|
||||
import retrofit2.http.Body
|
||||
|
|
@ -19,4 +21,7 @@ interface BlockAidApi {
|
|||
|
||||
@POST("solana/message/scan")
|
||||
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse
|
||||
|
||||
@POST("evm/transaction-bulk/scan")
|
||||
suspend fun scanEvmTransactionBulk(@Body request: EvmTransactionBulkScanRequest): List<GasEstimationResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.request
|
||||
|
||||
enum class BlockAidScanOptions(val value: String) {
|
||||
Simulation("simulation"),
|
||||
Validation("validation"),
|
||||
GasEstimation("gas_estimation"),
|
||||
}
|
||||
|
|
@ -14,9 +14,26 @@ data class EvmTransactionScanRequest(
|
|||
@Json(name = "metadata") val metadata: TransactionMetadata,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class EvmTransactionBulkScanRequest(
|
||||
@Json(name = "chain") val chain: String,
|
||||
@Json(name = "options") val options: List<String>,
|
||||
@Json(name = "metadata") val metadata: TransactionMetadata,
|
||||
@Json(name = "data") val data: List<Data>,
|
||||
@Json(name = "aggregated") val aggregated: Boolean = false,
|
||||
)
|
||||
|
||||
@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: List<Map<String, String>>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "from") val from: String,
|
||||
@Json(name = "to") val to: String,
|
||||
@Json(name = "data") val data: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.common.blockaid.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GasEstimationResponse(
|
||||
@Json(name = "gas_estimation") val gasEstimation: GasEstimationItem,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GasEstimationItem(
|
||||
@Json(name = "estimate") val estimate: String,
|
||||
)
|
||||
|
|
@ -26,6 +26,7 @@ sealed class ApiConfig {
|
|||
StakeKit,
|
||||
TangemPay,
|
||||
BlockAid,
|
||||
YieldSupply,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -35,6 +36,7 @@ sealed class ApiConfig {
|
|||
is StakeKit -> ID.StakeKit
|
||||
is TangemPay -> ID.TangemPay
|
||||
is BlockAid -> ID.BlockAid
|
||||
is YieldSupply -> ID.YieldSupply
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ enum class ApiEnvironment {
|
|||
@Json(name = "DEV_2")
|
||||
DEV_2,
|
||||
|
||||
@Json(name = "DEV_3")
|
||||
DEV_3,
|
||||
|
||||
@Json(name = "STAGE")
|
||||
STAGE,
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ internal class Express(
|
|||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createDevEnvironment(),
|
||||
createDev2Environment(),
|
||||
createDev3Environment(),
|
||||
createStageEnvironment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
|
|
@ -60,6 +61,12 @@ internal class Express(
|
|||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createDev3Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV_3,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ internal class TangemPay(
|
|||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.paera.com/bff/",
|
||||
baseUrl = "https://api.us.paera.com/bff/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ package com.tangem.datasource.api.common.config
|
|||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/** TangemTech [ApiConfig] */
|
||||
internal class TangemTech(
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val authProvider: AuthProvider,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
|
|
@ -38,29 +41,42 @@ internal class TangemTech(
|
|||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
headers = createHeaders(ApiEnvironment.DEV),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
headers = createHeaders(ApiEnvironment.STAGE),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
headers = createHeaders(ApiEnvironment.MOCK),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem.org/",
|
||||
headers = createHeaders(),
|
||||
headers = createHeaders(ApiEnvironment.PROD),
|
||||
)
|
||||
|
||||
private fun createHeaders() = buildMap {
|
||||
private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap {
|
||||
put(key = "api-key", value = ProviderSuspend { getApiKey(apiEnvironment) })
|
||||
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
|
||||
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
|
||||
}
|
||||
|
||||
private fun getApiKey(apiEnvironment: ApiEnvironment): String {
|
||||
return when (apiEnvironment) {
|
||||
ApiEnvironment.MOCK,
|
||||
ApiEnvironment.DEV,
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
|
||||
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
|
||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
|
||||
} ?: error("No tangem tech api config provided")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/** YieldSupply [ApiConfig] */
|
||||
internal class YieldSupply(
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val authProvider: AuthProvider,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createDevEnvironment(),
|
||||
createStageEnvironment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(ApiEnvironment.DEV),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(ApiEnvironment.STAGE),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(ApiEnvironment.MOCK),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://yield.tangem.org/",
|
||||
headers = createHeaders(ApiEnvironment.PROD),
|
||||
)
|
||||
|
||||
private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap {
|
||||
put(key = "api-key", value = ProviderSuspend {
|
||||
getApiKey(apiEnvironment)
|
||||
})
|
||||
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
|
||||
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
|
||||
}
|
||||
|
||||
private fun getApiKey(apiEnvironment: ApiEnvironment): String {
|
||||
return when (apiEnvironment) {
|
||||
ApiEnvironment.MOCK,
|
||||
ApiEnvironment.DEV,
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
-> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
|
||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
|
||||
} ?: error("No tangem tech api config provided")
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
interface ApiConfigsManager {
|
||||
|
||||
/** Flag that determines whether the manager is initialized */
|
||||
val isInitialized: StateFlow<Boolean>
|
||||
val initializedState: StateFlow<Boolean>
|
||||
|
||||
/** Initialize resources */
|
||||
fun initialize()
|
||||
|
|
|
|||
|
|
@ -26,27 +26,27 @@ internal class DevApiConfigsManager(
|
|||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
field = MutableStateFlow(value = getInitialConfigs())
|
||||
field = MutableStateFlow(value = getInitialConfigs())
|
||||
|
||||
override val isInitialized: StateFlow<Boolean>
|
||||
field = MutableStateFlow(value = false)
|
||||
override val initializedState: StateFlow<Boolean>
|
||||
field = MutableStateFlow(value = false)
|
||||
|
||||
override fun initialize() {
|
||||
isInitialized.value = false
|
||||
initializedState.value = false
|
||||
|
||||
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.distinctUntilChanged()
|
||||
.onEach { savedEnvironments ->
|
||||
val apiConfigs = configs.value
|
||||
|
||||
configs.value = apiConfigs.mapValues {
|
||||
val (config, currentEnvironment) = it
|
||||
configs.value = apiConfigs.mapValues { entry ->
|
||||
val (config, currentEnvironment) = entry
|
||||
|
||||
savedEnvironments[config.id.name] ?: currentEnvironment
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
isInitialized.value = true
|
||||
if (!initializedState.value) {
|
||||
initializedState.value = true
|
||||
}
|
||||
|
||||
notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ internal class MockApiConfigsManager(
|
|||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
field = MutableStateFlow(value = getInitialConfigs())
|
||||
field = MutableStateFlow(value = getInitialConfigs())
|
||||
|
||||
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
override val initializedState: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ internal class MockApiConfigsManager(
|
|||
super.addListener(listener)
|
||||
|
||||
configs
|
||||
.map { it.entries.firstOrNull { it.key.id == listener.id } }
|
||||
.map { map -> map.entries.firstOrNull { it.key.id == listener.id } }
|
||||
.filterNotNull()
|
||||
.onEach { (apiConfig, currentEnvironment) ->
|
||||
listener.onChange(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ abstract class MutableApiConfigsManager : ApiConfigsManager {
|
|||
* These listeners are notified whenever an environment change occurs.
|
||||
*/
|
||||
protected val registerListeners: Set<ApiConfigEnvChangeListener>
|
||||
field = mutableSetOf<ApiConfigEnvChangeListener>()
|
||||
field = mutableSetOf<ApiConfigEnvChangeListener>()
|
||||
|
||||
/** Change api environment [environment] by [id] */
|
||||
abstract suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal class ProdApiConfigsManager(
|
|||
private val apiConfigs: ApiConfigs,
|
||||
) : ApiConfigsManager {
|
||||
|
||||
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
override val initializedState: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
|
||||
override fun initialize() = Unit
|
||||
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ sealed class ApiResponseError : Exception() {
|
|||
|
||||
/** Represents a network error, typically when there's no connectivity. */
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data object NetworkException : ApiResponseError() {
|
||||
private fun readResolve(): Any = NetworkException
|
||||
class NetworkException : ApiResponseError() {
|
||||
private fun readResolve(): Any = NetworkException()
|
||||
}
|
||||
|
||||
/** Represents a timeout error, typically when the server takes too long to respond. */
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data object TimeoutException : ApiResponseError() {
|
||||
private fun readResolve(): Any = TimeoutException
|
||||
class TimeoutException : ApiResponseError() {
|
||||
private fun readResolve(): Any = TimeoutException()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -87,5 +87,5 @@ sealed class ApiResponseError : Exception() {
|
|||
*
|
||||
* @property cause The exception that caused this error.
|
||||
*/
|
||||
data class UnknownException(override val cause: Throwable) : ApiResponseError()
|
||||
class UnknownException(override val cause: Throwable) : ApiResponseError()
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
const val IF_NONE_MATCH_HEADER = "IfNoneMatch"
|
||||
const val ETAG_HEADER = "etag"
|
||||
|
|
@ -58,10 +58,10 @@ internal fun Throwable.toApiError(): ApiResponseError = when (this) {
|
|||
is ConnectException,
|
||||
is UnknownHostException,
|
||||
is SSLHandshakeException,
|
||||
-> ApiResponseError.NetworkException
|
||||
-> ApiResponseError.NetworkException()
|
||||
is TimeoutException,
|
||||
is TimeoutCancellationException,
|
||||
is SocketTimeoutException,
|
||||
-> ApiResponseError.TimeoutException
|
||||
-> ApiResponseError.TimeoutException()
|
||||
else -> ApiResponseError.UnknownException(cause = this)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class Asset(
|
||||
@Json(name = "contractAddress")
|
||||
val contractAddress: String,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ data class TokenMarketInfoResponse(
|
|||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class Network(
|
||||
@Json(name = "network_id")
|
||||
val networkId: String,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class OnrampCountryDTO(
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ import retrofit2.http.Body
|
|||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TangemPayApi {
|
||||
|
||||
|
|
@ -107,6 +110,13 @@ interface TangemPayApi {
|
|||
@Query("offset") offset: Int,
|
||||
): ApiResponse<VisaTxHistoryResponse>
|
||||
|
||||
@GET("v1/customer/transactions")
|
||||
suspend fun getTangemPayTxHistory(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("cursor") cursor: String?,
|
||||
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
|
||||
): ApiResponse<TangemPayTxHistoryResponse>
|
||||
|
||||
@GET("v1/customer/kyc")
|
||||
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
|
||||
|
||||
|
|
@ -115,4 +125,25 @@ interface TangemPayApi {
|
|||
|
||||
@POST("v1/deeplink/validate")
|
||||
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
|
||||
|
||||
@GET("v1/order/{order_id}")
|
||||
suspend fun getOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("order_id") orderId: String,
|
||||
): ApiResponse<OrderResponse>
|
||||
|
||||
@POST("v1/order")
|
||||
suspend fun createOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: OrderRequest,
|
||||
): ApiResponse<OrderResponse>
|
||||
|
||||
@GET("v1/customer/balance")
|
||||
suspend fun getCardBalance(@Header("Authorization") authHeader: String): ApiResponse<CardBalanceResponse>
|
||||
|
||||
@POST("v1/customer/card/details")
|
||||
suspend fun revealCardDetails(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: CardDetailsRequest,
|
||||
): ApiResponse<CardDetailsResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardDetailsRequest(
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OrderRequest(
|
||||
@Json(name = "wallet_address") val walletAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class CardBalanceResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
data class CardDetailsResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "expiration_month") val expirationMonth: String,
|
||||
@Json(name = "expiration_year") val expirationYear: String,
|
||||
@Json(name = "emboss_name") val embossName: String,
|
||||
@Json(name = "card_type") val cardType: String,
|
||||
@Json(name = "card_status") val cardStatus: String,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String,
|
||||
@Json(name = "pan") val pan: Secret,
|
||||
@Json(name = "cvv") val cvv: Secret,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Secret(
|
||||
@Json(name = "secret") val secret: String,
|
||||
@Json(name = "iv") val iv: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerMeResponse(
|
||||
|
|
@ -16,6 +17,9 @@ data class CustomerMeResponse(
|
|||
@Json(name = "product_instance") val productInstance: ProductInstance?,
|
||||
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
|
||||
@Json(name = "kyc") val kyc: Kyc?,
|
||||
@Json(name = "depositAddress") val depositAddress: String?,
|
||||
@Json(name = "card") val card: Card?,
|
||||
@Json(name = "balance") val balance: Balance?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -45,4 +49,25 @@ data class CustomerMeResponse(
|
|||
@Json(name = "review_answer") val reviewAnswer: String,
|
||||
@Json(name = "created_at") val createdAt: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Card(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "expiration_month") val expirationMonth: Int,
|
||||
@Json(name = "expiration_year") val expirationYear: Int,
|
||||
@Json(name = "emboss_name") val embossName: String,
|
||||
@Json(name = "card_type") val cardType: String,
|
||||
@Json(name = "card_status") val cardStatus: String,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Balance(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OrderResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "customer_id") val customerId: String?,
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "step") val step: String?,
|
||||
@Json(name = "data") val data: Data,
|
||||
@Json(name = "step_change_code") val stepChangeCode: Int?,
|
||||
@Json(name = "created_at") val createdAt: String?,
|
||||
@Json(name = "updated_at") val updatedAt: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "specification_name") val specificationName: String?,
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
@Json(name = "emboss_name") val embossName: String?,
|
||||
@Json(name = "product_instance_id") val productInstanceId: String?,
|
||||
@Json(name = "payment_account_id") val paymentAccountId: String?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayTxHistoryResponse(
|
||||
@Json(name = "error") val error: String?,
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "transactions") val transactions: List<Transaction>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Transaction(
|
||||
@Json(name = "id") val id: String, // UUID (used as cursor for pagination)
|
||||
@Json(name = "type") val type: String, // "SPEND", "COLLATERAL", "PAYMENT", "FEE"
|
||||
@Json(name = "spend") val spend: Spend? = null,
|
||||
@Json(name = "collateral") val collateral: Collateral? = null,
|
||||
@Json(name = "payment") val payment: Payment? = null,
|
||||
@Json(name = "fee") val fee: Fee? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class Spend(
|
||||
@Json(name = "amount") val amount: BigDecimal,
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "local_amount") val localAmount: BigDecimal? = null,
|
||||
@Json(name = "local_currency") val localCurrency: String? = null,
|
||||
@Json(name = "authorized_amount") val authorizedAmount: BigDecimal? = null,
|
||||
@Json(name = "authorization_method") val authorizationMethod: String? = null,
|
||||
@Json(name = "memo") val memo: String? = null,
|
||||
@Json(name = "receipt") val receipt: Boolean,
|
||||
@Json(name = "merchant_name") val merchantName: String,
|
||||
@Json(name = "merchant_category") val merchantCategory: String,
|
||||
@Json(name = "merchant_category_code") val merchantCategoryCode: String,
|
||||
@Json(name = "merchant_id") val merchantId: String? = null,
|
||||
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
|
||||
@Json(name = "enriched_merchant_name") val enrichedMerchantName: String? = null,
|
||||
@Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null,
|
||||
@Json(name = "card_id") val cardId: String? = null,
|
||||
@Json(name = "card_type") val cardType: String? = null,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "declined_reason") val declinedReason: String? = null,
|
||||
@Json(name = "authorized_at") val authorizedAt: DateTime,
|
||||
@Json(name = "posted_at") val postedAt: DateTime?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Collateral(
|
||||
@Json(name = "amount") val amount: BigDecimal,
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "memo") val memo: String? = null,
|
||||
@Json(name = "chain_id") val chainId: Long,
|
||||
@Json(name = "wallet_address") val walletAddress: String,
|
||||
@Json(name = "transaction_hash") val transactionHash: String,
|
||||
@Json(name = "posted_at") val postedAt: DateTime?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Payment(
|
||||
@Json(name = "amount") val amount: BigDecimal,
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "memo") val memo: String? = null,
|
||||
@Json(name = "chain_id") val chainId: Long? = null,
|
||||
@Json(name = "wallet_address") val walletAddress: String? = null,
|
||||
@Json(name = "transaction_hash") val transactionHash: String? = null,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "posted_at") val postedAt: DateTime,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Fee(
|
||||
@Json(name = "amount") val amount: BigDecimal,
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "description") val description: String? = null,
|
||||
@Json(name = "posted_at") val postedAt: DateTime,
|
||||
)
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ interface StakeKitApi {
|
|||
@Query("preferredValidatorsOnly") preferredValidatorsOnly: Boolean? = null,
|
||||
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null,
|
||||
@Query("type") type: YieldType? = null,
|
||||
@Query("yieldId") yieldId: String? = null,
|
||||
@Query("revenueOption") revenueOption: RevenueOption? = null,
|
||||
@Query("page") page: Int? = null,
|
||||
@Query("network") network: String? = null,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ data class ActionRequestBody(
|
|||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class ActionRequestBodyArgs(
|
||||
@Json(name = "amount")
|
||||
val amount: String,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class ConstructTransactionRequestBody(
|
||||
@Json(name = "gasArgs")
|
||||
val gasArgs: GasArgs? = null,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class AddressArgumentDTO(
|
||||
@Json(name = "required")
|
||||
val required: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("BooleanPropertyNaming")
|
||||
|
||||
package com.tangem.datasource.api.stakekit.models.response.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("BooleanPropertyNaming")
|
||||
|
||||
package com.tangem.datasource.api.stakekit.models.response.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
|
@ -198,6 +200,7 @@ data class YieldDTO(
|
|||
enum class RewardTypeDTO {
|
||||
@Json(name = "apy")
|
||||
APY, // compound rate
|
||||
|
||||
@Json(name = "apr")
|
||||
APR, // simple rate,
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ interface TangemTechApi {
|
|||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") eTag: String,
|
||||
@Body body: SaveWalletAccountsResponse,
|
||||
): ApiResponse<Unit>
|
||||
): ApiResponse<GetWalletAccountsResponse>
|
||||
|
||||
@GET("/v1/wallets/{walletId}/accounts/archived")
|
||||
suspend fun getWalletArchivedAccounts(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.datasource.api.tangemTech
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldModuleStatusResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface YieldSupplyApi {
|
||||
|
||||
@GET("api/v1/yield/markets")
|
||||
suspend fun getYieldMarkets(@Query("chainId") chainId: String? = null): ApiResponse<YieldMarketsResponse>
|
||||
|
||||
@GET("api/v1/yield/token/{chainId}/{tokenAddress}")
|
||||
suspend fun getYieldTokenStatus(
|
||||
@Path("chainId") chainId: Int,
|
||||
@Path("tokenAddress") tokenAddress: String,
|
||||
): ApiResponse<YieldSupplyMarketTokenDto>
|
||||
|
||||
@GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart")
|
||||
suspend fun getYieldTokenChart(
|
||||
@Path("chainId") chainId: Int,
|
||||
@Path("tokenAddress") tokenAddress: String,
|
||||
@Query("window") window: String? = null,
|
||||
@Query("bucketSizeDays") bucketSizeDays: Int? = null,
|
||||
): ApiResponse<YieldTokenChartResponse>
|
||||
|
||||
@POST("api/v1/module/activate")
|
||||
suspend fun activateYieldModule(
|
||||
@Body body: YieldSupplyChangeTokenStatusBody,
|
||||
): ApiResponse<YieldModuleStatusResponse>
|
||||
|
||||
@POST("api/v1/module/deactivate")
|
||||
suspend fun deactivateYieldModule(
|
||||
@Body body: YieldSupplyChangeTokenStatusBody,
|
||||
): ApiResponse<YieldModuleStatusResponse>
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("BooleanPropertyNaming")
|
||||
|
||||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
|
@ -12,12 +14,13 @@ data class CoinsResponse(
|
|||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class Coin(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "symbol") val symbol: String,
|
||||
@Json(name = "active") val active: Boolean,
|
||||
@Json(name = "networks") val networks: List<Network> = listOf(),
|
||||
@Json(name = "networks") val networks: List<Network> = emptyList(),
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class CreateUserNetworkAccountResponse(
|
||||
@Json(name = "status") val status: Boolean,
|
||||
@Json(name = "data") val data: AccountCreated,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,5 @@ data class SeedPhraseNotificationDTO(val status: Status) {
|
|||
|
||||
@Json(name = "accepted")
|
||||
ACCEPTED,
|
||||
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
import com.tangem.common.extensions.calculateHashCode
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class UserTokensResponse(
|
||||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class WalletBody(
|
||||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||
@Json(name = "name") val name: String? = null,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class WalletResponse(
|
||||
@Json(name = "notifyStatus") val notifyStatus: Boolean,
|
||||
@Json(name = "id") val id: String,
|
||||
|
|
|
|||
|
|
@ -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 YieldMarketsResponse(
|
||||
@Json(name = "tokens") val marketDtos: List<YieldSupplyMarketTokenDto>,
|
||||
@Json(name = "lastUpdatedAt") val lastUpdated: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class YieldModuleStatusResponse(
|
||||
@Json(name = "tokenAddress") val tokenAddress: String,
|
||||
@Json(name = "chainId") val chainId: Int,
|
||||
@Json(name = "isActive") val isActive: Boolean,
|
||||
@Json(name = "activatedAt") val activatedAt: String?,
|
||||
@Json(name = "deactivatedAt") val deactivatedAt: 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 YieldSupplyChangeTokenStatusBody(
|
||||
@Json(name = "tokenAddress") val tokenAddress: String,
|
||||
@Json(name = "chainId") val chainId: Int,
|
||||
@Json(name = "userAddress") val userAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class YieldSupplyMarketTokenDto(
|
||||
@Json(name = "tokenAddress") val tokenAddress: String? = null,
|
||||
@Json(name = "tokenSymbol") val tokenSymbol: String? = null,
|
||||
@Json(name = "tokenName") val tokenName: String? = null,
|
||||
@Json(name = "apy") val apy: BigDecimal? = null,
|
||||
@Json(name = "isActive") val isActive: Boolean? = null,
|
||||
@Json(name = "chainId") val chainId: Int? = null,
|
||||
@Json(name = "maxFeeNative") val maxFeeNative: String? = null,
|
||||
@Json(name = "maxFeeUSD") val maxFeeUSD: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class YieldTokenChartResponse(
|
||||
@Json(name = "underlying") val underlying: String,
|
||||
@Json(name = "market") val market: String,
|
||||
@Json(name = "bucketSizeDays") val bucketSizeDays: Int,
|
||||
@Json(name = "period") val period: String,
|
||||
@Json(name = "data") val data: List<DataPoint>,
|
||||
@Json(name = "avr") val averageApy: BigDecimal,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DataPoint(
|
||||
@Json(name = "bucketIndex") val bucketIndex: Int,
|
||||
@Json(name = "avgApy") val avgApy: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,8 +2,37 @@ package com.tangem.datasource.api.tangemTech.models.account
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.utils.SerializeNulls
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SaveWalletAccountsResponse(
|
||||
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
|
||||
)
|
||||
@Json(name = "accounts") val accounts: List<AccountDTO>,
|
||||
) {
|
||||
|
||||
@SerializeNulls
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountDTO(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String?,
|
||||
@Json(name = "derivation") val derivationIndex: Int,
|
||||
@Json(name = "icon") val icon: String,
|
||||
@Json(name = "iconColor") val iconColor: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
operator fun invoke(accounts: List<WalletAccountDTO>): SaveWalletAccountsResponse {
|
||||
return SaveWalletAccountsResponse(
|
||||
accounts = accounts.map { accountDto ->
|
||||
AccountDTO(
|
||||
id = accountDto.id,
|
||||
name = accountDto.name,
|
||||
derivationIndex = accountDto.derivationIndex,
|
||||
icon = accountDto.icon,
|
||||
iconColor = accountDto.iconColor,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,8 +39,8 @@ class AssetLoader @Inject constructor(
|
|||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
|
||||
parsedConfig
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to load config [$fileName] from assets")
|
||||
onFailure = { throwable ->
|
||||
Timber.e(throwable, "Failed to load config [$fileName] from assets")
|
||||
null
|
||||
},
|
||||
)
|
||||
|
|
@ -59,8 +59,8 @@ class AssetLoader @Inject constructor(
|
|||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
|
||||
parsedConfig.orEmpty()
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to load config [$fileName] from assets")
|
||||
onFailure = { throwable ->
|
||||
Timber.e(throwable, "Failed to load config [$fileName] from assets")
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
|
|
@ -79,8 +79,8 @@ class AssetLoader @Inject constructor(
|
|||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
|
||||
parsedConfig.orEmpty()
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to load config [$fileName] from assets")
|
||||
onFailure = { throwable ->
|
||||
Timber.e(throwable, "Failed to load config [$fileName] from assets")
|
||||
emptyMap()
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,10 +42,26 @@ internal object ApiConfigsModule {
|
|||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemTechConfig(
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
authProvider: AuthProvider,
|
||||
appInfoProvider: AppInfoProvider,
|
||||
): ApiConfig = TangemTech(
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
appVersionProvider = appVersionProvider,
|
||||
authProvider = authProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideYieldSupplyConfig(
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
authProvider: AuthProvider,
|
||||
appInfoProvider: AppInfoProvider,
|
||||
): ApiConfig = YieldSupply(
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
appVersionProvider = appVersionProvider,
|
||||
authProvider = authProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import com.tangem.common.json.MoshiJsonConverter
|
|||
import com.tangem.datasource.api.common.adapter.*
|
||||
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.datasource.utils.SerializeNullsFactory
|
||||
import com.tangem.domain.models.scan.serialization.*
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -28,6 +27,7 @@ class MoshiModule {
|
|||
@NetworkMoshi
|
||||
fun provideNetworkMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(SerializeNullsFactory)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
|
||||
.withSubtype(ProviderModel.Public::class.java, "public")
|
||||
|
|
@ -38,8 +38,8 @@ class MoshiModule {
|
|||
.add(BigIntegerAdapter())
|
||||
.add(LocalDateAdapter())
|
||||
.add(DateTimeAdapter())
|
||||
.add(VisaActivationRemoteState.jsonAdapter)
|
||||
.add(VisaCardActivationStatus.jsonAdapter)
|
||||
// .add(VisaActivationRemoteState.jsonAdapter)
|
||||
// .add(VisaCardActivationStatus.jsonAdapter)
|
||||
.add(
|
||||
NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java)
|
||||
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
|
||||
|
|
@ -84,8 +84,8 @@ class MoshiModule {
|
|||
val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters()
|
||||
|
||||
return Moshi.Builder().apply {
|
||||
add(VisaActivationRemoteState.jsonAdapter)
|
||||
add(VisaCardActivationStatus.jsonAdapter)
|
||||
// add(VisaActivationRemoteState.jsonAdapter)
|
||||
// add(VisaCardActivationStatus.jsonAdapter)
|
||||
adapters.forEach { this.add(it) }
|
||||
typedAdapters.forEach { add(it.key, it.value) }
|
||||
addLast(KotlinJsonAdapterFactory())
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.datasource.api.onramp.OnrampApi
|
|||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
|
||||
import com.tangem.datasource.di.utils.RetrofitApiBuilder
|
||||
import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -88,6 +89,15 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyApi(retrofitApiBuilder: RetrofitApiBuilder): YieldSupplyApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.YieldSupply,
|
||||
applyTimeoutAnnotations = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemTechMarketsApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechMarketsApi {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.datasource.di
|
|||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,4 +22,12 @@ internal object TxHistoryItemsStoreModule {
|
|||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
|
||||
return DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
|
||||
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
|
||||
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object YieldSupplyModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldMarketsStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): YieldMarketsStore {
|
||||
return DefaultYieldMarketsStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = listTypes<YieldSupplyMarketTokenDto>(),
|
||||
defaultValue = emptyList(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +192,7 @@ internal class RetrofitApiBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UseEmptyCounterpart")
|
||||
private companion object {
|
||||
|
||||
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
|
||||
|
|
|
|||
|
|
@ -72,15 +72,16 @@ internal class DefaultExpressServiceLoader @Inject constructor(
|
|||
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
|
||||
}
|
||||
|
||||
@Suppress("SuspendFunWithFlowReturnType")
|
||||
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
|
||||
val initializationStatus = initializationStatuses.value.get(key = userWalletId)
|
||||
val initializationStatus = initializationStatuses.value[userWalletId]
|
||||
if (initializationStatus != null) return initializationStatus
|
||||
|
||||
val cached = expressAssetsStore.getSyncOrNull(userWalletId)
|
||||
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
|
||||
|
||||
initializationStatuses.update {
|
||||
it.toMutableMap().apply {
|
||||
initializationStatuses.update { statuses ->
|
||||
statuses.toMutableMap().apply {
|
||||
put(key = userWalletId, value = default)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ internal class DefaultAvailableAppCurrenciesStore(
|
|||
|
||||
override suspend fun store(response: CurrenciesResponse) {
|
||||
val currencies = response.currencies
|
||||
.map {
|
||||
it.copy(
|
||||
iconSmallUrl = response.imageHost?.plus(IMAGE_SMALL)?.format(it.id),
|
||||
iconMediumUrl = response.imageHost?.plus(IMAGE_MEDIUM)?.format(it.id),
|
||||
.map { currency ->
|
||||
currency.copy(
|
||||
iconSmallUrl = response.imageHost?.plus(IMAGE_SMALL)?.format(currency.id),
|
||||
iconMediumUrl = response.imageHost?.plus(IMAGE_MEDIUM)?.format(currency.id),
|
||||
)
|
||||
}
|
||||
.associateBy(CurrenciesResponse.Currency::code)
|
||||
|
|
|
|||
|
|
@ -17,4 +17,9 @@ data class EnvironmentConfig(
|
|||
val devExpress: ExpressModel? = null,
|
||||
val stakeKitApiKey: String? = null,
|
||||
val blockAidApiKey: String? = null,
|
||||
val tangemApiKey: String? = null,
|
||||
val tangemApiKeyDev: String? = null,
|
||||
val tangemApiKeyStage: String? = null,
|
||||
val yieldModuleApiKey: String? = null,
|
||||
val yieldModuleApiKeyDev: String? = null,
|
||||
)
|
||||
|
|
@ -26,6 +26,11 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
tangemApiKey = value.tangemApiKey,
|
||||
tangemApiKeyDev = value.tangemApiKeyDev,
|
||||
tangemApiKeyStage = value.tangemApiKeyStage,
|
||||
yieldModuleApiKey = value.yieldModuleApiKey,
|
||||
yieldModuleApiKeyDev = value.yieldModuleApiKeyDev,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,8 +40,12 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "moralisApiKey") val moralisApiKey: String?,
|
||||
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
|
||||
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
|
||||
@Json(name = "tangemApiKey") val tangemApiKey: String?,
|
||||
@Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?,
|
||||
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
|
||||
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
|
||||
@Json(name = "yieldModuleApiKey") val yieldModuleApiKey: String?,
|
||||
@Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?,
|
||||
@Json(name = "blinkApiKey") val blinkApiKey: String?,
|
||||
@Json(name = "tatumApiKey") val tatumApiKey: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import java.math.BigDecimal
|
|||
/**
|
||||
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
|
||||
*
|
||||
* @see [com.tangem.domain.tokens.model.NetworkStatus]
|
||||
* @see [com.tangem.domain.models.network.NetworkStatus]
|
||||
*/
|
||||
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
|
||||
sealed interface NetworkStatusDM {
|
||||
|
|
@ -41,8 +41,8 @@ sealed interface NetworkStatusDM {
|
|||
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
|
||||
@Json(name = "selected_address") override val selectedAddress: String,
|
||||
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
|
||||
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
|
||||
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: Map<String, YieldSupplyStatus?> = emptyMap(),
|
||||
@Json(name = "amounts") val amounts: List<CurrencyAmount>,
|
||||
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: List<YieldSupplyStatus>,
|
||||
) : NetworkStatusDM
|
||||
|
||||
/**
|
||||
|
|
@ -107,10 +107,45 @@ sealed interface NetworkStatusDM {
|
|||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CurrencyAmount(
|
||||
@Json(name = "id") val id: CurrencyId,
|
||||
@Json(name = "amount") val amount: BigDecimal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class YieldSupplyStatus(
|
||||
@Json(name = "id") val id: CurrencyId,
|
||||
@Json(name = "is_active") val isActive: Boolean,
|
||||
@Json(name = "is_initialized") val isInitialized: Boolean,
|
||||
@Json(name = "is_allowed_to_spend") val isAllowedToSpend: Boolean,
|
||||
@Json(name = "effective_protocol_balance") val effectiveProtocolBalance: BigDecimal? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CurrencyId(
|
||||
@Json(name = "value") val value: String,
|
||||
) {
|
||||
|
||||
companion object Companion {
|
||||
|
||||
const val CONTRACT_ADDRESS_DELIMITER = '\u2693' // ⚓
|
||||
|
||||
fun createCoinId(coinId: String): CurrencyId {
|
||||
return CurrencyId(value = coinId)
|
||||
}
|
||||
|
||||
fun createTokenId(rawTokenId: String?, contractAddress: String): CurrencyId {
|
||||
return CurrencyId(
|
||||
value = buildString {
|
||||
if (rawTokenId != null) {
|
||||
append(rawTokenId)
|
||||
}
|
||||
append(CONTRACT_ADDRESS_DELIMITER)
|
||||
append(contractAddress)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,22 +26,22 @@ object NFTSdkAssetConverter : TwoWayConverter<Pair<Network, SdkNFTAsset>, NFTAss
|
|||
amount = asset.amount,
|
||||
decimals = asset.decimals,
|
||||
salePrice = salePrice,
|
||||
rarity = asset.rarity?.let {
|
||||
rarity = asset.rarity?.let { rarity ->
|
||||
NFTAsset.Rarity(
|
||||
rank = it.rank,
|
||||
label = it.label,
|
||||
rank = rarity.rank,
|
||||
label = rarity.label,
|
||||
)
|
||||
},
|
||||
media = asset.media?.let {
|
||||
media = asset.media?.let { media ->
|
||||
NFTAsset.Media(
|
||||
animationUrl = it.animationUrl,
|
||||
imageUrl = it.imageUrl,
|
||||
animationUrl = media.animationUrl,
|
||||
imageUrl = media.imageUrl,
|
||||
)
|
||||
},
|
||||
traits = asset.traits.map {
|
||||
traits = asset.traits.map { trait ->
|
||||
NFTAsset.Trait(
|
||||
name = it.name,
|
||||
value = it.value,
|
||||
name = trait.name,
|
||||
value = trait.value,
|
||||
)
|
||||
},
|
||||
source = StatusSource.CACHE,
|
||||
|
|
@ -65,22 +65,22 @@ object NFTSdkAssetConverter : TwoWayConverter<Pair<Network, SdkNFTAsset>, NFTAss
|
|||
amount = value.amount,
|
||||
decimals = value.decimals,
|
||||
salePrice = salePrice,
|
||||
rarity = value.rarity?.let {
|
||||
rarity = value.rarity?.let { rarity ->
|
||||
SdkNFTAsset.Rarity(
|
||||
rank = it.rank,
|
||||
label = it.label,
|
||||
rank = rarity.rank,
|
||||
label = rarity.label,
|
||||
)
|
||||
},
|
||||
media = value.media?.let {
|
||||
media = value.media?.let { media ->
|
||||
SdkNFTAsset.Media(
|
||||
animationUrl = it.animationUrl,
|
||||
imageUrl = it.imageUrl,
|
||||
animationUrl = media.animationUrl,
|
||||
imageUrl = media.imageUrl,
|
||||
)
|
||||
},
|
||||
traits = value.traits.map {
|
||||
traits = value.traits.map { trait ->
|
||||
SdkNFTAsset.Trait(
|
||||
name = it.name,
|
||||
value = it.value,
|
||||
name = trait.name,
|
||||
value = trait.value,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ class NFTSdkCollectionConverter(
|
|||
.filter {
|
||||
it.id !is NFTAsset.Identifier.Unknown
|
||||
}
|
||||
.let {
|
||||
if (it.isEmpty()) {
|
||||
.let { items ->
|
||||
if (items.isEmpty()) {
|
||||
NFTCollection.Assets.Empty
|
||||
} else {
|
||||
NFTCollection.Assets.Value(
|
||||
items = it,
|
||||
items = items,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ internal object PreferencesDataStore {
|
|||
|
||||
private fun createCorruptionHandler(): ReplaceFileCorruptionHandler<Preferences> {
|
||||
return ReplaceFileCorruptionHandler(
|
||||
produceNewData = {
|
||||
Timber.w(it)
|
||||
produceNewData = { corruptionException ->
|
||||
Timber.w(corruptionException)
|
||||
emptyPreferences()
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ object PreferencesKeys {
|
|||
|
||||
val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") }
|
||||
|
||||
val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") }
|
||||
|
||||
// region Notifications
|
||||
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
|
||||
|
||||
|
|
@ -162,6 +164,9 @@ object PreferencesKeys {
|
|||
// region Permission
|
||||
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")
|
||||
|
||||
fun getShouldShowAskNotificationPermissionViaBs() =
|
||||
booleanPreferencesKey("ShouldShowAskNotificationPermissionViaBs")
|
||||
|
||||
fun getShouldShowInitialPermissionScreen(permission: String) =
|
||||
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
|
|||
val adapter = moshi.adapter(T::class.java)
|
||||
emitAll(
|
||||
data.map { preferences ->
|
||||
preferences[key]?.let {
|
||||
preferences[key]?.let { value ->
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
adapter.fromJson(value)
|
||||
} catch (e: JsonDataException) {
|
||||
null
|
||||
}
|
||||
|
|
@ -37,9 +37,9 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
|
|||
return flow {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
emitAll(
|
||||
data.map {
|
||||
data.map { prefs ->
|
||||
try {
|
||||
it[key]?.let(adapter::fromJson) ?: default
|
||||
prefs[key]?.let(adapter::fromJson) ?: default
|
||||
} catch (e: JsonDataException) {
|
||||
default
|
||||
}
|
||||
|
|
@ -59,9 +59,9 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Pref
|
|||
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let {
|
||||
?.let { value ->
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
adapter.fromJson(value)
|
||||
} catch (e: JsonDataException) {
|
||||
null
|
||||
}
|
||||
|
|
@ -76,9 +76,9 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
|||
val adapter = moshi.adapter(T::class.java)
|
||||
data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let {
|
||||
?.let { value ->
|
||||
try {
|
||||
adapter.fromJson(it)
|
||||
adapter.fromJson(value)
|
||||
} catch (e: JsonDataException) {
|
||||
default
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<Str
|
|||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
emitAll(
|
||||
data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() },
|
||||
data.map { it[key]?.let(adapter::fromJson).orEmpty() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ inline fun <reified T> AppPreferencesStore.getObjectSet(key: Preferences.Key<Str
|
|||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
emitAll(
|
||||
data.map {
|
||||
it[key]?.let(adapter::fromJson) ?: emptySet()
|
||||
it[key]?.let(adapter::fromJson).orEmpty()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ internal class DefaultSwapBestRateAnimationStore(
|
|||
* If true, reset flag to false
|
||||
*/
|
||||
override suspend fun getSyncOrNull(): Boolean {
|
||||
val value = dataStore.getSyncOrNull() ?: true
|
||||
if (value) {
|
||||
val shouldShowBestRateAnimation = dataStore.getSyncOrNull() != false
|
||||
if (shouldShowBestRateAnimation) {
|
||||
dataStore.store(false)
|
||||
}
|
||||
return value
|
||||
return shouldShowBestRateAnimation
|
||||
}
|
||||
}
|
||||
|
|
@ -42,8 +42,8 @@ internal class DefaultExpressAssetsStore(
|
|||
runtimeStore.store(userWalletId.stringValue, item)
|
||||
}
|
||||
launch {
|
||||
persistenceStore.updateData {
|
||||
it.toMutableMap().apply {
|
||||
persistenceStore.updateData { assetsByWalletId ->
|
||||
assetsByWalletId.toMutableMap().apply {
|
||||
put(userWalletId.stringValue, item)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,19 @@ internal class DefaultStakingYieldsStore(
|
|||
}
|
||||
|
||||
override suspend fun store(items: List<YieldDTO>) {
|
||||
dataStore.updateData { _ ->
|
||||
items
|
||||
dataStore.updateData { data ->
|
||||
val updatedItems = data.toMutableList()
|
||||
items.forEach { newItem ->
|
||||
val existingItemIndex = data.indexOfFirst { it.id == newItem.id }
|
||||
if (existingItemIndex != -1) {
|
||||
// Update existing item
|
||||
updatedItems[existingItemIndex] = newItem
|
||||
} else {
|
||||
// Add new item
|
||||
updatedItems.add(newItem)
|
||||
}
|
||||
}
|
||||
updatedItems
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ internal class DefaultTokenReceiveWarningActionStore(
|
|||
) : TokenReceiveWarningActionStore {
|
||||
|
||||
override suspend fun getSync(): Set<String> {
|
||||
return persistenceStore.data.firstOrNull() ?: emptySet()
|
||||
return persistenceStore.data.firstOrNull().orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun store(symbol: String) {
|
||||
|
|
|
|||
|
|
@ -12,26 +12,26 @@ internal object PendingActionConverter : Converter<BalanceDTO.PendingAction, Pen
|
|||
passthrough = value.passthrough,
|
||||
args = with(value.args) {
|
||||
PendingAction.PendingActionArgs(
|
||||
amount = this?.amount?.let {
|
||||
amount = this?.amount?.let { amount ->
|
||||
PendingAction.PendingActionArgs.Amount(
|
||||
required = it.required,
|
||||
minimum = it.minimum,
|
||||
maximum = it.maximum,
|
||||
required = amount.required,
|
||||
minimum = amount.minimum,
|
||||
maximum = amount.maximum,
|
||||
)
|
||||
},
|
||||
duration = this?.duration?.let {
|
||||
duration = this?.duration?.let { duration ->
|
||||
PendingAction.PendingActionArgs.Duration(
|
||||
required = it.required,
|
||||
minimum = it.minimum,
|
||||
maximum = it.maximum,
|
||||
required = duration.required,
|
||||
minimum = duration.minimum,
|
||||
maximum = duration.maximum,
|
||||
)
|
||||
},
|
||||
validatorAddress = this?.validatorAddress?.required,
|
||||
validatorAddresses = this?.validatorAddresses?.required,
|
||||
tronResource = this?.tronResource?.let {
|
||||
tronResource = this?.tronResource?.let { tronResource ->
|
||||
PendingAction.PendingActionArgs.TronResource(
|
||||
required = it.required,
|
||||
options = it.options,
|
||||
required = tronResource.required,
|
||||
options = tronResource.options,
|
||||
)
|
||||
},
|
||||
signatureVerification = this?.signatureVerification?.required,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
||||
internal class DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
|
||||
) : TangemPayTxHistoryItemsStore,
|
||||
StringKeyDataStoreDecorator<String, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
|
||||
override fun provideStringKey(key: String): String = key
|
||||
|
||||
override suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
return storedValue?.get(cursor)
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
|
||||
store(key, newValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,11 @@ interface TangemPayStorage {
|
|||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
|
||||
|
||||
suspend fun storeCustomerWalletAddress(customerWalletAddress: String)
|
||||
suspend fun storeOrderId(customerWalletAddress: String, orderId: String)
|
||||
|
||||
suspend fun getCustomerWalletAddress(): String?
|
||||
suspend fun getOrderId(customerWalletAddress: String): String?
|
||||
|
||||
suspend fun clear(customerWalletAddress: String)
|
||||
suspend fun clearOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun clearAll(customerWalletAddress: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
||||
interface TangemPayTxHistoryItemsStore {
|
||||
|
||||
suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>?
|
||||
|
||||
suspend fun remove(key: String)
|
||||
|
||||
suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>)
|
||||
}
|
||||
|
|
@ -28,14 +28,14 @@ internal class DefaultWalletManagersStore(
|
|||
): WalletManager? {
|
||||
val walletManagers = getSyncOrNull(userWalletId)
|
||||
|
||||
return walletManagers?.singleOrNull {
|
||||
it.wallet.blockchain == blockchain &&
|
||||
it.wallet.publicKey.derivationPath?.rawPath == derivationPath
|
||||
return walletManagers?.singleOrNull { walletManager ->
|
||||
walletManager.wallet.blockchain == blockchain &&
|
||||
walletManager.wallet.publicKey.derivationPath?.rawPath == derivationPath
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllSync(userWalletId: UserWalletId): List<WalletManager> {
|
||||
return getSyncOrNull(userWalletId) ?: emptyList()
|
||||
return getSyncOrNull(userWalletId).orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.local.yieldsupply
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
internal class DefaultYieldMarketsStore(
|
||||
private val persistenceStore: DataStore<List<YieldSupplyMarketTokenDto>>,
|
||||
) : YieldMarketsStore {
|
||||
|
||||
override fun get(): Flow<List<YieldSupplyMarketTokenDto>> = persistenceStore.data
|
||||
|
||||
override suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>? {
|
||||
return persistenceStore.data.firstOrNull()
|
||||
}
|
||||
|
||||
override suspend fun store(items: List<YieldSupplyMarketTokenDto>) {
|
||||
persistenceStore.updateData { _ -> items }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.yieldsupply
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface YieldMarketsStore {
|
||||
|
||||
fun get(): Flow<List<YieldSupplyMarketTokenDto>>
|
||||
|
||||
suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>?
|
||||
|
||||
suspend fun store(items: List<YieldSupplyMarketTokenDto>)
|
||||
}
|
||||
|
|
@ -20,9 +20,9 @@ internal fun OkHttpClient.Builder.addHeaders(
|
|||
Interceptor { chain ->
|
||||
val request = chain.request().newBuilder().apply {
|
||||
runBlocking {
|
||||
requestHeaders.forEach {
|
||||
val value = it.value.invoke()
|
||||
if (value.isNotBlank()) addHeader(name = it.key, value = value)
|
||||
requestHeaders.forEach { header ->
|
||||
val value = header.value.invoke()
|
||||
if (value.isNotBlank()) addHeader(name = header.key, value = value)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class NetworkLogsSaveInterceptor(
|
|||
|
||||
private fun logResponseMessage(response: Response, startNs: Long) {
|
||||
val responseHeaders = response.headers
|
||||
val responseBody = response.body!!
|
||||
val responseBody = requireNotNull(response.body)
|
||||
val contentLength = responseBody.contentLength()
|
||||
|
||||
val message = if (!response.promisesBody()) {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ sealed class RequestHeader(vararg pairs: Pair<String, ProviderSuspend<String>>)
|
|||
fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
val charCondition = c == '\t' || c in '\u0020'..'\u007e'
|
||||
if (!charCondition) {
|
||||
val isChar = c == '\t' || c in '\u0020'..'\u007e'
|
||||
if (!isChar) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SerializeNulls
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Factory to serialize nulls in Moshi if the class is annotated with [SerializeNulls].
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object SerializeNullsFactory : JsonAdapter.Factory {
|
||||
|
||||
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
|
||||
val rawType = Types.getRawType(type)
|
||||
if (!rawType.isAnnotationPresent(SerializeNulls::class.java)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val nextAdapter: JsonAdapter<Any> = moshi.nextAdapter(this, type, annotations)
|
||||
|
||||
return nextAdapter.serializeNulls()
|
||||
}
|
||||
}
|
||||
|
|
@ -37,8 +37,17 @@ class ApiConfigTest {
|
|||
appInfoProvider = mockk(),
|
||||
)
|
||||
}
|
||||
ApiConfig.ID.YieldSupply -> {
|
||||
YieldSupply(
|
||||
environmentConfigStorage = mockk(),
|
||||
appVersionProvider = mockk(),
|
||||
authProvider = mockk(),
|
||||
appInfoProvider = mockk(),
|
||||
)
|
||||
}
|
||||
ApiConfig.ID.TangemTech -> {
|
||||
TangemTech(
|
||||
environmentConfigStorage = mockk(),
|
||||
appVersionProvider = mockk(),
|
||||
authProvider = mockk(),
|
||||
appInfoProvider = mockk(),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
|
|||
express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"),
|
||||
devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"),
|
||||
blockAidApiKey = BLOCK_AID_API_KEY,
|
||||
tangemApiKey = TANGEM_API_KEY,
|
||||
tangemApiKeyDev = TANGEM_API_KEY_DEV,
|
||||
tangemApiKeyStage = TANGEM_API_KEY_STAGE,
|
||||
yieldModuleApiKey = YIELD_MODULE_KEY,
|
||||
yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV,
|
||||
)
|
||||
|
||||
override suspend fun initialize() = environmentConfig
|
||||
|
|
@ -26,5 +31,10 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
|
|||
const val EXPRESS_API_KEY = "express_api_key"
|
||||
const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
|
||||
const val BLOCK_AID_API_KEY = "block_aid_api_key"
|
||||
const val TANGEM_API_KEY = "tangem_api_key"
|
||||
const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
|
||||
const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
|
||||
const val YIELD_MODULE_KEY = "yield_module_api_key"
|
||||
const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"
|
||||
}
|
||||
}
|
||||
|
|
@ -84,8 +84,17 @@ internal class ProdApiConfigsManagerTest {
|
|||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
}
|
||||
ApiConfig.ID.YieldSupply -> {
|
||||
YieldSupply(
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
appVersionProvider = appVersionProvider,
|
||||
authProvider = appAuthProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
}
|
||||
ApiConfig.ID.TangemTech -> {
|
||||
TangemTech(
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
appVersionProvider = appVersionProvider,
|
||||
authProvider = appAuthProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
|
|
@ -101,6 +110,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private fun provideTestModels() = ApiConfig.ID.entries.map {
|
||||
when (it) {
|
||||
ApiConfig.ID.Express -> createExpressModel()
|
||||
ApiConfig.ID.YieldSupply -> createYieldSupplyModel()
|
||||
ApiConfig.ID.TangemTech -> createTangemTechModel()
|
||||
ApiConfig.ID.StakeKit -> createStakeKitModel()
|
||||
ApiConfig.ID.TangemPay -> createTangemPayModel()
|
||||
|
|
@ -165,6 +175,30 @@ internal class ProdApiConfigsManagerTest {
|
|||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem.org/",
|
||||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend { MockEnvironmentConfigStorage.TANGEM_API_KEY },
|
||||
"card_id" to ProviderSuspend { APP_CARD_ID },
|
||||
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "android" },
|
||||
"system_version" to ProviderSuspend { "Android 16" },
|
||||
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
|
||||
"timezone" to ProviderSuspend {
|
||||
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
|
||||
},
|
||||
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createYieldSupplyModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.YieldSupply,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://yield.tangem.org/",
|
||||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY },
|
||||
"card_id" to ProviderSuspend { APP_CARD_ID },
|
||||
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
|
|
@ -242,6 +276,6 @@ internal class ProdApiConfigsManagerTest {
|
|||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val APP_CARD_ID = "app_card_id"
|
||||
const val APP_CARD_PUBLIC_KEY = "app_public_key"
|
||||
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
||||
}
|
||||
}
|
||||
|
|
@ -64,9 +64,15 @@ class NetworkStatusDMSerializationTest {
|
|||
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
|
||||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amounts = mapOf("ETH" to BigDecimal("1.2345")),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
"ETH" to NetworkStatusDM.YieldSupplyStatus(
|
||||
amounts = listOf(
|
||||
NetworkStatusDM.CurrencyAmount(
|
||||
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
|
||||
amount = BigDecimal("1.2345"),
|
||||
),
|
||||
),
|
||||
yieldSupplyStatuses = listOf(
|
||||
NetworkStatusDM.YieldSupplyStatus(
|
||||
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
isAllowedToSpend = false,
|
||||
|
|
@ -91,9 +97,15 @@ class NetworkStatusDMSerializationTest {
|
|||
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
|
||||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amounts = mapOf("ETH" to BigDecimal("1.2345")),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
"ETH" to NetworkStatusDM.YieldSupplyStatus(
|
||||
amounts = listOf(
|
||||
NetworkStatusDM.CurrencyAmount(
|
||||
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
|
||||
amount = BigDecimal("1.2345"),
|
||||
),
|
||||
),
|
||||
yieldSupplyStatuses = listOf(
|
||||
NetworkStatusDM.YieldSupplyStatus(
|
||||
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
isAllowedToSpend = false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
// --- DTO ---
|
||||
@SerializeNulls
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UserWithNulls(val id: String?, val name: String?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UserWithoutNulls(val id: String?, val name: String?)
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class SerializeNullsFactoryTest {
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(SerializeNullsFactory)
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun `should serialize nulls for annotated class`() {
|
||||
val adapter = moshi.adapter(UserWithNulls::class.java)
|
||||
|
||||
val json = adapter.toJson(UserWithNulls(id = null, name = "John"))
|
||||
|
||||
assertThat(json).isEqualTo("""{"id":null,"name":"John"}""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should skip nulls for non-annotated class`() {
|
||||
val adapter = moshi.adapter(UserWithoutNulls::class.java)
|
||||
|
||||
val json = adapter.toJson(UserWithoutNulls(id = null, name = "John"))
|
||||
|
||||
assertThat(json).isEqualTo("""{"name":"John"}""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize annotated class correctly`() {
|
||||
val adapter = moshi.adapter(UserWithNulls::class.java)
|
||||
|
||||
val json = """{"id":null,"name":"Jane"}"""
|
||||
val result = adapter.fromJson(json)
|
||||
|
||||
assertThat(result).isEqualTo(UserWithNulls(id = null, name = "Jane"))
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M = getOr
|
|||
* @param params The parameters to store in the [ParamsContainer],
|
||||
|
||||
*/
|
||||
@Suppress("NullableToStringCall")
|
||||
inline fun <reified M : Model, reified P : Any> AppComponentContext.getOrCreateModel(
|
||||
params: P?,
|
||||
messageSender: UiMessageSender? = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.pagination.fetcher
|
||||
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.exception.EndOfPaginationException
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* First page: cursor = null
|
||||
* Next pages: cursor = cursorFromItem(lastItemOfPreviousPage)
|
||||
*/
|
||||
class CursorBatchFetcher<TRequestParams : Any, TItem : Any>(
|
||||
private val prefetchDistance: Int,
|
||||
private val batchSize: Int,
|
||||
private val subFetcher: SubFetcher<TRequestParams, TItem>,
|
||||
private val cursorFromItem: (TItem) -> String,
|
||||
) : BatchFetcher<TRequestParams, List<TItem>> {
|
||||
|
||||
data class Request<TRequestParams>(
|
||||
val limit: Int,
|
||||
val cursor: String?,
|
||||
val params: TRequestParams,
|
||||
)
|
||||
|
||||
fun interface SubFetcher<TRequestParams : Any, TItem : Any> {
|
||||
suspend fun fetch(
|
||||
request: Request<TRequestParams>,
|
||||
lastResult: BatchFetchResult<List<TItem>>?,
|
||||
isFirstBatchFetching: Boolean,
|
||||
): BatchFetchResult<List<TItem>>
|
||||
}
|
||||
|
||||
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
|
||||
|
||||
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<List<TItem>> {
|
||||
val request = Request(
|
||||
cursor = null,
|
||||
limit = prefetchDistance,
|
||||
params = requestParams,
|
||||
)
|
||||
|
||||
val result = runCatching {
|
||||
subFetcher.fetch(request = request, lastResult = null, isFirstBatchFetching = true)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
return BatchFetchResult.Error(it)
|
||||
}
|
||||
|
||||
lastRequest.value = request
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun fetchNext(
|
||||
overrideRequestParams: TRequestParams?,
|
||||
lastResult: BatchFetchResult<List<TItem>>,
|
||||
): BatchFetchResult<List<TItem>> {
|
||||
val lastRequest = requireNotNull(lastRequest.value) { "fetchFirst() must be called before fetchNext()" }
|
||||
|
||||
if (lastResult is BatchFetchResult.Success && lastResult.last && overrideRequestParams == null) {
|
||||
return BatchFetchResult.Error(EndOfPaginationException())
|
||||
}
|
||||
|
||||
val nextReq: Request<TRequestParams> =
|
||||
if (lastResult is BatchFetchResult.Success<List<TItem>>) {
|
||||
val items = lastResult.data
|
||||
if (items.isEmpty()) {
|
||||
return BatchFetchResult.Error(EndOfPaginationException())
|
||||
}
|
||||
|
||||
val nextCursor = cursorFromItem(items.last())
|
||||
|
||||
Request(
|
||||
cursor = nextCursor,
|
||||
limit = batchSize,
|
||||
params = overrideRequestParams ?: lastRequest.params,
|
||||
)
|
||||
} else {
|
||||
lastRequest.copy(limit = batchSize, params = overrideRequestParams ?: lastRequest.params)
|
||||
}
|
||||
|
||||
val result = runCatching {
|
||||
subFetcher.fetch(request = nextReq, lastResult = lastResult, isFirstBatchFetching = false)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
return BatchFetchResult.Error(it)
|
||||
}
|
||||
|
||||
this.lastRequest.value = nextReq
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -25,11 +25,16 @@ fun Resources.getStringSafe(@StringRes id: Int): String {
|
|||
*/
|
||||
fun Resources.getStringSafe(@StringRes id: Int, vararg formatArgs: Any): String {
|
||||
return runCatching { getString(id, *formatArgs) }
|
||||
.recoverCatching {
|
||||
.recoverCatching { throwable ->
|
||||
// If something goes wrong, returns the resource without arguments
|
||||
val string = getString(id)
|
||||
|
||||
reportIssue(it, resources = this, id, *formatArgs)
|
||||
reportIssue(
|
||||
throwable = throwable,
|
||||
resources = this,
|
||||
id = id,
|
||||
formatArgs = formatArgs,
|
||||
)
|
||||
|
||||
string
|
||||
}
|
||||
|
|
@ -60,8 +65,13 @@ fun Resources.getPluralStringSafe(@PluralsRes id: Int, count: Int, vararg format
|
|||
}
|
||||
|
||||
private fun Result<String>.getOrResourceName(resources: Resources, id: Int, vararg formatArgs: Any): String {
|
||||
return getOrElse {
|
||||
reportIssue(it, resources, id, formatArgs)
|
||||
return getOrElse { throwable ->
|
||||
reportIssue(
|
||||
throwable = throwable,
|
||||
resources = resources,
|
||||
id = id,
|
||||
formatArgs = formatArgs,
|
||||
)
|
||||
|
||||
// If something still goes wrong, returns the resource name
|
||||
resources.getResourceEntryName(id)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
|
|
@ -30,4 +32,29 @@ fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemThe
|
|||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that draws a fade effect. Used on screens with a list of repeating
|
||||
* elements and floating button at the bottom of the screen.
|
||||
*/
|
||||
@Composable
|
||||
fun Fade(
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = TangemTheme.colors.background.secondary,
|
||||
height: Dp = 32.dp,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -68,9 +68,9 @@ fun CardWithIcon(
|
|||
internal fun IconWithTitleAndDescription(
|
||||
title: String,
|
||||
description: String?,
|
||||
iconBackground: Color = TangemTheme.colors.background.secondary,
|
||||
icon: @Composable () -> Unit,
|
||||
additionalContent: @Composable () -> Unit = {},
|
||||
iconBackground: Color = TangemTheme.colors.background.secondary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
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