Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-28 14:11:23 +03:00
commit 0470e1f010
1144 changed files with 48463 additions and 11166 deletions

View file

@ -6,20 +6,17 @@ import com.tangem.datasource.utils.RequestHeader
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
* Express [ApiConfig]
*
* @property environmentConfig environment config
* @property expressAuthProvider express auth provider
* @property appVersionProvider app version provider
* @property appInfoProvider app info provider
*/
internal class Express(
private val environmentConfig: EnvironmentConfig,
private val expressAuthProvider: ExpressAuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
@ -102,7 +99,7 @@ internal class Express(
private fun createHeaders(isProd: Boolean) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appInfoProvider).values)
}
private fun getApiKey(isProd: Boolean): String {

View file

@ -6,14 +6,12 @@ import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
* Gasless transactions [ApiConfig]
*/
internal class GaslessTxService(
private val authProvider: AuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
@ -50,7 +48,7 @@ internal class GaslessTxService(
)
private fun createHeaders(environment: ApiEnvironment) = buildMap {
putAll(RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(RequestHeader.AppVersionPlatformHeaders(appInfoProvider).values)
put(
key = "Authorization",
value = ProviderSuspend {

View file

@ -5,14 +5,12 @@ import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
* News [ApiConfig]
[REDACTED_AUTHOR]
*/
internal class News(
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
private val authProvider: AuthProvider,
) : ApiConfig() {
@ -64,7 +62,7 @@ internal class News(
apiEnvironment = Provider { environment },
).values,
)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appInfoProvider).values)
}
private companion object {

View file

@ -3,11 +3,11 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
import com.tangem.utils.info.AppInfoProvider
internal sealed class TangemPay(
private val environmentConfig: EnvironmentConfig,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
@ -52,7 +52,7 @@ internal sealed class TangemPay(
)
private fun createHeaders(apiEnvironment: ApiEnvironment) = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"version" to ProviderSuspend { appInfoProvider.appVersion },
"platform" to ProviderSuspend { "Android" },
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
)
@ -74,8 +74,8 @@ internal sealed class TangemPay(
class Bff(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
) : TangemPay(environmentConfig, appVersionProvider) {
appInfoProvider: AppInfoProvider,
) : TangemPay(environmentConfig, appInfoProvider) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
@ -93,8 +93,8 @@ internal sealed class TangemPay(
class Auth(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
) : TangemPay(environmentConfig, appVersionProvider) {
appInfoProvider: AppInfoProvider,
) : TangemPay(environmentConfig, appInfoProvider) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"

View file

@ -5,11 +5,9 @@ import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/** TangemTech [ApiConfig] */
internal class TangemTech(
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
@ -62,7 +60,7 @@ internal class TangemTech(
private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap {
putAll(from = RequestHeader.TangemApiKeyHeader(authProvider, Provider { apiEnvironment }).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
}
}

View file

@ -6,12 +6,10 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/** YieldSupply [ApiConfig] */
internal class YieldSupply(
private val environmentConfig: EnvironmentConfig,
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
@ -66,7 +64,7 @@ internal class YieldSupply(
put(key = "api-key", value = ProviderSuspend {
getApiKey(apiEnvironment)
})
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
}

View file

@ -85,6 +85,18 @@ interface TangemPayApi {
@Body body: FreezeUnfreezeCardRequest,
): ApiResponse<FreezeUnfreezeCardResponse>
@GET("v1/fees/{type}")
suspend fun getFee(
@Header("Authorization") authHeader: String,
@Path("type") type: String,
): ApiResponse<FeeResponse>
@POST("v1/customer/card/reissue")
suspend fun reissueCard(
@Header("Authorization") authHeader: String,
@Body body: ReissueCardRequest,
): ApiResponse<ReissueCardResponse>
@POST("v1/customer/card/withdraw/data")
suspend fun getWithdrawData(
@Header("Authorization") authHeader: String,
@ -96,4 +108,11 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String,
@Body body: WithdrawRequest,
): ApiResponse<WithdrawResponse>
@PATCH("v1/customer/card/{card_id}")
suspend fun updateCard(
@Header("Authorization") authHeader: String,
@Body body: UpdateCardRequest,
@Path("card_id") cardId: String,
): ApiResponse<UpdateCardDisplayNameResponse>
}

View file

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

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UpdateCardRequest(
@Json(name = "display_name") val displayName: String? = null,
@Json(name = "card_limit") val cardLimit: CardLimit? = null,
) {
@JsonClass(generateAdapter = true)
data class CardLimit(
@Json(name = "amount") val amount: String,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@ -29,6 +30,9 @@ data class CustomerMeResponse(
@Json(name = "status") val status: Status,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
@Json(name = "display_name") val displayName: String?,
@Json(name = "actual_card_limit") val actualCardLimit: CardLimit?,
@Json(name = "admin_card_limit") val adminCardLimit: CardLimit?,
) {
@JsonClass(generateAdapter = false)
enum class Status {
@ -70,6 +74,12 @@ data class CustomerMeResponse(
}
}
@JsonClass(generateAdapter = true)
data class CardLimit(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "period_type") val periodType: String,
)
@JsonClass(generateAdapter = true)
data class PaymentAccount(
@Json(name = "id") val id: String,

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FeeResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "type") val type: String,
@Json(name = "amount") val amount: String,
@Json(name = "currency") val currency: String,
@Json(name = "description") val description: String,
)
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ReissueCardResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "order_id") val orderId: String,
@Json(name = "status") val status: String,
)
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UpdateCardDisplayNameResponse(
@Json(name = "result") val result: Result?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "display_name") val displayName: String?,
)
}

View file

@ -42,7 +42,8 @@ data class UserTokensResponse(
return otherToken.contractAddress == this.contractAddress &&
otherToken.networkId == this.networkId &&
otherToken.derivationPath == this.derivationPath &&
otherToken.decimals == this.decimals
otherToken.decimals == this.decimals &&
otherToken.dynamicAddressesEnabled == this.dynamicAddressesEnabled
}
override fun hashCode(): Int = calculateHashCode(
@ -50,6 +51,7 @@ data class UserTokensResponse(
networkId.hashCode(),
derivationPath.hashCode(),
decimals.hashCode(),
dynamicAddressesEnabled.hashCode(),
)
}

View file

@ -7,7 +7,6 @@ import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -23,13 +22,11 @@ internal object ApiConfigsModule {
fun provideExpressConfig(
environmentConfig: EnvironmentConfig,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig {
return Express(
environmentConfig = environmentConfig,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}
@ -48,55 +45,47 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemTechConfig(
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = TangemTech(
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,
)
fun provideTangemTechConfig(authProvider: AuthProvider, appInfoProvider: AppInfoProvider): ApiConfig {
return TangemTech(
authProvider = authProvider,
appInfoProvider = appInfoProvider,
)
}
@Provides
@IntoSet
fun provideNewsConfig(
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = News(
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
authProvider = authProvider,
)
fun provideNewsConfig(authProvider: AuthProvider, appInfoProvider: AppInfoProvider): ApiConfig {
return News(
appInfoProvider = appInfoProvider,
authProvider = authProvider,
)
}
@Provides
@IntoSet
fun provideYieldSupplyConfig(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = YieldSupply(
environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,
)
): ApiConfig {
return YieldSupply(
environmentConfig = environmentConfig,
authProvider = authProvider,
appInfoProvider = appInfoProvider,
)
}
@Provides
@IntoSet
fun provideTangemPayBffConfig(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider)
fun provideTangemPayBffConfig(environmentConfig: EnvironmentConfig, appInfoProvider: AppInfoProvider): ApiConfig {
return TangemPay.Bff(environmentConfig, appInfoProvider)
}
@Provides
@IntoSet
fun provideTangemPayAuthConfig(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider)
fun provideTangemPayAuthConfig(environmentConfig: EnvironmentConfig, appInfoProvider: AppInfoProvider): ApiConfig {
return TangemPay.Auth(environmentConfig, appInfoProvider)
}
@Provides
@IntoSet
@ -112,14 +101,9 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideGaslessServiceConfig(
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig {
fun provideGaslessServiceConfig(authProvider: AuthProvider, appInfoProvider: AppInfoProvider): ApiConfig {
return GaslessTxService(
authProvider = authProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}

View file

@ -1,21 +0,0 @@
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

@ -52,7 +52,7 @@ class MoshiModule {
.withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created")
.withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status")
.withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card")
.withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card")
.withSubtype(PaymentAccountStatusValueDM.ActiveAccount::class.java, "active_account")
.withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
)
.add(

View file

@ -2,7 +2,9 @@ package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,4 +22,12 @@ internal object TangemPayStoresModule {
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideTangemPayReissueCardStore(): TangemPayReissueCardStore {
return DefaultTangemPayReissueCardStore(
feeStore = RuntimeDataStore(),
)
}
}

View file

@ -11,7 +11,7 @@ object ExpressUtils {
private const val BATCH_ID_CHANGENOW = "BB000013"
private const val BATCH_ID_PARTNER = "AF990015"
fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String? {
fun getRefCode(userWallet: UserWallet?, appPreferencesStore: AppPreferencesStore): String? {
return when (userWallet) {
is UserWallet.Cold -> {
when {

View file

@ -1,27 +0,0 @@
package com.tangem.datasource.info
import android.os.Build
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() = Locale.getDefault().language
override val timezone: String
get() = TimeZone.getDefault().id
override val appVersion: String
get() = appVersionProvider.versionName
override val isHuaweiDevice: Boolean
get() = Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true) ||
Build.BRAND.equals("HUAWEI", ignoreCase = true)
}

View file

@ -10,6 +10,7 @@ data class EnvironmentConfig(
val mercuryoWidgetId: String = "",
val mercuryoSecret: String = "",
val amplitudeApiKey: String = "",
val amplitudeApiKeyDev: String? = null,
val appsFlyerApiKey: String = "",
val appsAppId: String = "",
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),

View file

@ -28,6 +28,7 @@ internal object GeneratedEnvironmentConfigConverter {
mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret,
blockchainSdkConfig = createBlockchainSdkConfig(),
amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey,
amplitudeApiKeyDev = GeneratedEnvironmentConfig.amplitudeApiKeyDev,
appsFlyerApiKey = AppsFlyer.appsFlyerDevKey,
appsAppId = AppsFlyer.appsFlyerAppID,
walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId,

View file

@ -29,11 +29,12 @@ sealed interface NetworkStatusDM {
/**
* Verified
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
* @property yieldSupplyStatuses yield supply statuses
*/
@NameLabel("amounts")
data class Verified(
@ -65,6 +66,11 @@ sealed interface NetworkStatusDM {
@Json(name = "error_message") val errorMessage: String,
) : NetworkStatusDM
/**
* Id
*
* @property value blockchain id [com.tangem.blockchain.common.Blockchain.id]
*/
@JsonClass(generateAdapter = true)
data class ID(
@Json(name = "value") val value: String,

View file

@ -16,6 +16,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PR
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration
import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration
import com.tangem.datasource.local.preferences.utils.SwapCurrencyIdMigration
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
@ -80,6 +81,7 @@ internal object PreferencesDataStore {
legacyKeyName = LEGACY_DEFAULT_KEY_NAME,
keyName = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY.name,
),
SwapCurrencyIdMigration(),
CleanupKeyMigration(key = APP_LOGS_KEY),
CleanupKeyMigration(key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY),
CleanupKeyMigration(key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY),

View file

@ -136,7 +136,7 @@ object PreferencesKeys {
val HAS_HAD_FIRST_TOP_UP_KEY by lazy { stringPreferencesKey(name = "hasHadFirstTopUp") }
val PENDING_DISCOVERY_SYNC_KEY by lazy { stringPreferencesKey(name = "pendingDiscoverySync") }
val PENDING_ASSETS_DISCOVERY_KEY by lazy { stringPreferencesKey(name = "pendingAssetsDiscovery") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }

View file

@ -0,0 +1,151 @@
package com.tangem.datasource.local.preferences.utils
import androidx.datastore.core.DataMigration
import androidx.datastore.preferences.core.Preferences
import com.tangem.datasource.local.preferences.PreferencesKeys
/**
* Migrates cached CryptoCurrency.ID strings from old blockchain.id format to new networkId format.
*
* After refactoring, Network.rawId stores backendId values (e.g. "ethereum") instead of
* blockchain.id values (e.g. "ETH"). CryptoCurrency.ID body contains this value, so cached IDs
* like "coin⟨ETH⟩ethereum" must become "coin⟨ethereum⟩ethereum".
*
* Affected DataStore keys: [PreferencesKeys.SWAP_TRANSACTIONS_KEY],
* [PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY].
*/
internal class SwapCurrencyIdMigration : DataMigration<Preferences> {
override suspend fun shouldMigrate(currentData: Preferences): Boolean {
return currentData.contains(PreferencesKeys.SWAP_TRANSACTIONS_KEY) ||
currentData.contains(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY)
}
override suspend fun migrate(currentData: Preferences): Preferences {
val mutablePrefs = currentData.toMutablePreferences()
currentData[PreferencesKeys.SWAP_TRANSACTIONS_KEY]?.let { json ->
mutablePrefs[PreferencesKeys.SWAP_TRANSACTIONS_KEY] = migrateJson(json)
}
currentData[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY]?.let { json ->
mutablePrefs[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY] = migrateJson(json)
}
return mutablePrefs.toPreferences()
}
override suspend fun cleanUp() {
// nothing to clean up
}
/**
* Replaces old blockchain.id values with networkId values inside CryptoCurrency.ID strings
* found anywhere in the JSON. Works by finding all `oldId` and `oldId` patterns and
* replacing the old ID with the new one.
*/
private fun migrateJson(json: String): String {
var result = json
for ((oldId, newId) in BLOCKCHAIN_ID_TO_NETWORK_ID) {
// Body without derivation path: ⟨oldId⟩ → ⟨newId⟩
result = result.replace("$BODY_START$oldId$BODY_END", "$BODY_START$newId$BODY_END")
// Body with derivation path: ⟨oldId→ → ⟨newId→
result = result.replace(
"$BODY_START$oldId$DERIVATION_DELIMITER",
"$BODY_START$newId$DERIVATION_DELIMITER",
)
}
return result
}
private companion object {
const val BODY_START = '\u27E8' // ⟨
const val BODY_END = '\u27E9' // ⟩
const val DERIVATION_DELIMITER = '\u2192' // →
/** Mapping of old blockchain.id → new networkId (only entries where values differ). */
val BLOCKCHAIN_ID_TO_NETWORK_ID = mapOf(
"ARBITRUM-ONE" to "arbitrum-one",
"ARBITRUM/test" to "arbitrum-one/test",
"AVALANCHE" to "avalanche",
"AVALANCHE/test" to "avalanche/test",
"BINANCE" to "binancecoin",
"BINANCE/test" to "binancecoin/test",
"BSC" to "binance-smart-chain",
"BSC/test" to "binance-smart-chain/test",
"BTC" to "bitcoin",
"BTC/test" to "bitcoin/test",
"BCH" to "bitcoin-cash",
"BCH/test" to "bitcoin-cash/test",
"CARDANO-S" to "cardano",
"DOGE" to "dogecoin",
"DUC" to "ducatus",
"ETH" to "ethereum",
"ETH/test" to "ethereum/test",
"ETC" to "ethereum-classic",
"ETC/test" to "ethereum-classic/test",
"ETH-Pow" to "ethereum-pow-iou",
"ETH-Pow/test" to "ethereum-pow-iou/test",
"FTM" to "fantom",
"FTM/test" to "fantom/test",
"GNO" to "xdai",
"KAS" to "kaspa",
"KAS/test" to "kaspa/test",
"KAVA" to "kava",
"KAVA/test" to "kava/test",
"Kusama" to "kusama",
"LTC" to "litecoin",
"NEAR" to "near-protocol",
"NEAR/test" to "near-protocol/test",
"NEXA" to "nexa",
"NEXA/test" to "nexa/test",
"OPTIMISM" to "optimistic-ethereum",
"Polkadot" to "polkadot",
"POLYGON" to "polygon-pos",
"POLYGON/test" to "polygon-pos/test",
"RSK" to "rootstock",
"SOLANA" to "solana",
"SOLANA/test" to "solana/test",
"TELOS" to "telos",
"TELOS/test" to "telos/test",
"The-Open-Network" to "the-open-network",
"The-Open-Network/test" to "the-open-network/test",
"TRON" to "tron",
"TRON/test" to "tron/test",
"XLM" to "stellar",
"XLM/test" to "stellar/test",
"XRP" to "xrp",
"XTZ" to "tezos",
"DASH" to "dash",
"xdc" to "xdc-network",
"xdc/test" to "xdc-network/test",
"hedera" to "hedera-hashgraph",
"hedera/test" to "hedera-hashgraph/test",
"areon" to "areon-network",
"areon/test" to "areon-network/test",
"pls" to "pulsechain",
"pls/test" to "pulsechain/test",
"zkSyncEra" to "zksync",
"zkSyncEra/test" to "zksync/test",
"polygonZkEVM" to "polygon-zkevm",
"polygonZkEVM/test" to "polygon-zkevm/test",
"flare" to "flare-network",
"flare/test" to "flare-network/test",
"playa3ull" to "playa3ull-games",
"sei" to "sei-network",
"sei/test" to "sei-network/test",
"casper" to "casper-network",
"casper/test" to "casper-network/test",
"odyssey" to "dione",
"odyssey/test" to "dione/test",
"hyperliquid" to "hyperevm",
"hyperliquid/test" to "hyperevm/test",
"quai" to "quai-network",
"quai/test" to "quai-network/test",
"manta/test" to "manta-pacific/test",
"dischain" to "ethereumfair",
)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.datasource.local.visa
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
internal class DefaultTangemPayReissueCardStore(
private val feeStore: RuntimeDataStore<TangemPayReissueCardFee>,
) : TangemPayReissueCardStore {
override suspend fun storeReissueFee(
userWalletId: UserWalletId,
tangemPayReissueCardFee: TangemPayReissueCardFee,
) {
feeStore.store(userWalletId.stringValue, tangemPayReissueCardFee)
}
override suspend fun getReissueFee(userWalletId: UserWalletId): TangemPayReissueCardFee? {
return feeStore.getSyncOrNull(userWalletId.stringValue)
}
override suspend fun storeReissueOrderId(cardId: String, orderId: String) {
// TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs
}
override suspend fun getOrderId(cardId: String): String? {
// TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs
return null
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
interface TangemPayReissueCardStore {
suspend fun storeReissueFee(userWalletId: UserWalletId, tangemPayReissueCardFee: TangemPayReissueCardFee)
suspend fun getReissueFee(userWalletId: UserWalletId): TangemPayReissueCardFee?
suspend fun storeReissueOrderId(cardId: String, orderId: String)
suspend fun getOrderId(cardId: String): String?
}

View file

@ -4,6 +4,7 @@ package com.tangem.datasource.local.visa.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.serialization.SerializedBigDecimal
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal
@ -37,17 +38,14 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "issuing_card") val marker: Boolean = true,
) : PaymentAccountStatusValueDM
@NameLabel("active_card")
data class ActiveCard(
@Json(name = "active_card") val isLocked: Boolean,
@NameLabel("active_account")
data class ActiveAccount(
@Json(name = "customer_id") val customerId: String,
@Json(name = "card_id") val cardId: String,
@Json(name = "last_four_digits") val lastFourDigits: String,
@Json(name = "currency_code") val currencyCode: String,
@Json(name = "deposit_address") val depositAddress: String?,
@Json(name = "is_pin_set") val isPinSet: Boolean,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
@Json(name = "cards") val cards: List<TangemPayCard>,
) : PaymentAccountStatusValueDM
@NameLabel("card_issue_failed")
@ -70,4 +68,15 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "token_contract_address") val tokenContractAddress: String,
@Json(name = "balance") val balance: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class TangemPayCard(
@Json(name = "id") val id: String,
@Json(name = "has_pin_code") val hasPinCode: Boolean,
@Json(name = "display_name") val displayName: String?,
@Json(name = "actual_daily_limit") val actualDailyLimit: SerializedBigDecimal?,
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
@Json(name = "is_frozen") val isFrozen: Boolean,
@Json(name = "last_digits") val lastDigits: String,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.utils
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
@ -34,7 +35,11 @@ class MoshiDataStoreSerializer<T>(
override suspend fun readFrom(input: InputStream): T {
return input.bufferedReader().use { reader ->
adapter.fromJson(reader.readText()) ?: defaultValue
try {
adapter.fromJson(reader.readText()) ?: defaultValue
} catch (e: Exception) {
throw CorruptionException("Failed to deserialize data", e)
}
}
}

View file

@ -1,14 +1,11 @@
package com.tangem.datasource.utils
import android.os.Build
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader.checkHeaderValueOrEmpty
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import java.util.Locale
import java.util.TimeZone
/**
@ -29,17 +26,16 @@ sealed class RequestHeader(vararg pairs: Pair<String, ProviderSuspend<String>>)
)
class AppVersionPlatformHeaders(
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
) : RequestHeader(
"system_version" to ProviderSuspend { appInfoProvider.osVersion },
"version" to ProviderSuspend { appVersionProvider.versionName },
"version" to ProviderSuspend { appInfoProvider.appVersion },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"language" to ProviderSuspend { appInfoProvider.language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
"device" to ProviderSuspend { appInfoProvider.device.checkHeaderValueOrEmpty() },
)
/**

View file

@ -51,21 +51,18 @@ class ApiConfigTest {
Express(
environmentConfig = environmentConfig,
expressAuthProvider = mockk(),
appVersionProvider = mockk(),
appInfoProvider = mockk(),
)
}
ApiConfig.ID.YieldSupply -> {
YieldSupply(
environmentConfig = environmentConfig,
appVersionProvider = mockk(),
authProvider = appAuthProvider,
appInfoProvider = mockk(),
)
}
ApiConfig.ID.TangemTech -> {
TangemTech(
appVersionProvider = mockk(),
authProvider = appAuthProvider,
appInfoProvider = mockk(),
)
@ -73,23 +70,21 @@ class ApiConfigTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
ApiConfig.ID.TangemPay -> TangemPay.Bff(
environmentConfig = environmentConfig,
appVersionProvider = mockk(),
appInfoProvider = mockk(),
)
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
environmentConfig = environmentConfig,
appVersionProvider = mockk(),
appInfoProvider = mockk(),
)
ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
ApiConfig.ID.News -> News(
appVersionProvider = mockk(),
authProvider = appAuthProvider,
appInfoProvider = mockk(),
)
ApiConfig.ID.GaslessTxService -> GaslessTxService(
authProvider = appAuthProvider,
appVersionProvider = mockk(),
appInfoProvider = mockk(),
)
}

View file

@ -19,7 +19,6 @@ import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.test.core.ProvideTestModels
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
@ -38,7 +37,6 @@ import java.util.TimeZone
internal class ProdApiConfigsManagerTest {
private val environmentConfig = createMockEnvironmentConfig()
private val appVersionProvider = mockk<AppVersionProvider>()
private val expressAuthProvider = mockk<ExpressAuthProvider>()
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
private val p2pEthPoolAuthProvider = mockk<P2PEthPoolAuthProvider>()
@ -52,14 +50,13 @@ internal class ProdApiConfigsManagerTest {
@BeforeEach
fun setup() {
clearMocks(
appVersionProvider,
expressAuthProvider,
stakeKitAuthProvider,
appAuthProvider,
appInfoProvider,
)
every { appVersionProvider.versionName } returns VERSION_NAME
every { appInfoProvider.appVersion } returns VERSION_NAME
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { p2pEthPoolAuthProvider.getApiKey() } returns P2P_API_KEY
@ -71,6 +68,8 @@ internal class ProdApiConfigsManagerTest {
coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
every { appInfoProvider.osVersion } returns "Android 16"
every { appInfoProvider.language } returns Locale.getDefault().toLanguageTag()
every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}"
manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
}
@ -94,21 +93,18 @@ internal class ProdApiConfigsManagerTest {
Express(
environmentConfig = environmentConfig,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}
ApiConfig.ID.YieldSupply -> {
YieldSupply(
environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,
)
}
ApiConfig.ID.TangemTech -> {
TangemTech(
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,
)
@ -116,23 +112,21 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay.Bff(
environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
ApiConfig.ID.News -> News(
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,
)
ApiConfig.ID.GaslessTxService -> GaslessTxService(
authProvider = appAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}
@ -195,7 +189,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME },
"system_version" to ProviderSuspend { "Android 16" },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"language" to ProviderSuspend { Locale.getDefault().toLanguageTag().checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
@ -218,7 +212,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"language" to ProviderSuspend { Locale.getDefault().toLanguageTag().checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
@ -241,7 +235,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"language" to ProviderSuspend { Locale.getDefault().toLanguageTag().checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
@ -316,7 +310,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"language" to ProviderSuspend { Locale.getDefault().toLanguageTag().checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
@ -394,7 +388,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"language" to ProviderSuspend { Locale.getDefault().toLanguageTag().checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},

View file

@ -0,0 +1,224 @@
package com.tangem.datasource.local.preferences.utils
import androidx.datastore.preferences.core.mutablePreferencesOf
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.preferences.PreferencesKeys
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class SwapCurrencyIdMigrationTest {
private val migration = SwapCurrencyIdMigration()
// region shouldMigrate
@Test
fun `shouldMigrate returns true when swap transactions key exists`() = runTest {
val prefs = mutablePreferencesOf(PreferencesKeys.SWAP_TRANSACTIONS_KEY to "[]")
assertThat(migration.shouldMigrate(prefs)).isTrue()
}
@Test
fun `shouldMigrate returns true when last swapped currency key exists`() = runTest {
val prefs = mutablePreferencesOf(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to "[]")
assertThat(migration.shouldMigrate(prefs)).isTrue()
}
@Test
fun `shouldMigrate returns true when both keys exist`() = runTest {
val prefs = mutablePreferencesOf(
PreferencesKeys.SWAP_TRANSACTIONS_KEY to "[]",
PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to "[]",
)
assertThat(migration.shouldMigrate(prefs)).isTrue()
}
@Test
fun `shouldMigrate returns false when no keys exist`() = runTest {
val prefs = mutablePreferencesOf()
assertThat(migration.shouldMigrate(prefs)).isFalse()
}
// endregion
// region migrate — coin IDs without derivation path
@Test
fun `migrates simple coin ID - ETH to ethereum`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}ETH${BE}ethereum"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}ethereum${BE}ethereum"))
}
@Test
fun `migrates simple coin ID - BTC to bitcoin`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}BTC${BE}bitcoin"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}bitcoin${BE}bitcoin"))
}
@Test
fun `migrates BSC to binance-smart-chain`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}BSC${BE}binancecoin"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}binance-smart-chain${BE}binancecoin"))
}
// endregion
// region migrate — coin IDs with derivation path
@Test
fun `migrates coin ID with derivation path`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}ETH${DP}12367123${BE}ethereum"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}ethereum${DP}12367123${BE}ethereum"))
}
@Test
fun `migrates POLYGON with derivation path`() = runTest {
val result = migrateLastSwapped(currencyIdJson("coin${BS}POLYGON${DP}99999${BE}polygon-pos"))
assertThat(result).isEqualTo(currencyIdJson("coin${BS}polygon-pos${DP}99999${BE}polygon-pos"))
}
// endregion
// region migrate — token IDs
@Test
fun `migrates token ID with contract address`() = runTest {
val result = migrateLastSwapped(currencyIdJson("token${BS}ETH${BE}usdt${CA}0xdAC17"))
assertThat(result).isEqualTo(currencyIdJson("token${BS}ethereum${BE}usdt${CA}0xdAC17"))
}
@Test
fun `migrates token ID with derivation path and contract address`() = runTest {
val result = migrateLastSwapped(
currencyIdJson("token${BS}ETH${DP}12345${BE}usdt${CA}0xdAC17"),
)
assertThat(result).isEqualTo(
currencyIdJson("token${BS}ethereum${DP}12345${BE}usdt${CA}0xdAC17"),
)
}
// endregion
// region migrate — swap transactions (both from and to IDs)
@Test
fun `migrates both fromCryptoCurrencyId and toCryptoCurrencyId`() = runTest {
val from = "coin${BS}ETH${BE}ethereum"
val to = "coin${BS}BTC${BE}bitcoin"
val oldJson = "[{\"fromCryptoCurrencyId\":\"$from\",\"toCryptoCurrencyId\":\"$to\"}]"
val expectedFrom = "coin${BS}ethereum${BE}ethereum"
val expectedTo = "coin${BS}bitcoin${BE}bitcoin"
val expected = "[{\"fromCryptoCurrencyId\":\"$expectedFrom\",\"toCryptoCurrencyId\":\"$expectedTo\"}]"
val result = migrateSwapTransactions(oldJson)
assertThat(result).isEqualTo(expected)
}
// endregion
// region migrate — no-op cases
@Test
fun `does not modify already migrated IDs`() = runTest {
val json = currencyIdJson("coin${BS}ethereum${BE}ethereum")
val result = migrateLastSwapped(json)
assertThat(result).isEqualTo(json)
}
@Test
fun `does not modify IDs where blockchain id equals networkId`() = runTest {
val json = currencyIdJson("coin${BS}cosmos${BE}cosmos")
val result = migrateLastSwapped(json)
assertThat(result).isEqualTo(json)
}
@Test
fun `does not modify empty list`() = runTest {
val result = migrateLastSwapped("[]")
assertThat(result).isEqualTo("[]")
}
// endregion
// region migrate — multiple entries
@Test
fun `migrates multiple entries in list`() = runTest {
val old1 = currencyIdValue("coin${BS}ETH${BE}ethereum")
val old2 = currencyIdValue("coin${BS}TRON${BE}tron")
val oldJson = "[$old1,$old2]"
val new1 = currencyIdValue("coin${BS}ethereum${BE}ethereum")
val new2 = currencyIdValue("coin${BS}tron${BE}tron")
val expected = "[$new1,$new2]"
val result = migrateLastSwapped(oldJson)
assertThat(result).isEqualTo(expected)
}
// endregion
// region migrate — preserves unrelated keys
@Test
fun `preserves unrelated preference keys`() = runTest {
val unrelatedKey = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY
val unrelatedValue = "some_value"
val prefs = mutablePreferencesOf(
PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to currencyIdJson("coin${BS}ETH${BE}ethereum"),
unrelatedKey to unrelatedValue,
)
val result = migration.migrate(prefs)
assertThat(result[unrelatedKey]).isEqualTo(unrelatedValue)
}
// endregion
// region helpers
private suspend fun migrateLastSwapped(json: String): String? {
val prefs = mutablePreferencesOf(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to json)
val result = migration.migrate(prefs)
return result[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY]
}
private suspend fun migrateSwapTransactions(json: String): String? {
val prefs = mutablePreferencesOf(PreferencesKeys.SWAP_TRANSACTIONS_KEY to json)
val result = migration.migrate(prefs)
return result[PreferencesKeys.SWAP_TRANSACTIONS_KEY]
}
private fun currencyIdJson(id: String): String = "[${currencyIdValue(id)}]"
private fun currencyIdValue(id: String): String = "{\"cryptoCurrencyId\":\"$id\"}"
private companion object {
const val BS = '\u27E8' // ⟨ body start
const val BE = '\u27E9' // ⟩ body end
const val DP = '\u2192' // → derivation path delimiter
const val CA = '\u2693' // ⚓ contract address delimiter
}
// endregion
}