Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-05 11:53:07 +03:00
commit 8fe3e9e3c1
974 changed files with 23162 additions and 21412 deletions

View file

@ -113,6 +113,12 @@ sealed class AnalyticsParam {
data object WalletConnect : TxSentFrom("WalletConnect")
data object Sell : TxSentFrom("Sell")
data class NFT(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("NFT"), TxData
}
sealed interface TxData {

View file

@ -22,9 +22,35 @@ sealed class MainScreenAnalyticsEvent(
)
// region Action Buttons feature
data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
data class ButtonBuy(
val status: AnalyticsParam.Status,
val screenType: String? = null,
) : MainScreenAnalyticsEvent(
event = "Button - Buy",
params = mapOf(AnalyticsParam.STATUS to status.value),
params = buildMap {
put(AnalyticsParam.STATUS, status.value)
screenType?.let { put(AnalyticsParam.TYPE, it) }
},
)
data object ButtonReceive : MainScreenAnalyticsEvent(
event = "Button - Receive",
)
data object LimitsClicked : MainScreenAnalyticsEvent(
event = "Limits Clicked",
)
data object NoticeBalancesInfo : MainScreenAnalyticsEvent(
event = "Notice - Balances Info",
)
data object NoticeLimitsInfo : MainScreenAnalyticsEvent(
event = "Notice - Limits Info",
)
data object ButtonExplore : MainScreenAnalyticsEvent(
event = "Button - Explore",
)
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
@ -83,4 +109,9 @@ sealed class MainScreenAnalyticsEvent(
params = mapOf(ERROR_CODE to errorCode),
)
// endregion
companion object {
const val VISA_TYPE = "Visa"
const val WALLET_TYPE = "Wallet"
}
}

View file

@ -7,14 +7,6 @@
"name": "ONRAMP_ENABLED",
"version": "undefined"
},
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",
"version": "5.22.0"
},
{
"name": "NAVIGATION_REFACTORING",
"version": "5.23.0"
},
{
"name": "VISA_ONBOARDING_ENABLED",
"version": "undefined"
@ -27,6 +19,14 @@
"name": "NFT_ENABLED",
"version": "undefined"
},
{
"name": "NFT_EVM_ENABLED",
"version": "undefined"
},
{
"name": "NFT_SOLANA_ENABLED",
"version": "undefined"
},
{
"name": "STAKING_CARDANO_ENABLED",
"version": "undefined"
@ -36,21 +36,13 @@
"version": "5.22.0"
},
{
"name": "ASK_BIOMETRY_REFACTORING_ENABLED",
"version": "5.22.0"
},
{
"name": "NOTE_REFACTORING_ENABLED",
"version": "5.23.0"
"name": "NEW_ARTWORK_LOADING",
"version": "5.24.0"
},
{
"name": "NEW_ATTESTATION_ENABLED",
"version": "undefined"
},
{
"name": "TWIN_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "SEND_V2_ENABLED",
"version": "5.23.0"
@ -62,5 +54,13 @@
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0"
},
{
"name": "STAKING_LOADING_REFACTORING_ENABLED",
"version": "5.25.0"
}
]

View file

@ -32,7 +32,7 @@ internal class DevFeatureTogglesManager(
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
) ?: emptyMap<String, Boolean>()
val localFeatureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())

View file

@ -2,6 +2,7 @@ package com.tangem.core.configtoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.Moshi
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.storage.ConfigToggle
@ -12,6 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,7 +26,11 @@ import kotlin.collections.set
internal class DevTogglesManagerTest {
private val localTogglesStorage = mockk<TogglesStorage>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val appPreferenceStore = AppPreferencesStore(
moshi = Moshi.Builder().build(),
dispatchers = TestingCoroutineDispatcherProvider(),
preferencesDataStore = mockk(relaxed = true),
)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localTogglesStorage = localTogglesStorage,

View file

@ -8,6 +8,7 @@ import com.squareup.moshi.Types
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,7 +25,11 @@ internal class LocalTogglesStorageTest {
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val assetLoader = AssetLoader(
assetReader = assetReader,
moshi = moshi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val storage = LocalTogglesStorage(assetLoader)

View file

@ -72,6 +72,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.math.BigInteger
internal class BigIntegerAdapter {
@FromJson
fun fromJson(value: String) = BigInteger(value)
@ToJson
fun toJson(value: BigInteger) = value.toString()
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.api.common.blockaid
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import retrofit2.http.Body
import retrofit2.http.POST
interface BlockAidApi {
@POST("site/scan")
suspend fun scanDomain(@Body request: DomainScanRequest): DomainScanResponse
@POST("evm/json-rpc/scan")
suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse
@POST("solana/message/scan")
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.common.blockaid.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DomainScanRequest(
@Json(name = "url") val url: String,
)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.common.blockaid.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
@JsonClass(generateAdapter = true)
data class EvmTransactionScanRequest(
@Json(name = "chain") val chain: String,
@Json(name = "account_address") val accountAddress: String,
@Json(name = "method") val method: String,
@Json(name = "data") val data: RpcData,
@Json(name = "options") val options: List<String> = listOf("simulation", "validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,
)
@JsonClass(generateAdapter = true)
data class RpcData(
@Json(name = "jsonrpc") val jsonrpc: String = "2.0",
@Json(name = "method") val method: String,
@Json(name = "params") val params: String,
)

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.common.blockaid.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
@JsonClass(generateAdapter = true)
data class SolanaTransactionScanRequest(
@Json(name = "encoding") val encoding: String = "base64",
@Json(name = "chain") val chain: String,
@Json(name = "method") val method: String,
@Json(name = "options") val options: List<String> = listOf("simulation, validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,
@Json(name = "account_address") val accountAddress: String,
@Json(name = "transactions") val transactions: List<String>,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AccountSummaryResponse(
@Json(name = "assets_diffs") val assetsDiffs: List<AssetDiff>,
@Json(name = "exposures") val exposures: List<Exposure>,
)

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AssetDiff(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: Asset,
@Json(name = "in") val inTransfer: List<Transfer>? = null,
@Json(name = "out") val outTransfer: List<Transfer>? = null,
)
@JsonClass(generateAdapter = true)
data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String,
)
@JsonClass(generateAdapter = true)
data class Transfer(
@Json(name = "value") val value: String,
@Json(name = "raw_value") val rawValue: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DomainScanResponse(
@Json(name = "status") val status: String,
@Json(name = "is_malicious") val isMalicious: Boolean?,
)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Exposure(
@Json(name = "asset") val asset: Asset,
@Json(name = "spenders") val spenders: Map<String, SpenderDetails>,
)
@JsonClass(generateAdapter = true)
data class SpenderDetails(
@Json(name = "exposure") val exposure: List<ExposureDetail>,
@Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null,
)
@JsonClass(generateAdapter = true)
data class ExposureDetail(
@Json(name = "value") val value: String,
@Json(name = "raw_value") val rawValue: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimulationResponse(
@Json(name = "status") val status: String,
@Json(name = "account_summary") val accountSummary: AccountSummaryResponse,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TransactionMetadata(
@Json(name = "domain") val domain: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TransactionScanResponse(
@Json(name = "validation") val validation: ValidationResponse,
@Json(name = "simulation") val simulation: SimulationResponse,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ValidationResponse(
@Json(name = "status") val status: String,
@Json(name = "result_type") val resultType: String,
)

View file

@ -24,9 +24,9 @@ sealed class ApiConfig {
Express,
TangemTech,
StakeKit,
TangemVisaAuth,
TangemVisa,
TangemCardSdk,
BlockAid,
}
private fun initializeId(): ID {
@ -34,9 +34,9 @@ sealed class ApiConfig {
is Express -> ID.Express
is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit
is TangemVisaAuth -> ID.TangemVisaAuth
is TangemVisa -> ID.TangemVisa
is TangemCardSdk -> ID.TangemCardSdk
is BlockAid -> ID.BlockAid
}
}
@ -44,6 +44,7 @@ sealed class ApiConfig {
internal const val DEBUG_BUILD_TYPE = "debug"
internal const val INTERNAL_BUILD_TYPE = "internal"
internal const val MOCKED_BUILD_TYPE = "mocked"
internal const val EXTERNAL_BUILD_TYPE = "external"
internal const val RELEASE_BUILD_TYPE = "release"
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.utils.ProviderSuspend
import kotlinx.coroutines.flow.first
internal class BlockAid(
private val configStorage: EnvironmentConfigStorage,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val environmentConfigs = listOf(
createProdEnvironment(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.blockaid.io/v0/",
headers = buildMap {
put(
key = "X-API-KEY",
value = ProviderSuspend {
requireNotNull(
configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey,
)
},
)
put("accept", ProviderSuspend { "application/json" })
put("content-type", ProviderSuspend { "application/json" })
},
)
}

View file

@ -48,10 +48,8 @@ internal class Express(
private fun createHeaders(isProd: Boolean) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId))
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode))
}
private fun getApiKey(isProd: Boolean): String {
@ -73,6 +71,7 @@ internal class Express(
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")

View file

@ -7,15 +7,22 @@ internal class TangemVisa(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.DEV
override val environmentConfigs = listOf(
createProdEnvironment(),
createDevEnvironment(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://bff.tangem.com/",
baseUrl = "https://api.paera.com/bff/",
headers = createHeaders(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.api.common.config
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal class TangemVisaAuth(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE
override val environmentConfigs = listOf(
createStageEnvironment(),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "https://api-s.tangem.org/",
headers = createHeaders(),
)
private fun createHeaders() = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },
)
}

View file

@ -5,10 +5,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.*
/**
* Interface of Tangem Express API (new swap mechanism)
@ -17,16 +14,29 @@ import retrofit2.http.Query
interface TangemExpressApi {
@POST("assets")
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
suspend fun getAssets(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: AssetsRequestBody,
): ApiResponse<List<Asset>>
@POST("pairs")
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
suspend fun getPairs(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: PairsRequestBody,
): ApiResponse<List<SwapPair>>
@GET("providers")
suspend fun getProviders(): ApiResponse<List<ExchangeProvider>>
suspend fun getProviders(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<ExchangeProvider>>
@GET("exchange-quote")
suspend fun getExchangeQuote(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@ -40,6 +50,8 @@ interface TangemExpressApi {
@GET("exchange-data")
suspend fun getExchangeData(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@ -57,8 +69,16 @@ interface TangemExpressApi {
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
suspend fun getExchangeStatus(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("txId") txId: String,
): ApiResponse<ExchangeStatusResponse>
@POST("exchange-sent")
suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse<ExchangeSentResponseBody>
suspend fun exchangeSent(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: ExchangeSentRequestBody,
): ApiResponse<ExchangeSentResponseBody>
}

View file

@ -3,37 +3,52 @@ package com.tangem.datasource.api.onramp
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.*
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
interface OnrampApi {
@GET("currencies")
suspend fun getCurrencies(): ApiResponse<List<OnrampCurrencyDTO>>
suspend fun getCurrencies(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<OnrampCurrencyDTO>>
@GET("countries")
suspend fun getCountries(): ApiResponse<List<OnrampCountryDTO>>
suspend fun getCountries(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<OnrampCountryDTO>>
@GET("country-by-ip")
suspend fun getCountryByIp(): ApiResponse<OnrampCountryDTO>
suspend fun getCountryByIp(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<OnrampCountryDTO>
@GET("payment-methods")
suspend fun getPaymentMethods(): ApiResponse<List<PaymentMethodDTO>>
suspend fun getPaymentMethods(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<PaymentMethodDTO>>
@POST("onramp-pairs")
suspend fun getPairs(@Body body: OnrampPairsRequest): ApiResponse<List<OnrampPairDTO>>
suspend fun getPairs(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: OnrampPairsRequest,
): ApiResponse<List<OnrampPairDTO>>
@GET("onramp-quote")
suspend fun getQuote(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromCurrencyCode") fromCurrencyCode: String,
@Query("fromPrecision") fromPrecision: Int,
@Query("toContractAddress") toContractAddress: String,
@ -47,6 +62,8 @@ interface OnrampApi {
@GET("onramp-data")
suspend fun getData(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String?,
@Query("fromCurrencyCode") fromCurrencyCode: String,
@Query("fromPrecision") fromPrecision: Int,
@Query("toContractAddress") toContractAddress: String,
@ -64,5 +81,9 @@ interface OnrampApi {
): ApiResponse<OnrampDataResponse>
@GET("onramp-status")
suspend fun getStatus(@Query("txId") txId: String): ApiResponse<OnrampStatusResponse>
suspend fun getStatus(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("txId") txId: String,
): ApiResponse<OnrampStatusResponse>
}

View file

@ -32,7 +32,7 @@ data class OnrampDataJson(
val requestId: String,
@Json(name = "externalTxId")
val externalTxId: String,
val externalTxId: String?,
@Json(name = "externalTxUrl")
val externalTxUrl: String?,

View file

@ -21,7 +21,7 @@ data class OnrampStatusResponse(
val failReason: String?,
@Json(name = "externalTxId")
val externalTxId: String,
val externalTxId: String?,
@Json(name = "externalTxUrl")
val externalTxUrl: String?,
@ -91,4 +91,10 @@ enum class Status {
@Json(name = "paused")
Paused,
@Json(name = "refund-in-progress")
RefundInProgress,
@Json(name = "refunded")
Refunded,
}

View file

@ -140,6 +140,40 @@ interface TangemTechApi {
@GET("stories/{story_id}")
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
// region push notifications
@GET("notification/push_notifications_eligible_networks")
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>
@POST("user-wallets/applications/")
suspend fun createApplicationId(
@Body
body: NotificationApplicationCreateBody,
): ApiResponse<NotificationApplicationIdResponse>
@PATCH("user-wallets/applications/{application_id}")
suspend fun updatePushTokenForApplicationId(
@Path("application_id") applicationId: String,
@Body body: NotificationApplicationCreateBody,
): ApiResponse<String>
@PATCH("user-wallets/wallets/{wallet_id}/notify")
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
// endregion
// region wallets
@PATCH("user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
suspend fun associateApplicationIdWithWallets(
@Path("application_id") applicationId: String,
@Body body: List<WalletIdBody>,
): ApiResponse<Unit>
@GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
// endregion
companion object {
val marketsQuoteFields = listOf(
"price",

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CryptoNetworkResponse(
@Json(name = "id") val id: Int,
@Json(name = "networkId") val networkId: String,
@Json(name = "name") val name: String,
)

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NotificationApplicationCreateBody(
@Json(name = "pushToken") val pushToken: String? = null,
@Json(name = "platform") val platform: String? = null,
@Json(name = "device") val device: String? = null,
@Json(name = "systemVersion") val systemVersion: String? = null,
@Json(name = "language") val language: String? = null,
@Json(name = "timezone") val timezone: String? = null,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NotificationApplicationIdResponse(
@Json(name = "uid") val appId: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NotificationApplicationUpdateBody(
@Json(name = "pushToken") val pushToken: String,
@Json(name = "systemVersion") val systemVersion: String? = null,
@Json(name = "language") val language: String? = null,
@Json(name = "timezone") val timezone: String? = null,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletBody(
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
@Json(name = "name") val name: String? = null,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletIdBody(
@Json(name = "id") val walletId: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletResponse(
@Json(name = "notifyStatus") val notifyStatus: Boolean,
@Json(name = "id") val id: String,
@Json(name = "name") val name: String? = null,
)

View file

@ -1,9 +1,17 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.*
import retrofit2.http.Body
@ -11,65 +19,71 @@ import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
interface TangemVisaApi {
@GET("product_instance/activation_status")
// region: auth
@POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
@POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
@POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse<JWTResponse>
// endregion
// region: activation
@POST("v1/activation/status")
suspend fun getRemoteActivationStatus(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
@Body request: ActivationStatusRequest,
): ApiResponse<CardActivationRemoteStateResponse>
@ReadTimeout(duration = 20, TimeUnit.MINUTES)
@GET("product_instance/activation_status")
suspend fun getRemoteActivationStatusLongPoll(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
): ApiResponse<CardActivationRemoteStateResponse>
@GET("product_instance/card_wallet_acceptance")
@POST("v1/activation/acceptance/message")
suspend fun getCardWalletAcceptance(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("activation_order") activationOrderId: String,
@Query("customer_wallet_address") customerWalletAddress: String,
): ApiResponse<CardWalletDataToSignResponse>
@Body request: GetCardWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@GET("product_instance/customer_wallet_acceptance")
@POST("v1/activation/acceptance/message")
suspend fun getCustomerWalletAcceptance(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("activation_order") activationOrderId: String,
@Query("card_wallet_address") cardWalletAddress: String,
): ApiResponse<CustomerWalletDataToSignResponse>
@Body request: GetCustomerWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@POST("product_instance/activation_by_card_wallet")
@POST("v1/activation/data")
suspend fun activateByCardWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCardWalletRequest,
): ApiResponse<Unit>
@POST("product_instance/activation_by_customer_wallet")
@POST("v1/activation/data")
suspend fun activateByCustomerWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCustomerWalletRequest,
): ApiResponse<Unit>
@POST("product_instance/issuer_activation")
@POST("v1/activation/pin")
suspend fun setPinCode(
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,
): ApiResponse<Unit>
// endregion
@GET("customer/info")
suspend fun getCustomerInfo(
@Header("Authorization") authHeader: String,

View file

@ -1,32 +0,0 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
import com.tangem.datasource.api.visa.models.response.JWTResponse
import retrofit2.http.Field
import retrofit2.http.POST
import retrofit2.http.Query
interface TangemVisaAuthApi {
@POST("auth/card_wallet")
suspend fun generateNonceByWalletAddress(
@Query("card_wallet_address") cardWalletAddress: String,
): GenerateNonceResponse
@POST("auth/card_id")
suspend fun generateNonceByCard(
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
): GenerateNonceResponse
@POST("auth/get_token")
suspend fun getAccessToken(
@Query("session_id") sessionId: String,
@Query("signature") signature: String,
@Query("salt") salt: String?,
): JWTResponse
@POST("auth/refresh_token")
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse<JWTResponse>
}

View file

@ -5,22 +5,15 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ActivationByCardWalletRequest(
@Json(name = "customer_id") val customerId: String,
@Json(name = "product_instance_id") val productInstanceId: String,
@Json(name = "activation_order_id") val activationOrderId: String,
@Json(name = "data") val data: Data,
@Json(name = "order_id") val orderId: String,
@Json(name = "card_wallet") val cardWallet: CardWallet,
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
@Json(name = "otp") val otp: Otp,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "card_wallet") val cardWallet: CardWallet,
@Json(name = "otp") val otp: Otp,
)
@JsonClass(generateAdapter = true)
data class CardWallet(
@Json(name = "address") val address: String,
@Json(name = "card_wallet_confirmation") val cardWalletConfirmation: CardWalletConfirmation?,
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
)
@JsonClass(generateAdapter = true)

View file

@ -5,19 +5,12 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ActivationByCustomerWalletRequest(
@Json(name = "customer_id") val customerId: String,
@Json(name = "product_instance_id") val productInstanceId: String,
@Json(name = "activation_order_id") val activationOrderId: String,
@Json(name = "data") val data: Data,
@Json(name = "order_id") val orderId: String,
@Json(name = "customer_wallet") val customerWallet: CustomerWallet,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "customer_wallet") val customerWallet: CustomerWallet,
)
@JsonClass(generateAdapter = true)
data class CustomerWallet(
@Json(name = "address") val address: String,
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
@Json(name = "address") val customerWalletAddress: String,
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ActivationStatusRequest(
@Json(name = "card_id") val cardId: String,
@Json(name = "card_public_key") val cardPublicKey: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNoneByCardIdRequest(
@Json(name = "auth_type") val authType: String = "card_id",
@Json(name = "card_id") val cardId: String,
@Json(name = "card_public_key") val cardPublicKey: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNoneByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetAccessTokenByCardIdRequest(
@Json(name = "auth_type") val authType: String = "card_id",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
@Json(name = "salt") val salt: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetAccessTokenByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetCardWalletAcceptanceRequest(
@Json(name = "type") val type: String = "card_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetCustomerWalletAcceptanceRequest(
@Json(name = "type") val type: String = "customer_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RefreshTokenByCardIdRequest(
@Json(name = "auth_type") val authType: String = "card_id",
@Json(name = "refresh_token") val refreshToken: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RefreshTokenByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "refresh_token") val refreshToken: String,
)

View file

@ -5,14 +5,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinCodeRequest(
@Json(name = "customer_id") val customerId: String,
@Json(name = "activation_order_id") val activationOrderId: String,
@Json(name = "product_instance_id") val productInstanceId: String,
@Json(name = "data") val data: Data,
) {
data class Data(
@Json(name = "session_key") val sessionKey: String,
@Json(name = "iv") val iv: String,
@Json(name = "encrypted_pin") val encryptedPin: String,
)
}
@Json(name = "order_id") val orderId: String,
@Json(name = "session_id") val sessionId: String,
@Json(name = "iv") val iv: String,
@Json(name = "pin") val pin: String,
)

View file

@ -5,12 +5,21 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardActivationRemoteStateResponse(
@Json(name = "activation_status") val status: String,
@Json(name = "activation_order") val activationOrder: ActivationOrder?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "status") val status: String,
@Json(name = "order") val activationOrder: ActivationOrder?,
@Json(name = "stepChangeCode") val stepChangeCode: Int?,
@Json(name = "updatedAt") val updatedAt: String?,
)
@JsonClass(generateAdapter = true)
data class ActivationOrder(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)
}

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerWalletDataToSignResponse(
@Json(name = "data_for_customer_wallet") val dataForCardWallet: Data,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "hash") val hash: String,
)
}

View file

@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNonceResponse(
@Json(name = "nonce") val nonce: String,
@Json(name = "session_id") val sessionId: String,
)
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "nonce") val nonce: String,
@Json(name = "session_id") val sessionId: String,
)
}

View file

@ -5,12 +5,17 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class JWTResponse(
@Json(name = "access_token") val accessToken: String,
@Json(name = "expires_in") val expiresIn: Int,
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
@Json(name = "refresh_token") val refreshToken: String,
@Json(name = "token_type") val tokenType: String,
@Json(name = "not-before-policy") val notBeforePolicy: Int,
@Json(name = "session_state") val sessionState: String,
@Json(name = "scope") val scope: String,
)
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "access_token") val accessToken: String,
@Json(name = "expires_in") val expiresIn: Int,
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
@Json(name = "refresh_token") val refreshToken: String,
@Json(name = "token_type") val tokenType: String,
@Json(name = "not-before-policy") val notBeforePolicy: Int,
@Json(name = "session_state") val sessionState: String,
@Json(name = "scope") val scope: String,
)
}

View file

@ -4,11 +4,11 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardWalletDataToSignResponse(
@Json(name = "dataForCardWallet") val dataForCardWallet: Data,
data class VisaDataToSignResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Data(
data class Result(
@Json(name = "hash") val hash: String,
)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.appcurrency
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import kotlinx.coroutines.flow.Flow
/**
* Store of app currency data model [CurrenciesResponse.Currency]
*
[REDACTED_AUTHOR]
*/
interface AppCurrencyResponseStore {
/** Get flow of [CurrenciesResponse.Currency] */
fun get(): Flow<CurrenciesResponse.Currency?>
/** Get [CurrenciesResponse.Currency] synchronously or null */
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
}

View file

@ -0,0 +1,28 @@
package com.tangem.datasource.appcurrency
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import kotlinx.coroutines.flow.Flow
/**
* Default implementation of [AppCurrencyResponseStore]
*
* @property appPreferencesStore app preferences store
*/
internal class DefaultAppCurrencyResponseStore(
private val appPreferencesStore: AppPreferencesStore,
) : AppCurrencyResponseStore {
override fun get(): Flow<CurrenciesResponse.Currency?> {
return appPreferencesStore.getObject(PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
}
override suspend fun getSyncOrNull(): CurrenciesResponse.Currency? {
return appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
}
}

View file

@ -3,8 +3,10 @@ package com.tangem.datasource.asset.loader
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.utils.coroutines.runCatching
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -23,69 +25,63 @@ import javax.inject.Singleton
class AssetLoader @Inject constructor(
val assetReader: AssetReader,
@NetworkMoshi val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
) {
/** Load content [Content] of asset file [fileName] */
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun <reified Content> load(fileName: String): Content? {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
suspend inline fun <reified Content> load(fileName: String): Content? = runCatching(dispatchers.io) {
val json = assetReader.read(fullFileName = "$fileName.json")
moshi.adapter<Content>().fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
null
},
)
moshi.adapter<Content>().fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
null
},
)
/** Load list [V] values of asset file [fileName] */
suspend inline fun <reified V> loadList(fileName: String): List<V> {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
suspend inline fun <reified V> loadList(fileName: String): List<V> = runCatching(dispatchers.io) {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyList()
},
)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyList()
},
)
/** Load map [String] keys and [V] values of asset file [fileName] */
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
return runCatching {
val json = assetReader.read(fullFileName = "$fileName.json")
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> = runCatching(dispatchers.io) {
val json = assetReader.read(fullFileName = "$fileName.json")
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyMap()
},
)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config [$fileName] from assets")
emptyMap()
},
)
}

View file

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

View file

@ -39,12 +39,13 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemAuthVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig =
TangemVisaAuth(appVersionProvider)
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider)
@Provides
@IntoSet
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider)
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
}
@Provides
@IntoSet

View file

@ -1,8 +1,11 @@
package com.tangem.datasource.di
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.datasource.appcurrency.DefaultAppCurrencyResponseStore
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -18,4 +21,10 @@ internal object AppCurrencyDataModule {
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideAppCurrencyResponseStore(appPreferencesStore: AppPreferencesStore): AppCurrencyResponseStore {
return DefaultAppCurrencyResponseStore(appPreferencesStore)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.info.AndroidAppInfoProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AppInfoModule {
@Singleton
@Provides
fun provideAppInfoProvider(appVersionProvider: AppVersionProvider): AppInfoProvider {
return AndroidAppInfoProvider(appVersionProvider)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.*
import com.tangem.datasource.local.preferences.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -26,6 +25,7 @@ internal object AppPreferencesStoreModule {
return AppPreferencesStore(
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
moshi = moshi,
dispatchers = dispatchers,
)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,10 +16,7 @@ internal object AssetReaderModule {
@Singleton
@Provides
fun providesAsserReader(
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): AssetReader {
return AndroidAssetReader(context.assets, dispatchers)
fun providesAsserReader(@ApplicationContext context: Context): AssetReader {
return AndroidAssetReader(context.assets)
}
}

View file

@ -6,10 +6,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
import com.tangem.datasource.api.common.adapter.DateTimeAdapter
import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters
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.domain.models.scan.serialization.*
@ -38,6 +35,7 @@ class MoshiModule {
.withDefaultValue(ProviderModel.UnsupportedType),
)
.add(BigDecimalAdapter())
.add(BigIntegerAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(VisaActivationRemoteState.jsonAdapter)
@ -51,12 +49,14 @@ class MoshiModule {
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
.withSubtype(NFTCollection.Identifier.TON::class.java, "ton")
.withSubtype(NFTCollection.Identifier.Solana::class.java, "sol")
.withDefaultValue(NFTCollection.Identifier.Unknown),
)
.add(
PolymorphicJsonAdapterFactory.of(NFTAsset.Identifier::class.java, "bc")
.withSubtype(NFTAsset.Identifier.EVM::class.java, "evm")
.withSubtype(NFTAsset.Identifier.TON::class.java, "ton")
.withSubtype(NFTAsset.Identifier.Solana::class.java, "sol")
.withDefaultValue(NFTAsset.Identifier.Unknown),
)
.addLast(KotlinJsonAdapterFactory())
@ -71,6 +71,7 @@ class MoshiModule {
val adapters = MoshiJsonConverter.getTangemSdkAdapters() +
listOf(
BigDecimalAdapter(),
BigIntegerAdapter(),
WalletDerivedKeysMapAdapter(),
ScanResponseDerivedKeysMapAdapter(),
ByteArrayKeyAdapter(),
@ -98,6 +99,7 @@ class MoshiModule {
adapters = MoshiJsonConverter.getTangemSdkAdapters() +
listOf(
BigDecimalAdapter(),
BigIntegerAdapter(),
WalletDerivedKeysMapAdapter(),
ScanResponseDerivedKeysMapAdapter(),
ByteArrayKeyAdapter(),

View file

@ -4,6 +4,7 @@ import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
@ -17,7 +18,6 @@ import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.datasource.api.visa.TangemVisaAuthApi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.*
@ -191,29 +191,6 @@ internal object NetworkModule {
)
}
@Provides
@Singleton
fun provideTangemVisaAuthApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemVisaAuthApi {
return createApi<TangemVisaAuthApi>(
id = ApiConfig.ID.TangemVisaAuth,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
).applyTimeoutAnnotations()
},
)
}
@Provides
@Singleton
fun provideTangemVisaApi(
@ -282,6 +259,29 @@ internal object NetworkModule {
.create(T::class.java)
}
@Provides
@Singleton
fun provideBlockAidApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): BlockAidApi {
return createApi<BlockAidApi>(
id = ApiConfig.ID.BlockAid,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
).applyTimeoutAnnotations()
},
)
}
private inline fun <reified T> createApi(
id: ApiConfig.ID,
moshi: Moshi,

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
@ -26,21 +27,27 @@ internal object QuotesStoreModule {
@Provides
@Singleton
fun provideQuotesStore(
fun providePersistenceQuotesStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): QuotesStore {
return DefaultQuotesStore(
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "quotes") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
): DataStore<Map<String, QuotesResponse.Quote>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "quotes") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Provides
@Singleton
fun provideQuotesStore(persistenceStore: DataStore<Map<String, QuotesResponse.Quote>>): QuotesStore {
return DefaultQuotesStore(
persistenceStore = persistenceStore,
runtimeStore = RuntimeSharedStore(),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
@ -49,21 +50,29 @@ internal object StakingStoreModule {
@Provides
@Singleton
fun provideStakingBalanceStore(
fun provideYieldsBalancesPersistenceStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<Map<String, Set<YieldBalanceWrapperDTO>>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<YieldBalanceWrapperDTO>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "yield_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Provides
@Singleton
fun provideStakingBalanceStore(
persistenceStore: DataStore<Map<String, Set<YieldBalanceWrapperDTO>>>,
): StakingBalanceStore {
return DefaultStakingBalanceStore(
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<YieldBalanceWrapperDTO>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "yield_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
persistenceStore = persistenceStore,
runtimeStore = RuntimeSharedStore(),
)
}

View file

@ -5,11 +5,14 @@ import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -33,31 +36,34 @@ typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>
internal class DefaultExpressServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : ExpressServiceLoader {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>) {
override suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>) {
withContext(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWalletId)
val initializationStatus = getInitializationStatusInternal(userWallet.walletId)
try {
if (userTokens.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
userWalletId = userWallet.walletId.stringValue,
refCode = getRefCode(userWallet, appPreferencesStore),
body = AssetsRequestBody(tokensList = userTokens),
).getOrThrow()
expressAssetsStore.store(userWalletId, response)
expressAssetsStore.store(userWallet.walletId, response)
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
if (expressAssetsStore.getSyncOrNull(userWalletId) == null) {
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
initializationStatus.update { e.lceError() }
}
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -13,8 +14,8 @@ import kotlinx.coroutines.flow.Flow
*/
interface ExpressServiceLoader {
/** Update service using [userWalletId] and [userTokens] */
suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>)
/** Update service using [userWallet] and [userTokens] */
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>

View file

@ -0,0 +1,46 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.common.CardIdRangeDec
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.runBlocking
object ExpressUtils {
private const val BATCH_ID_CHANGENOW = "BB000013"
private const val BATCH_ID_PARTNER = "AF990015"
fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String {
return when {
isRing(userWallet, appPreferencesStore) -> "ring"
isChangeNow(userWallet) -> "ChangeNow"
isPartner(userWallet) -> "partner"
else -> ""
}
}
private fun isRing(selectedUserWallet: UserWallet, appPreferencesStore: AppPreferencesStore): Boolean {
val addedWalletsWithRings = runBlocking {
appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY,
default = emptySet(),
)
}
return addedWalletsWithRings.contains(selectedUserWallet.walletId.stringValue)
}
private fun isChangeNow(selectedUserWallet: UserWallet): Boolean {
val changeNowRange = CardIdRangeDec(
start = "AF99001800554008",
end = "AF99001800559994",
)
val card = selectedUserWallet.scanResponse.card
return card.batchId == BATCH_ID_CHANGENOW || changeNowRange?.contains(card.cardId) == true
}
private fun isPartner(selectedUserWallet: UserWallet): Boolean {
return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_PARTNER
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.info
import android.os.Build
import com.tangem.utils.SupportedLanguages
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import java.util.*
import javax.inject.Inject
internal class AndroidAppInfoProvider @Inject constructor(
private val appVersionProvider: AppVersionProvider,
) : AppInfoProvider {
override val platform: String
get() = "Android"
override val device: String
get() = "${Build.MANUFACTURER} ${Build.MODEL}"
override val osVersion: String
get() = Build.VERSION.RELEASE
override val language: String
get() = SupportedLanguages.getCurrentSupportedLanguageCode()
override val timezone: String
get() = TimeZone.getDefault().id
override val appVersion: String
get() = appVersionProvider.versionName
}

View file

@ -14,4 +14,5 @@ data class EnvironmentConfig(
val express: ExpressModel? = null,
val devExpress: ExpressModel? = null,
val stakeKitApiKey: String? = null,
val blockAidApiKey: String? = null,
)

View file

@ -23,6 +23,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
express = value.express,
devExpress = value.devExpress,
stakeKitApiKey = value.stakeKitApiKey,
blockAidApiKey = value.blockaidApiKey,
)
}
}

View file

@ -38,6 +38,7 @@ class EnvironmentConfigModel(
@Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?,
@Json(name = "moralisApiKey") val moralisApiKey: String?,
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
)
@JsonClass(generateAdapter = true)

View file

@ -37,8 +37,12 @@ class AppLogsStore @Inject constructor(
private val mutex = Mutex()
private val zipMutex = Mutex()
private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
private val logFile by lazy {
File(applicationContext.filesDir, PERMITTED_FILE_NAME)
}
private val logFileZip by lazy {
File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
}
private val formatter = DateTimeFormatterBuilder()
.appendDayOfMonth(2)
@ -55,12 +59,12 @@ class AppLogsStore @Inject constructor(
.toFormatter()
/** Get log file */
fun getFile(): File? = if (file.exists()) file else null
fun getFile(): File? = if (logFile.exists()) logFile else null
suspend fun getZipFile(): File? {
return zipMutex.withLock {
if (file.exists()) {
zip(listOf(file), fileZip)
if (logFile.exists()) {
zip(listOf(logFile), logFileZip)
} else {
null
}
@ -98,8 +102,8 @@ class AppLogsStore @Inject constructor(
/** Delete deprecated logs if file size exceeds [maxSize] */
fun deleteDeprecatedLogs(maxSize: Int) {
launchWithLock {
if (file.exists() && file.length() > maxSize) {
file.delete()
if (logFile.exists() && logFile.length() > maxSize) {
logFile.delete()
}
}
}
@ -117,7 +121,7 @@ class AppLogsStore @Inject constructor(
}
private fun writeMessage(tag: String, vararg messages: String) {
BufferedWriter(FileWriter(file, true)).use { writer ->
BufferedWriter(FileWriter(logFile, true)).use { writer ->
writer.append(formatter.print(DateTime.now()))
writer.append(": $tag ")
messages.forEach(writer::append)
@ -126,8 +130,8 @@ class AppLogsStore @Inject constructor(
}
private fun createFileIfNotExist() {
if (!file.exists()) {
runCatching { file.createNewFile() }
if (!logFile.exists()) {
runCatching { logFile.createNewFile() }
.onFailure(Timber::e)
}
}

View file

@ -96,21 +96,33 @@ internal class DefaultNFTRuntimeStore(
private fun NFTCollections.Content.Collections.mergeWithPrices(prices: Map<NFTAsset.Identifier, NFTSalePrice>) =
copy(
collections = this.collections?.map { data ->
data.copy(
assets = when (val assets = data.assets) {
collections = this.collections
?.map { data ->
val 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))
},
items = assets.items
.filter { !it.name.isNullOrEmpty() }
.sortedBy { it.name }
.map { asset ->
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
},
)
},
)
},
}
val assetsCount = when (assets) {
is NFTCollection.Assets.Value -> assets.items.size
else -> data.count
}
data.copy(
assets = assets,
count = assetsCount,
)
}
?.filter { it.count > 0 }
?.sortedBy { it.name },
source = this.source,
)

View file

@ -4,14 +4,17 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
object NFTSdkAssetConverter : TwoWayConverter<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)
val salePrice = asset.salePrice?.let { NFTSdkAssetSalePriceConverter(assetId).convert(it) }
?: NFTSalePrice.Empty(assetId)
return NFTAsset(
id = assetId,
collectionId = collectionId,
@ -20,13 +23,9 @@ object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
owner = asset.owner,
name = asset.name,
description = asset.description,
salePrice = asset.salePrice?.let {
NFTSalePrice.Value(
assetId = assetId,
value = it.value,
symbol = it.symbol,
)
} ?: NFTSalePrice.Empty(assetId = assetId),
amount = asset.amount,
decimals = asset.decimals,
salePrice = salePrice,
rarity = asset.rarity?.let {
NFTAsset.Rarity(
rank = it.rank,
@ -48,4 +47,42 @@ object NFTSdkAssetConverter : Converter<Pair<Network, SdkNFTAsset>, NFTAsset> {
source = StatusSource.CACHE,
)
}
override fun convertBack(value: NFTAsset): Pair<Network, SdkNFTAsset> {
val assetId = NFTSdkAssetIdentifierConverter.convertBack(value.id)
val collectionId = NFTSdkCollectionIdentifierConverter.convertBack(value.collectionId)
val salePrice = (value.salePrice as? NFTSalePrice.Value)?.let {
NFTSdkAssetSalePriceConverter(value.id).convertBack(it)
}
return value.network to SdkNFTAsset(
identifier = assetId,
collectionIdentifier = collectionId,
blockchainId = value.network.id.value,
contractType = value.contractType,
owner = value.owner,
name = value.name,
description = value.description,
amount = value.amount,
decimals = value.decimals,
salePrice = salePrice,
rarity = value.rarity?.let {
SdkNFTAsset.Rarity(
rank = it.rank,
label = it.label,
)
},
media = value.media?.let {
SdkNFTAsset.Media(
url = it.url,
mimetype = it.mimetype,
)
},
traits = value.traits.map {
SdkNFTAsset.Trait(
name = it.name,
value = it.value,
)
},
)
}
}

View file

@ -9,10 +9,15 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter<SdkNFTAsset.Identifier,
is SdkNFTAsset.Identifier.EVM -> NFTAsset.Identifier.EVM(
tokenId = value.tokenId,
tokenAddress = value.tokenAddress,
contractType = NFTAsset.Identifier.EVM.ContractType.valueOf(value.contractType.name),
)
is SdkNFTAsset.Identifier.TON -> NFTAsset.Identifier.TON(
tokenAddress = value.tokenAddress,
)
is SdkNFTAsset.Identifier.Solana -> NFTAsset.Identifier.Solana(
tokenAddress = value.tokenAddress,
cnft = value.cnft,
)
is SdkNFTAsset.Identifier.Unknown -> NFTAsset.Identifier.Unknown
}
@ -20,10 +25,15 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter<SdkNFTAsset.Identifier,
is NFTAsset.Identifier.EVM -> SdkNFTAsset.Identifier.EVM(
tokenId = value.tokenId,
tokenAddress = value.tokenAddress,
contractType = SdkNFTAsset.Identifier.EVM.ContractType.valueOf(value.contractType.name),
)
is NFTAsset.Identifier.TON -> SdkNFTAsset.Identifier.TON(
tokenAddress = value.tokenAddress,
)
is NFTAsset.Identifier.Solana -> SdkNFTAsset.Identifier.Solana(
tokenAddress = value.tokenAddress,
cnft = value.cnft,
)
is NFTAsset.Identifier.Unknown -> SdkNFTAsset.Identifier.Unknown
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.local.nft.converter
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.blockchain.nft.models.NFTAsset.SalePrice as SDKSalePrice
internal class NFTSdkAssetSalePriceConverter(
private val assetId: NFTAsset.Identifier,
) : TwoWayConverter<SDKSalePrice, NFTSalePrice.Value> {
override fun convert(value: SDKSalePrice): NFTSalePrice.Value {
return NFTSalePrice.Value(
assetId = assetId,
value = value.value,
symbol = value.symbol,
)
}
override fun convertBack(value: NFTSalePrice.Value): SDKSalePrice {
return SDKSalePrice(
symbol = value.symbol,
value = value.value,
)
}
}

View file

@ -12,6 +12,9 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
is SdkNFTCollection.Identifier.TON -> NFTCollection.Identifier.TON(
contractAddress = value.contractAddress,
)
is SdkNFTCollection.Identifier.Solana -> NFTCollection.Identifier.Solana(
collection = value.collection,
)
is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown
}
@ -22,6 +25,9 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
is NFTCollection.Identifier.TON -> SdkNFTCollection.Identifier.TON(
contractAddress = value.contractAddress,
)
is NFTCollection.Identifier.Solana -> SdkNFTCollection.Identifier.Solana(
collection = value.collection,
)
is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown
}
}

View file

@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Application preferences store.
@ -19,6 +20,7 @@ import com.squareup.moshi.Types
*/
class AppPreferencesStore(
val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
private val preferencesDataStore: DataStore<Preferences>,
) : DataStore<Preferences> by preferencesDataStore {

View file

@ -123,6 +123,12 @@ object PreferencesKeys {
val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
val NOTIFICATIONS_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "notificationsEnabledStates") }
// endregion
// region Promo
fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId")

View file

@ -5,23 +5,25 @@ import androidx.datastore.preferences.core.edit
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import com.tangem.datasource.local.preferences.AppPreferencesStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
/** Get flow of nullable data [T] by string [key] */
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
val adapter = moshi.adapter(T::class.java)
return data.map { preferences ->
preferences[key]?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}.distinctUntilChanged()
return flow {
val adapter = moshi.adapter(T::class.java)
emitAll(
data.map { preferences ->
preferences[key]?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}.distinctUntilChanged(),
)
}
}
/**
@ -32,16 +34,19 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
* @see getObjectList
* */
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
return data.map {
try {
it[key]?.let(adapter::fromJson) ?: default
} catch (e: JsonDataException) {
default
}
}.distinctUntilChanged()
return flow {
val adapter = moshi.adapter(T::class.java)
emitAll(
data.map {
try {
it[key]?.let(adapter::fromJson) ?: default
} catch (e: JsonDataException) {
default
}
}.distinctUntilChanged(),
)
}
}
/**
* Get nullable data [T] by string [key]
*
@ -49,26 +54,27 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
*
* @see getObjectListSync
* */
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
return data.firstOrNull()
?.get(key)
?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? =
withContext(dispatchers.io) {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
data.firstOrNull()
?.get(key)
?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}
}
}
/** Get data [T] by string [key]. If data is not found, it returns [default] */
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
key: Preferences.Key<String>,
default: T,
): T {
): T = withContext(dispatchers.io) {
val adapter = moshi.adapter(T::class.java)
return data.firstOrNull()
data.firstOrNull()
?.get(key)
?.let {
try {
@ -87,37 +93,47 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
*
* @see storeObjectList
* */
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
edit { it[key] = adapter.toJson(value) }
}
@Suppress("OptionalUnit")
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T): Unit =
withContext(dispatchers.io) {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
edit { it[key] = adapter.toJson(value) }
}
/** Store list of data [value] by string [key] */
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
edit { it[key] = adapter.toJson(value) }
}
suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferences.Key<String>, value: List<T>) =
withContext(dispatchers.io) {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
edit { it[key] = adapter.toJson(value) }
}
/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */
inline fun <reified T> AppPreferencesStore.getObjectList(key: Preferences.Key<String>): Flow<List<T>?> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged()
return flow {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
emitAll(
data.map {
it[key]?.let(adapter::fromJson)
}.distinctUntilChanged(),
)
}
}
/** Get list of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> =
withContext(dispatchers.io) {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Store map with [String] key and value [V] by string [key] */
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
key: Preferences.Key<String>,
value: Map<String, V>,
) {
) = withContext(dispatchers.io) {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
@ -125,37 +141,47 @@ suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
}
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> =
withContext(dispatchers.io) {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */
inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Flow<Map<String, V>> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return flow {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() }
emitAll(
data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() },
)
}
}
/** Get set of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> =
withContext(dispatchers.io) {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Get flow of set of [T] by string [key], or empty if data is not found */
inline fun <reified T> AppPreferencesStore.getObjectSet(key: Preferences.Key<String>): Flow<Set<T>> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.map {
it[key]?.let(adapter::fromJson) ?: emptySet()
return flow {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
emitAll(
data.map {
it[key]?.let(adapter::fromJson) ?: emptySet()
},
)
}
}

View file

@ -22,7 +22,9 @@ internal class SharedPreferencesKeyMigration(
private val keyName: String,
) : DataMigration<Preferences> {
private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
private val legacyPrefs by lazy {
context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
}
override suspend fun cleanUp() {
val sharedPrefsEditor = legacyPrefs.edit()

View file

@ -10,13 +10,24 @@ import com.tangem.utils.extensions.orZero
/**
* Converter from [QuotesResponse.Quote] to [Quote.Value]
*
* @property isCached flag that determines whether the quote is a cache
* @property source status source
*
[REDACTED_AUTHOR]
*/
internal class QuoteConverter(private val isCached: Boolean) :
class QuoteConverter(
private val source: StatusSource,
) :
Converter<Map.Entry<String, QuotesResponse.Quote>, Quote.Value> {
/**
* Secondary constructor
*
* @param isCached flag that determines whether the quote is a cache
*/
constructor(isCached: Boolean) : this(
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): Quote.Value {
val (currencyId, quote) = value
@ -24,7 +35,7 @@ internal class QuoteConverter(private val isCached: Boolean) :
rawCurrencyId = CryptoCurrency.RawID(currencyId),
fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
source = source,
)
}
}

View file

@ -4,6 +4,8 @@ import androidx.datastore.core.DataStore
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
internal class DefaultStakingYieldsStore(
private val dataStore: DataStore<List<YieldDTO>>,
@ -17,9 +19,18 @@ internal class DefaultStakingYieldsStore(
return dataStore.data.firstOrNull().orEmpty()
}
override suspend fun getSyncWithTimeout(): List<YieldDTO>? {
return withTimeoutOrNull(timeout = YIELDS_WAITING_TIMEOUT, block = { get().firstOrNull() })
}
override suspend fun store(items: List<YieldDTO>) {
dataStore.updateData { _ ->
items
}
}
private companion object {
val YIELDS_WAITING_TIMEOUT = 15.seconds
}
}

View file

@ -9,5 +9,7 @@ interface StakingYieldsStore {
suspend fun getSync(): List<YieldDTO>
suspend fun getSyncWithTimeout(): List<YieldDTO>?
suspend fun store(items: List<YieldDTO>)
}

View file

@ -7,7 +7,7 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceItem
import com.tangem.utils.converter.Converter
internal class YieldBalanceConverter(
class YieldBalanceConverter(
private val source: StatusSource,
) : Converter<YieldBalanceWrapperDTO, YieldBalance> {

View file

@ -13,7 +13,7 @@ interface UserWalletsStore {
fun getSyncOrNull(key: UserWalletId): UserWallet?
suspend fun getSyncStrict(key: UserWalletId): UserWallet
fun getSyncStrict(key: UserWalletId): UserWallet
suspend fun getAllSyncOrNull(): List<UserWallet>?

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.*
import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
@ -34,7 +35,6 @@ private val API_CONFIGS = setOf(
Express(configManager, expressAuthProvider, appVersionProvider),
TangemTech(appVersionProvider, appAuthProvider),
StakeKit(stakeKitAuthProvider),
TangemVisaAuth(appVersionProvider),
TangemVisa(appVersionProvider),
)
@ -49,9 +49,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
@Before
fun setup() {
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getUserId() } returns EXPRESS_USER_ID
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { expressAuthProvider.getRefCode() } returns EXPRESS_REF_CODE
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
@ -87,9 +85,9 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
is Express -> createExpressModel()
is TangemTech -> createTangemTechModel()
is StakeKit -> createStakeKitModel()
is TangemVisaAuth -> createVisaAuthModel()
is TangemVisa -> createVisaModel()
is TangemCardSdk -> createTangemCardSdkModel()
is BlockAid -> createBlockAidSdkModel()
}
}
@ -100,6 +98,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
@ -115,6 +114,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> "[REDACTED_ENV_URL]"
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> "https://express.tangem.com/v1/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
@ -127,9 +127,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
}
},
"user-id" to ProviderSuspend { EXPRESS_USER_ID },
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
"refcode" to ProviderSuspend { EXPRESS_REF_CODE },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
@ -177,26 +175,12 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
)
}
private fun createVisaAuthModel(): Model {
return Model(
id = ApiConfig.ID.TangemVisaAuth,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "https://api-s.tangem.org/",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
),
),
)
}
private fun createVisaModel(): Model {
return Model(
id = ApiConfig.ID.TangemVisa,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://bff.tangem.com/",
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
@ -215,6 +199,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
)
}
private fun createBlockAidSdkModel(): Model {
return Model(
id = ApiConfig.ID.BlockAid,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.blockaid.io/v0/",
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]

View file

@ -7,6 +7,7 @@ import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.every
@ -22,7 +23,11 @@ class AssetLoaderTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val assetLoader = AssetLoader(
assetReader = assetReader,
moshi = moshi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun load() = runTest {

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.asset.reader
import android.content.res.AssetManager
import com.google.common.truth.Truth
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
@ -15,7 +14,7 @@ import java.io.IOException
internal class AndroidAssetReaderTest {
private val assetManager = mockk<AssetManager>()
private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider())
private val assetReader = AndroidAssetReader(assetManager)
@Test
fun read_content() = runTest {

View file

@ -3,19 +3,16 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
class BuyCurrencyDeepLink(
val onReceive: (externalTxId: String) -> Unit,
val onReceive: () -> Unit,
) : DeepLink() {
override val uri = BUY_REDIRECT_DEEPLINK
override fun onReceive(params: Map<String, String>) {
onReceive(
params["merchant_transaction_id"] ?: return,
)
onReceive()
}
companion object {
const val ONRAMP_REDIRECT_DEEPLINK = "https://tangem.com/success?action=dismissBrowser"
private const val BUY_REDIRECT_DEEPLINK = "tangem://redirect?action=dismissBrowser"
}
}

View file

@ -514,6 +514,9 @@
<string name="no_account_polkadot">La cuenta de destino no está activa. Envíe %s o más para activar la cuenta.</string>
<string name="no_account_send_to_create">Para crear una cuenta, envíe fondos a esta dirección</string>
<string name="no_trustline_xlm_asset">La cuenta de destino no tiene una Trustline para el activo que se envía.</string>
<string name="notification_referral_promo_button">Únete ahora</string>
<string name="notification_referral_promo_text">Comparte tu código y gana 5 USDT por venta. Tu amigo obtiene un 10% de descuento.</string>
<string name="notification_referral_promo_title">¡Obtén RECOMPENSAS por cada amigo!</string>
<string name="onboarding_access_code_feature_1_description">Deba configurar un único código de acceso para proteger todss sus dispositivos.</string>
<string name="onboarding_access_code_feature_1_title">Proteger</string>
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más adelante</string>

View file

@ -875,15 +875,15 @@
<string name="staking_notification_additional_ada_deposit_text">Cardanoネットワークでステーキングする場合、残高全体が使用されます。追加の2ADAは確保され、ステーキング解除後に返却されます。ステーキング中、ADAはロック解除されたままです。</string>
<string name="staking_notification_additional_ada_deposit_title">ADAステーキングの詳細</string>
<string name="staking_notification_claim_rewards_text">獲得した報酬はあなたのアドレスに直接送られ、すぐ使用可能です。</string>
<string name="staking_notification_earn_rewards_text">Tangemで暗号資産をステーキングしよう</string>
<string name="staking_notification_earn_rewards_text_daily">Tangemで暗号資産をステーキングしよう</string>
<string name="staking_notification_earn_rewards_text_hourly">Tangemで暗号資産をステーキングしよう</string>
<string name="staking_notification_earn_rewards_text_monthly">Tangemで暗号資産をステーキングしよう</string>
<string name="staking_notification_earn_rewards_text">Tangemでは暗号資産をステーキングできます</string>
<string name="staking_notification_earn_rewards_text_daily">Tangemでは暗号資産をステーキングできます</string>
<string name="staking_notification_earn_rewards_text_hourly">Tangemでは暗号資産をステーキングできます</string>
<string name="staking_notification_earn_rewards_text_monthly">Tangemでは暗号資産をステーキングできます</string>
<string name="staking_notification_earn_rewards_text_period_day">ステーキングを行うと%1$sを受け取ることができます。ステーキング報酬は毎日受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_hour">ステーキングを行うと%1$sを受け取ることができます。ステーキング報酬は1時間ごとに受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_month">ステーキングを行うと%1$sを受け取ることができます。ステーキング報酬は毎月受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_week">ステーキングを行うと%1$sを受け取ることができます。ステーキング報酬は毎週受け取れます。</string>
<string name="staking_notification_earn_rewards_text_weekly">Tangemで暗号資産をステーキングしよう</string>
<string name="staking_notification_earn_rewards_text_weekly">Tangemでは暗号資産をステーキングできます</string>
<string name="staking_notification_earn_rewards_title">ステーキング報酬を獲得</string>
<string name="staking_notification_low_staked_balance_text">残りのステーキング残高が少なすぎてステーキングを解除できません。最小のステーキング解除残高を満たすには、さらにステーキングする必要があります。</string>
<string name="staking_notification_low_staked_balance_title">ステーキング残高が低いです</string>
@ -1044,6 +1044,8 @@
<string name="transaction_history_transaction_validator">バリデーター: %s</string>
<string name="transfer_min_amount_error">最小%s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">最小取引金額は%1$sです。</string>
<string name="tron_will_be_send_token_fee_description">Tronネットワークの人気トークンの手数料は高めです。TRXをステーキングすると、より安く、あるいは無料で取引できます。</string>
<string name="tron_will_be_send_token_fee_title">Tronネットワーク手数料を節約</string>
<string name="try_to_load_data_again_button_title">もう一度やり直してください</string>
<string name="twin_error_same_card">同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。</string>
<string name="twin_error_wrong_twin">間違ったツインカードをスキャンしました。別のカードをお試しください。</string>

View file

@ -526,6 +526,9 @@
<string name="no_account_polkadot">Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис.</string>
<string name="no_account_send_to_create">Для створення акаунту надішліть кошти на цю адресу</string>
<string name="no_trustline_xlm_asset">Акаунт одержувача не містить трастлайну для активу, що надсилається.</string>
<string name="notification_referral_promo_button">Приєднатися</string>
<string name="notification_referral_promo_text">Поділіться промокодом — заробіть 5 USDT з кожної покупки. Ваші друзі отримають знижку 10% на картку Tangem!</string>
<string name="notification_referral_promo_title">Отримуй бонуси за кожного друга!</string>
<string name="onboarding_access_code_feature_1_description">Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець</string>
<string name="onboarding_access_code_feature_1_title">Захист</string>
<string name="onboarding_access_code_feature_2_description">Встановіть індивідуальний код доступу для кожної картки або кільця пізніше.</string>

View file

@ -535,6 +535,13 @@
<string name="nft_details_base_information">Base information</string>
<string name="nft_details_chain">Chain</string>
<string name="nft_details_contract_address">Contract Address</string>
<string name="nft_details_info_chain">stub</string>
<string name="nft_details_info_contract_address">stub</string>
<string name="nft_details_info_rarity_label">stub</string>
<string name="nft_details_info_rarity_rank">stub</string>
<string name="nft_details_info_token_address">stub</string>
<string name="nft_details_info_token_id">stub</string>
<string name="nft_details_info_token_standard">stub</string>
<string name="nft_details_last_sale_price">Last sale price</string>
<string name="nft_details_rarity_label">Rarity label</string>
<string name="nft_details_rarity_rank">Rarity rank</string>
@ -1056,6 +1063,8 @@
<string name="transaction_history_transaction_validator">validator: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">The minimum transaction amount is %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Tron network fees for popular tokens are higher. Stake some TRX for cheaper or free transactions.</string>
<string name="tron_will_be_send_token_fee_title">Save on Tron network fees</string>
<string name="try_to_load_data_again_button_title">Try again</string>
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
<string name="twin_error_wrong_twin">You\'ve scanned wrong twin card. Please try another one</string>
@ -1308,6 +1317,8 @@
<string name="wc_connection_reqeust_would_like">Would like to</string>
<string name="wc_connection_request">Connection request</string>
<string name="wc_connections">Connections</string>
<string name="wc_contents">Contents</string>
<string name="wc_copy_data_button_text">Copy data</string>
<string name="wc_disconnect_all">Disconnect all</string>
<string name="wc_disconnect_all_alert_desc">Text about discnected all dApps</string>
<string name="wc_disconnect_all_alert_title">Disconect All dApps</string>
@ -1318,6 +1329,10 @@
<string name="wc_no_sessions_title">No sessions</string>
<string name="wc_notification_security_risk_subtitle">This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets</string>
<string name="wc_notification_security_risk_title">Known security risk</string>
<string name="wc_request_from">Request from</string>
<string name="wc_signature_type">Signature Type</string>
<string name="wc_transaction_request">Transaction request</string>
<string name="wc_transaction_request_title">Transaction request</string>
<string name="wc_wallet_connect">Wallet connect</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>

View file

@ -0,0 +1,17 @@
package com.tangem.core.ui.components.artwork
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Immutable
data class ArtworkUM(
val verifiedArtwork: ImmutableList<Byte>? = null,
val defaultUrl: String,
) {
constructor(bytes: ByteArray?, defaultUrl: String) : this(
verifiedArtwork = bytes?.toList()?.toImmutableList(),
defaultUrl = defaultUrl,
)
}

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