Updated on 2026-08-14
This commit is contained in:
commit
4daf088b89
648 changed files with 20086 additions and 2462 deletions
|
|
@ -40,10 +40,8 @@ sealed class ApiConfig {
|
|||
|
||||
companion object {
|
||||
internal const val DEBUG_BUILD_TYPE = "debug"
|
||||
internal const val DEBUG_PG_BUILD_TYPE = "debugPG"
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
@ -69,12 +69,10 @@ internal class Express(
|
|||
fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE,
|
||||
DEBUG_PG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.STAGE
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ internal class TangemTech(
|
|||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem-tech.com/v1/",
|
||||
baseUrl = "https://api.tangem.org/v1/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://devapi.tangem-tech.com/v1/",
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import java.lang.reflect.Type
|
||||
|
||||
internal class ApiResponseCallAdapter(
|
||||
private val resultType: Type,
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
) : CallAdapter<Type, Call<ApiResponse<Type>>> {
|
||||
|
||||
override fun responseType(): Type = resultType
|
||||
|
||||
override fun adapt(call: Call<Type>): Call<ApiResponse<Type>> {
|
||||
return ApiResponseCallDelegate(call)
|
||||
return ApiResponseCallDelegate(call, analyticsErrorHandler)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
|
||||
class ApiResponseCallAdapterFactory private constructor(
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
) : CallAdapter.Factory() {
|
||||
|
||||
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
|
||||
if (getRawType(returnType) != Call::class.java) {
|
||||
|
|
@ -19,11 +22,11 @@ class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory(
|
|||
}
|
||||
|
||||
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
|
||||
return ApiResponseCallAdapter(resultType)
|
||||
return ApiResponseCallAdapter(resultType, analyticsErrorHandler)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create() = ApiResponseCallAdapterFactory()
|
||||
fun create(analyticsErrorHandler: AnalyticsErrorHandler) = ApiResponseCallAdapterFactory(analyticsErrorHandler)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
|
|
@ -9,6 +10,7 @@ import timber.log.Timber
|
|||
|
||||
internal class ApiResponseCallDelegate<T : Any>(
|
||||
private val wrappedCall: Call<T>,
|
||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
) : Call<ApiResponse<T>> {
|
||||
|
||||
override fun enqueue(callback: Callback<ApiResponse<T>>) {
|
||||
|
|
@ -16,7 +18,7 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
}
|
||||
|
||||
override fun execute(): Response<ApiResponse<T>> = throw NotImplementedError()
|
||||
override fun clone(): Call<ApiResponse<T>> = ApiResponseCallDelegate(wrappedCall.clone())
|
||||
override fun clone(): Call<ApiResponse<T>> = ApiResponseCallDelegate(wrappedCall.clone(), analyticsErrorHandler)
|
||||
override fun request(): Request = wrappedCall.request()
|
||||
override fun timeout(): Timeout = wrappedCall.timeout()
|
||||
override fun isExecuted(): Boolean = wrappedCall.isExecuted
|
||||
|
|
@ -30,7 +32,7 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
) : Callback<T> {
|
||||
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val safeResponse = response.toSafeApiResponse()
|
||||
val safeResponse = response.toSafeApiResponse(analyticsErrorHandler)
|
||||
|
||||
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,51 +19,51 @@ sealed class ApiResponseError : Exception() {
|
|||
) : ApiResponseError() {
|
||||
|
||||
// region Error Codes
|
||||
enum class Code(val code: Int) {
|
||||
enum class Code(val numericCode: Int) {
|
||||
// 4xx Server Errors
|
||||
BAD_REQUEST(code = 400),
|
||||
UNAUTHORIZED(code = 401),
|
||||
PAYMENT_REQUIRED(code = 402),
|
||||
FORBIDDEN(code = 403),
|
||||
NOT_FOUND(code = 404),
|
||||
METHOD_NOT_ALLOWED(code = 405),
|
||||
NOT_ACCEPTABLE(code = 406),
|
||||
PROXY_AUTHENTICATION_REQUIRED(code = 407),
|
||||
REQUEST_TIMEOUT(code = 408),
|
||||
CONFLICT(code = 409),
|
||||
GONE(code = 410),
|
||||
LENGTH_REQUIRED(code = 411),
|
||||
PRECONDITION_FAILED(code = 412),
|
||||
PAYLOAD_TOO_LARGE(code = 413),
|
||||
URI_TOO_LONG(code = 414),
|
||||
UNSUPPORTED_MEDIA_TYPE(code = 415),
|
||||
RANGE_NOT_SATISFIABLE(code = 416),
|
||||
EXPECTATION_FAILED(code = 417),
|
||||
IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324
|
||||
UNPROCESSABLE_ENTITY(code = 422),
|
||||
LOCKED(code = 423),
|
||||
FAILED_DEPENDENCY(code = 424),
|
||||
TOO_EARLY(code = 425),
|
||||
UPGRADE_REQUIRED(code = 426),
|
||||
PRECONDITION_REQUIRED(code = 428),
|
||||
TOO_MANY_REQUESTS(code = 429),
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431),
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS(code = 451),
|
||||
BAD_REQUEST(numericCode = 400),
|
||||
UNAUTHORIZED(numericCode = 401),
|
||||
PAYMENT_REQUIRED(numericCode = 402),
|
||||
FORBIDDEN(numericCode = 403),
|
||||
NOT_FOUND(numericCode = 404),
|
||||
METHOD_NOT_ALLOWED(numericCode = 405),
|
||||
NOT_ACCEPTABLE(numericCode = 406),
|
||||
PROXY_AUTHENTICATION_REQUIRED(numericCode = 407),
|
||||
REQUEST_TIMEOUT(numericCode = 408),
|
||||
CONFLICT(numericCode = 409),
|
||||
GONE(numericCode = 410),
|
||||
LENGTH_REQUIRED(numericCode = 411),
|
||||
PRECONDITION_FAILED(numericCode = 412),
|
||||
PAYLOAD_TOO_LARGE(numericCode = 413),
|
||||
URI_TOO_LONG(numericCode = 414),
|
||||
UNSUPPORTED_MEDIA_TYPE(numericCode = 415),
|
||||
RANGE_NOT_SATISFIABLE(numericCode = 416),
|
||||
EXPECTATION_FAILED(numericCode = 417),
|
||||
IM_A_TEAPOT(numericCode = 418), // Not an error, but an April Fools' joke from RFC 2324
|
||||
UNPROCESSABLE_ENTITY(numericCode = 422),
|
||||
LOCKED(numericCode = 423),
|
||||
FAILED_DEPENDENCY(numericCode = 424),
|
||||
TOO_EARLY(numericCode = 425),
|
||||
UPGRADE_REQUIRED(numericCode = 426),
|
||||
PRECONDITION_REQUIRED(numericCode = 428),
|
||||
TOO_MANY_REQUESTS(numericCode = 429),
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE(numericCode = 431),
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS(numericCode = 451),
|
||||
// 5xx Server Errors
|
||||
INTERNAL_SERVER_ERROR(code = 500),
|
||||
NOT_IMPLEMENTED(code = 501),
|
||||
BAD_GATEWAY(code = 502),
|
||||
SERVICE_UNAVAILABLE(code = 503),
|
||||
GATEWAY_TIMEOUT(code = 504),
|
||||
HTTP_VERSION_NOT_SUPPORTED(code = 505),
|
||||
VARIANT_ALSO_NEGOTIATES(code = 506),
|
||||
INSUFFICIENT_STORAGE(code = 507),
|
||||
LOOP_DETECTED(code = 508),
|
||||
NOT_EXTENDED(code = 510),
|
||||
NETWORK_AUTHENTICATION_REQUIRED(code = 511),
|
||||
INTERNAL_SERVER_ERROR(numericCode = 500),
|
||||
NOT_IMPLEMENTED(numericCode = 501),
|
||||
BAD_GATEWAY(numericCode = 502),
|
||||
SERVICE_UNAVAILABLE(numericCode = 503),
|
||||
GATEWAY_TIMEOUT(numericCode = 504),
|
||||
HTTP_VERSION_NOT_SUPPORTED(numericCode = 505),
|
||||
VARIANT_ALSO_NEGOTIATES(numericCode = 506),
|
||||
INSUFFICIENT_STORAGE(numericCode = 507),
|
||||
LOOP_DETECTED(numericCode = 508),
|
||||
NOT_EXTENDED(numericCode = 510),
|
||||
NETWORK_AUTHENTICATION_REQUIRED(numericCode = 511),
|
||||
;
|
||||
|
||||
override fun toString(): String = "$code - $name"
|
||||
override fun toString(): String = "$numericCode - $name"
|
||||
|
||||
companion object {
|
||||
val values = values()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
|
|
@ -9,18 +11,20 @@ import java.net.UnknownHostException
|
|||
import java.util.concurrent.TimeoutException
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
||||
internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse<T> {
|
||||
val body = body()
|
||||
|
||||
return if (isSuccessful && body != null) {
|
||||
apiSuccess(body)
|
||||
} else {
|
||||
val code = ApiResponseError.HttpException.Code.values
|
||||
.firstOrNull { it.code == code() }
|
||||
.firstOrNull { it.numericCode == code() }
|
||||
val e = try {
|
||||
if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
} else {
|
||||
sendHttpError(code, analyticsErrorHandler)
|
||||
|
||||
ApiResponseError.HttpException(code, message(), errorBody()?.string())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -32,6 +36,21 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> Response<T>.sendHttpError(
|
||||
code: ApiResponseError.HttpException.Code,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
) {
|
||||
val fullRequestUrl = raw().request.url.toUrl()
|
||||
val shortUrl = fullRequestUrl.authority + fullRequestUrl.path
|
||||
analyticsErrorHandler.sendErrorEvent(
|
||||
ApiErrorEvent(
|
||||
endpoint = shortUrl,
|
||||
code = code.numericCode,
|
||||
message = errorBody()?.string().orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Throwable.toApiError(): ApiResponseError = when (this) {
|
||||
is ConnectException,
|
||||
is UnknownHostException,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.api.common.response.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
internal data class ApiErrorEvent(
|
||||
val endpoint: String,
|
||||
val code: Int,
|
||||
val message: String,
|
||||
) : AnalyticsEvent(
|
||||
category = "Tangem API Service",
|
||||
event = "Exception",
|
||||
params = mapOf(
|
||||
"Endpoint" to endpoint,
|
||||
"Code" to code.toString(),
|
||||
"Message" to message,
|
||||
),
|
||||
)
|
||||
|
|
@ -20,7 +20,7 @@ data class TokenMarketListResponse(
|
|||
@Json(name = "name") val name: String,
|
||||
@Json(name = "symbol") val symbol: String,
|
||||
@Json(name = "current_price") val currentPrice: BigDecimal,
|
||||
@Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage,
|
||||
@Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage?,
|
||||
@Json(name = "market_rating") val marketRating: Int?,
|
||||
@Json(name = "market_cap") val marketCap: BigDecimal?,
|
||||
@Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?,
|
||||
|
|
@ -28,9 +28,9 @@ data class TokenMarketListResponse(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PriceChangePercentage(
|
||||
@Json(name = "24h") val h24: BigDecimal,
|
||||
@Json(name = "1w") val week1: BigDecimal,
|
||||
@Json(name = "30d") val day30: BigDecimal,
|
||||
@Json(name = "24h") val h24: BigDecimal?,
|
||||
@Json(name = "1w") val week1: BigDecimal?,
|
||||
@Json(name = "30d") val day30: BigDecimal?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.datasource.api.stakekit.models.response.model
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.api.stakekit.models.request.Address
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.PendingAction.PendingActionArgs.Amount
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -31,6 +32,8 @@ data class BalanceDTO(
|
|||
val pricePerShare: BigDecimal,
|
||||
@Json(name = "pendingActions")
|
||||
val pendingActions: List<PendingAction>,
|
||||
@Json(name = "pendingActionConstraints")
|
||||
val pendingActionConstraints: List<PendingActionConstraints>?,
|
||||
@Json(name = "token")
|
||||
val tokenDTO: TokenDTO,
|
||||
@Json(name = "validatorAddress")
|
||||
|
|
@ -99,7 +102,7 @@ data class BalanceDTO(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class Amount(
|
||||
@Json(name = "required")
|
||||
val required: Boolean,
|
||||
val required: Boolean = true,
|
||||
@Json(name = "minimum")
|
||||
val minimum: BigDecimal?,
|
||||
@Json(name = "maximum")
|
||||
|
|
@ -136,6 +139,14 @@ data class BalanceDTO(
|
|||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PendingActionConstraints(
|
||||
@Json(name = "type")
|
||||
val type: StakingActionTypeDTO,
|
||||
@Json(name = "amount")
|
||||
val amountArg: Amount,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Required(
|
||||
@Json(name = "required")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.di
|
|||
|
||||
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.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
|
|
@ -61,6 +62,7 @@ internal object NetworkModule {
|
|||
fun provideExpressApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
appLogsStore: AppLogsStore,
|
||||
): TangemExpressApi {
|
||||
|
|
@ -69,6 +71,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
|
|
@ -83,6 +86,7 @@ internal object NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
appLogsStore: AppLogsStore,
|
||||
): StakeKitApi {
|
||||
return createApi(
|
||||
|
|
@ -90,6 +94,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
|
||||
connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
|
||||
|
|
@ -109,6 +114,7 @@ internal object NetworkModule {
|
|||
fun provideOnrampApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
appLogsStore: AppLogsStore,
|
||||
): OnrampApi {
|
||||
|
|
@ -117,6 +123,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
|
|
@ -130,6 +137,7 @@ internal object NetworkModule {
|
|||
fun provideTangemTechApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechApi {
|
||||
return createApi(
|
||||
|
|
@ -137,6 +145,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = { applyTimeoutAnnotations() },
|
||||
)
|
||||
}
|
||||
|
|
@ -147,6 +156,7 @@ internal object NetworkModule {
|
|||
fun provideTangemTechApiV2(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
): TangemTechApiV2 {
|
||||
return provideTangemTechApiInternal(
|
||||
|
|
@ -154,6 +164,7 @@ internal object NetworkModule {
|
|||
context = context,
|
||||
appVersionProvider = appVersionProvider,
|
||||
baseUrl = PROD_V2_TANGEM_TECH_BASE_URL,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +173,7 @@ internal object NetworkModule {
|
|||
fun provideTangemTechMarketsApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechMarketsApi {
|
||||
return createApi(
|
||||
|
|
@ -169,6 +181,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = {
|
||||
this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
|
@ -183,6 +196,7 @@ internal object NetworkModule {
|
|||
fun provideTangemVisaAuthApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
appLogsStore: AppLogsStore,
|
||||
): TangemVisaAuthApi {
|
||||
|
|
@ -191,6 +205,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
|
|
@ -204,6 +219,7 @@ internal object NetworkModule {
|
|||
fun provideTangemVisaApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
appLogsStore: AppLogsStore,
|
||||
): TangemVisaApi {
|
||||
|
|
@ -212,6 +228,7 @@ internal object NetworkModule {
|
|||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
|
|
@ -226,6 +243,7 @@ internal object NetworkModule {
|
|||
context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
baseUrl: String,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
timeouts: Timeouts = Timeouts(),
|
||||
requestHeaders: List<RequestHeader> = listOf(AppVersionPlatformHeaders(appVersionProvider)),
|
||||
): T {
|
||||
|
|
@ -257,7 +275,7 @@ internal object NetworkModule {
|
|||
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
|
||||
.baseUrl(baseUrl)
|
||||
.client(client)
|
||||
.build()
|
||||
|
|
@ -269,6 +287,7 @@ internal object NetworkModule {
|
|||
moshi: Moshi,
|
||||
context: Context,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
timeouts: Timeouts = Timeouts(),
|
||||
clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this },
|
||||
): T {
|
||||
|
|
@ -276,7 +295,7 @@ internal object NetworkModule {
|
|||
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
|
||||
.baseUrl(environmentConfig.baseUrl)
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
|
|
|
|||
|
|
@ -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.local.walletconnect.DefaultWalletConnectStore
|
||||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
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 WalletConnectModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletConnectStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletConnectStore {
|
||||
return DefaultWalletConnectStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = setTypes<WcSessionDTO>(),
|
||||
defaultValue = emptySet(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "wallet_connect_sessions") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,18 +4,15 @@ import android.content.Context
|
|||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -30,7 +27,7 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
class AppLogsStore @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(
|
||||
|
|
@ -38,8 +35,10 @@ class AppLogsStore @Inject constructor(
|
|||
CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") },
|
||||
)
|
||||
private val mutex = Mutex()
|
||||
private val zipMutex = Mutex()
|
||||
|
||||
private val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME)
|
||||
private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
|
||||
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
|
||||
|
||||
private val formatter = DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(2)
|
||||
|
|
@ -58,6 +57,16 @@ class AppLogsStore @Inject constructor(
|
|||
/** Get log file */
|
||||
fun getFile(): File? = if (file.exists()) file else null
|
||||
|
||||
suspend fun getZipFile(): File? {
|
||||
return zipMutex.withLock {
|
||||
if (file.exists()) {
|
||||
zip(listOf(file), fileZip)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Save log [message] */
|
||||
fun saveLogMessage(tag: String, message: String) {
|
||||
// Temporally logs are not saved in prod environment
|
||||
|
|
@ -133,8 +142,41 @@ class AppLogsStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
private suspend fun zip(filesToCompress: List<File>, outputZipFile: File): File? {
|
||||
return withContext(dispatchers.io) {
|
||||
if (outputZipFile.exists() && !outputZipFile.delete()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
|
||||
FileOutputStream(outputZipFile).use { fos ->
|
||||
ZipOutputStream(fos).use { zos ->
|
||||
filesToCompress.forEach { file ->
|
||||
FileInputStream(file).use { inStream ->
|
||||
val ze = ZipEntry(file.name)
|
||||
zos.putNextEntry(ze)
|
||||
var len: Int
|
||||
while (inStream.read(buffer).also { len = it } > 0) {
|
||||
zos.write(buffer, 0, len)
|
||||
}
|
||||
}
|
||||
}
|
||||
zos.finish() // Ensures the zip output is finalized
|
||||
}
|
||||
}
|
||||
outputZipFile
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BUFFER_SIZE = 1024
|
||||
|
||||
const val LOG_FILE_NAME = "logs.txt"
|
||||
const val NEW_LOG_FILE_NAME = "app_logs.txt"
|
||||
// the only name that we allow to send as email to company addresses
|
||||
const val PERMITTED_FILE_NAME = "log.txt"
|
||||
const val PERMITTED_FILE_NAME_ZIP = "log.zip"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import timber.log.Timber
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class NetworkAddressConverter(
|
||||
class NetworkAddressConverter(
|
||||
private val selectedAddress: String,
|
||||
) : TwoWayConverter<Set<NetworkStatusDM.Address>, NetworkAddress> {
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ private typealias AmountsDomainModel = Map<CryptoCurrency.ID, CryptoCurrencyAmou
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
|
||||
object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
|
||||
|
||||
override fun convert(value: AmountsDataModel): AmountsDomainModel {
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.utils.converter.TwoWayConverter
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object NetworkDerivationPathConverter :
|
||||
object NetworkDerivationPathConverter :
|
||||
TwoWayConverter<NetworkStatusDM.DerivationPath, Network.DerivationPath> {
|
||||
|
||||
override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM?> {
|
||||
object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM?> {
|
||||
|
||||
override fun convert(value: NetworkStatus): NetworkStatusDM? {
|
||||
return when (val status = value.value) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.tokens.model.Network
|
|||
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal sealed interface NetworkStatusDM {
|
||||
sealed interface NetworkStatusDM {
|
||||
|
||||
val networkId: Network.ID
|
||||
val derivationPath: DerivationPath
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ internal class DefaultNFTPersistenceStore(
|
|||
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
|
||||
) : NFTPersistenceStore {
|
||||
|
||||
override fun getCollections(): Flow<List<NFTCollection>> = collectionsPersistenceStore.data
|
||||
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
|
||||
|
||||
override suspend fun getCollectionsSync(): List<NFTCollection>? = collectionsPersistenceStore
|
||||
.data
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ internal class DefaultNFTRuntimeStore(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow<NFTAsset> =
|
||||
override fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow<NFTAsset?> =
|
||||
collectionsRuntimeStore
|
||||
.get()
|
||||
.combine(getSalePrice(assetId)) { collectionsData, price ->
|
||||
|
|
@ -50,13 +50,17 @@ internal class DefaultNFTRuntimeStore(
|
|||
.getCollection(collectionId)
|
||||
?.getAsset(assetId)
|
||||
?.mergeWithPrice(price)
|
||||
?: NFTAsset.Error(assetId)
|
||||
}
|
||||
|
||||
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice> = pricesRuntimeStore
|
||||
.get()
|
||||
.map { it[assetId] ?: NFTSalePrice.Empty(assetId) }
|
||||
|
||||
override suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice = pricesRuntimeStore
|
||||
.getSyncOrNull()
|
||||
?.let { it[assetId] }
|
||||
?: NFTSalePrice.Empty(assetId)
|
||||
|
||||
override suspend fun saveCollections(collections: NFTCollections) {
|
||||
collectionsRuntimeStore.store(collections)
|
||||
}
|
||||
|
|
@ -72,8 +76,13 @@ internal class DefaultNFTRuntimeStore(
|
|||
?.collections
|
||||
?.firstOrNull { it.id == collectionId }
|
||||
|
||||
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier): NFTAsset? =
|
||||
assets.firstOrNull { it.id == assetId }
|
||||
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier): NFTAsset? = when (val assets = assets) {
|
||||
is NFTCollection.Assets.Empty,
|
||||
is NFTCollection.Assets.Loading,
|
||||
is NFTCollection.Assets.Failed,
|
||||
-> null
|
||||
is NFTCollection.Assets.Value -> assets.items.firstOrNull { it.id == assetId }
|
||||
}
|
||||
|
||||
private fun NFTCollections.mergeWithPrices(prices: Map<NFTAsset.Identifier, NFTSalePrice>): NFTCollections =
|
||||
when (val content = this.content) {
|
||||
|
|
@ -89,18 +98,23 @@ internal class DefaultNFTRuntimeStore(
|
|||
copy(
|
||||
collections = this.collections?.map { data ->
|
||||
data.copy(
|
||||
assets = data.assets.map { asset ->
|
||||
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
|
||||
assets = when (val assets = data.assets) {
|
||||
is NFTCollection.Assets.Empty,
|
||||
is NFTCollection.Assets.Loading,
|
||||
is NFTCollection.Assets.Failed,
|
||||
-> assets
|
||||
is NFTCollection.Assets.Value -> assets.copy(
|
||||
items = assets.items.map { asset ->
|
||||
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
source = this.source,
|
||||
)
|
||||
|
||||
private fun NFTAsset.mergeWithPrice(price: NFTSalePrice): NFTAsset = when (this) {
|
||||
is NFTAsset.Error -> this
|
||||
is NFTAsset.Value -> copy(
|
||||
salePrice = price,
|
||||
)
|
||||
}
|
||||
private fun NFTAsset.mergeWithPrice(price: NFTSalePrice): NFTAsset = copy(
|
||||
salePrice = price,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.blockchain.nft.models.NFTCollection
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NFTPersistenceStore {
|
||||
fun getCollections(): Flow<List<NFTCollection>>
|
||||
fun getCollections(): Flow<List<NFTCollection>?>
|
||||
|
||||
suspend fun getCollectionsSync(): List<NFTCollection>?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.local.nft
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
|
|
@ -11,10 +12,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
|||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithCustomKeyTypes
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -25,41 +28,51 @@ class NFTPersistenceStoreFactory @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
fun provide(network: Network): NFTPersistenceStore {
|
||||
val networkStringIdentifier = listOfNotNull(
|
||||
network.id.value,
|
||||
network.derivationPath.value,
|
||||
).joinToString("_") {
|
||||
// remove all non-alphanumeric characters
|
||||
it
|
||||
.toCharArray()
|
||||
.filter(Char::isLetterOrDigit)
|
||||
.joinToString("")
|
||||
.lowercase()
|
||||
}
|
||||
fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore {
|
||||
// simplify network identifier so that is correct for a file name
|
||||
// e.g. eth_m4460000 or theopennetwork_m446070
|
||||
val networkStringId =
|
||||
network.id.formatted() + network.derivationPath.formatted()?.let { "_$it" }.orEmpty()
|
||||
// simplify user wallet id so that is correct for a file name
|
||||
// e.g. 9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a
|
||||
val userWalletStringId = userWalletId.formatted()
|
||||
return DefaultNFTPersistenceStore(
|
||||
collectionsPersistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = listTypes<NFTCollection>(),
|
||||
defaultValue = emptyList(),
|
||||
),
|
||||
produceFile = {
|
||||
context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_collections")
|
||||
},
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
collectionsPersistenceStore = createPersistenceStore(
|
||||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_collections
|
||||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_collections
|
||||
fileName = "nft_${userWalletStringId}_${networkStringId}_collections",
|
||||
types = listTypes<NFTCollection>(),
|
||||
defaultValue = emptyList(),
|
||||
),
|
||||
pricesPersistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = {
|
||||
context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_prices")
|
||||
},
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
pricesPersistenceStore = createPersistenceStore(
|
||||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
|
||||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
|
||||
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
|
||||
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> createPersistenceStore(fileName: String, types: ParameterizedType, defaultValue: T): DataStore<T> =
|
||||
DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = types,
|
||||
defaultValue = defaultValue,
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = fileName) },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
|
||||
private fun Network.ID.formatted(): String = value
|
||||
.filter(Char::isLetterOrDigit)
|
||||
.lowercase()
|
||||
|
||||
private fun Network.DerivationPath.formatted(): String? = value
|
||||
?.filter(Char::isLetterOrDigit)
|
||||
?.lowercase()
|
||||
|
||||
private fun UserWalletId.formatted(): String = stringValue
|
||||
.lowercase()
|
||||
}
|
||||
|
|
@ -16,7 +16,9 @@ interface NFTRuntimeStore {
|
|||
|
||||
fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow<NFTAsset?>
|
||||
|
||||
fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice?>
|
||||
fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice>
|
||||
|
||||
suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice
|
||||
|
||||
suspend fun saveCollections(collections: NFTCollections)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,12 @@ import com.tangem.domain.tokens.model.Network
|
|||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
|
||||
class NFTSdkAssetConverter(
|
||||
private val nftSdkAssetIdentifierConverter: NFTSdkAssetIdentifierConverter,
|
||||
private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter,
|
||||
) : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
|
||||
object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
|
||||
override fun convert(value: Pair<Network, SdkNFTAsset>): NFTAsset {
|
||||
val (network, asset) = value
|
||||
val assetId = nftSdkAssetIdentifierConverter.convert(asset.identifier)
|
||||
val collectionId = nftSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier)
|
||||
return NFTAsset.Value(
|
||||
val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier)
|
||||
val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier)
|
||||
return NFTAsset(
|
||||
id = assetId,
|
||||
collectionId = collectionId,
|
||||
network = network,
|
||||
|
|
@ -28,23 +25,22 @@ class NFTSdkAssetConverter(
|
|||
assetId = assetId,
|
||||
value = it.value,
|
||||
symbol = it.symbol,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
} ?: NFTSalePrice.Empty(assetId = assetId),
|
||||
rarity = asset.rarity?.let {
|
||||
NFTAsset.Value.Rarity(
|
||||
NFTAsset.Rarity(
|
||||
rank = it.rank,
|
||||
label = it.label,
|
||||
)
|
||||
},
|
||||
media = asset.media?.let {
|
||||
NFTAsset.Value.Media(
|
||||
NFTAsset.Media(
|
||||
url = it.url,
|
||||
mimetype = it.mimetype,
|
||||
)
|
||||
},
|
||||
traits = asset.traits.map {
|
||||
NFTAsset.Value.Trait(
|
||||
NFTAsset.Trait(
|
||||
name = it.name,
|
||||
value = it.value,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
package com.tangem.datasource.local.nft.converter
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
|
||||
|
||||
class NFTSdkCollectionConverter(
|
||||
private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter,
|
||||
private val nftSdkAssetConverter: NFTSdkAssetConverter,
|
||||
) : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
|
||||
object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
|
||||
override fun convert(value: Pair<Network, SdkNFTCollection>): NFTCollection {
|
||||
val (network, collection) = value
|
||||
val collectionId = nftSdkCollectionIdentifierConverter.convert(collection.identifier)
|
||||
val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier)
|
||||
return NFTCollection(
|
||||
id = collectionId,
|
||||
network = network,
|
||||
|
|
@ -22,10 +20,20 @@ class NFTSdkCollectionConverter(
|
|||
count = collection.count,
|
||||
assets = collection.assets
|
||||
.map { asset ->
|
||||
nftSdkAssetConverter.convert(network to asset)
|
||||
NFTSdkAssetConverter.convert(network to asset)
|
||||
}
|
||||
.filter {
|
||||
it.id !is NFTAsset.Identifier.Unknown
|
||||
}
|
||||
.let {
|
||||
if (it.isEmpty()) {
|
||||
NFTCollection.Assets.Empty
|
||||
} else {
|
||||
NFTCollection.Assets.Value(
|
||||
items = it,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.local.token.converter
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.PendingActionConstraints
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object PendingActionConstraintsConverter :
|
||||
Converter<BalanceDTO.PendingActionConstraints, PendingActionConstraints> {
|
||||
override fun convert(value: BalanceDTO.PendingActionConstraints): PendingActionConstraints {
|
||||
return PendingActionConstraints(
|
||||
type = StakingActionTypeConverter.convert(value.type),
|
||||
amountArg = PendingAction.PendingActionArgs.Amount(
|
||||
required = value.amountArg.required,
|
||||
minimum = value.amountArg.minimum,
|
||||
maximum = value.amountArg.maximum,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,8 @@ internal class YieldBalanceConverter(
|
|||
pendingActions = PendingActionConverter
|
||||
.convertList(item.pendingActions)
|
||||
.sortedBy { it.passthrough },
|
||||
pendingActionsConstraints = PendingActionConstraintsConverter
|
||||
.convertList(item.pendingActionConstraints.orEmpty()),
|
||||
isPending = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ interface UserWalletsStore {
|
|||
|
||||
val userWallets: Flow<List<UserWallet>>
|
||||
|
||||
suspend fun getSyncOrNull(key: UserWalletId): UserWallet?
|
||||
fun getSyncOrNull(key: UserWalletId): UserWallet?
|
||||
|
||||
suspend fun getSyncStrict(key: UserWalletId): UserWallet
|
||||
|
||||
suspend fun getAllSyncOrNull(): List<UserWallet>?
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.walletconnect
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal typealias WcSessionCollection = Set<WcSessionDTO>
|
||||
|
||||
class DefaultWalletConnectStore(
|
||||
private val persistenceStore: DataStore<WcSessionCollection>,
|
||||
) : WalletConnectStore {
|
||||
|
||||
override val sessions: Flow<WcSessionCollection>
|
||||
get() = persistenceStore.data
|
||||
|
||||
override suspend fun saveSessions(sessions: WcSessionCollection) {
|
||||
persistenceStore.updateData { data -> data.plus(sessions) }
|
||||
}
|
||||
|
||||
override suspend fun removeSessions(sessions: WcSessionCollection) {
|
||||
persistenceStore.updateData { data -> data.minus(sessions) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.local.walletconnect
|
||||
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
interface WalletConnectStore {
|
||||
|
||||
val sessions: Flow<WcSessionCollection>
|
||||
suspend fun findSessionByTopic(topic: String) = sessions.first().find { it.topic == topic }
|
||||
|
||||
suspend fun saveSessions(sessions: WcSessionCollection)
|
||||
suspend fun saveSession(session: WcSessionDTO) = saveSessions(setOf(session))
|
||||
|
||||
suspend fun removeSessions(sessions: WcSessionCollection)
|
||||
suspend fun removeSession(session: WcSessionDTO) = removeSessions(setOf(session))
|
||||
}
|
||||
|
|
@ -6,8 +6,6 @@ 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.DEBUG_PG_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
|
||||
|
|
@ -97,12 +95,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
private fun createExpressModel(): Model {
|
||||
val environment = when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE,
|
||||
DEBUG_PG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.STAGE
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
|
|
@ -114,12 +110,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
environment = environment,
|
||||
baseUrl = when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE,
|
||||
DEBUG_PG_BUILD_TYPE,
|
||||
-> "[REDACTED_ENV_URL]"
|
||||
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}]")
|
||||
|
|
@ -152,7 +146,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
id = ApiConfig.ID.TangemTech,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem-tech.com/v1/",
|
||||
baseUrl = "https://api.tangem.org/v1/",
|
||||
headers = mapOf(
|
||||
"card_id" to ProviderSuspend { APP_CARD_ID },
|
||||
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue