Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-15 14:25:11 +03:00
commit 5ee5b4f61b
563 changed files with 13917 additions and 4692 deletions

View file

@ -16,6 +16,9 @@ enum class ApiEnvironment {
@Json(name = "DEV_2")
DEV_2,
@Json(name = "DEV_3")
DEV_3,
@Json(name = "STAGE")
STAGE,

View file

@ -28,6 +28,7 @@ internal class Express(
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createDev2Environment(),
createDev3Environment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
@ -60,6 +61,12 @@ internal class Express(
headers = createHeaders(isProd = false),
)
private fun createDev3Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV_3,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",

View file

@ -43,7 +43,7 @@ internal class TangemPay(
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.paera.com/bff/",
baseUrl = "https://api.us.paera.com/bff/",
headers = createHeaders(),
)

View file

@ -73,6 +73,7 @@ internal class TangemTech(
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey

View file

@ -17,6 +17,7 @@ data class CustomerMeResponse(
@Json(name = "product_instance") val productInstance: ProductInstance?,
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
@Json(name = "kyc") val kyc: Kyc?,
@Json(name = "depositAddress") val depositAddress: String?,
@Json(name = "card") val card: Card?,
@Json(name = "balance") val balance: Balance?,
)

View file

@ -156,7 +156,7 @@ interface TangemTechApi {
@Path("walletId") walletId: String,
@Header("If-Match") eTag: String,
@Body body: SaveWalletAccountsResponse,
): ApiResponse<Unit>
): ApiResponse<GetWalletAccountsResponse>
@GET("/v1/wallets/{walletId}/accounts/archived")
suspend fun getWalletArchivedAccounts(

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldModuleStatusResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
import retrofit2.http.Body
import retrofit2.http.GET
@ -15,13 +15,13 @@ import retrofit2.http.Query
interface YieldSupplyApi {
@GET("api/v1/yield/markets")
suspend fun getYieldMarkets(@Query("chainId") chainId: Int? = null): ApiResponse<YieldMarketsResponse>
suspend fun getYieldMarkets(@Query("chainId") chainId: String? = null): ApiResponse<YieldMarketsResponse>
@GET("api/v1/yield/token/{chainId}/{tokenAddress}")
suspend fun getYieldTokenStatus(
@Path("chainId") chainId: Int,
@Path("tokenAddress") tokenAddress: String,
): ApiResponse<YieldTokenStatusResponse>
): ApiResponse<YieldSupplyMarketTokenDto>
@GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart")
suspend fun getYieldTokenChart(

View file

@ -2,21 +2,9 @@ package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldMarketsResponse(
@Json(name = "tokens") val marketDtos: List<MarketDto>,
@Json(name = "tokens") val marketDtos: List<YieldSupplyMarketTokenDto>,
@Json(name = "lastUpdatedAt") val lastUpdated: String,
) {
@JsonClass(generateAdapter = true)
data class MarketDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: String? = null,
@Json(name = "tokenName") val tokenName: String? = null,
@Json(name = "apy") val apy: BigDecimal,
@Json(name = "isActive") val isActive: Boolean,
@Json(name = "chainId") val chainId: Int? = null,
)
}
)

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldTokenStatusResponse(
data class YieldSupplyMarketTokenDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: String? = null,
@Json(name = "tokenName") val tokenName: String? = null,

View file

@ -2,8 +2,37 @@ package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.utils.SerializeNulls
@JsonClass(generateAdapter = true)
data class SaveWalletAccountsResponse(
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
)
@Json(name = "accounts") val accounts: List<AccountDTO>,
) {
@SerializeNulls
@JsonClass(generateAdapter = true)
data class AccountDTO(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String?,
@Json(name = "derivation") val derivationIndex: Int,
@Json(name = "icon") val icon: String,
@Json(name = "iconColor") val iconColor: String,
)
companion object {
operator fun invoke(accounts: List<WalletAccountDTO>): SaveWalletAccountsResponse {
return SaveWalletAccountsResponse(
accounts = accounts.map { accountDto ->
AccountDTO(
id = accountDto.id,
name = accountDto.name,
derivationIndex = accountDto.derivationIndex,
icon = accountDto.icon,
iconColor = accountDto.iconColor,
)
},
)
}
}
}

View file

@ -9,9 +9,8 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.utils.SerializeNullsFactory
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -28,6 +27,7 @@ class MoshiModule {
@NetworkMoshi
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(SerializeNullsFactory)
.add(
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
.withSubtype(ProviderModel.Public::class.java, "public")
@ -38,8 +38,8 @@ class MoshiModule {
.add(BigIntegerAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
// .add(VisaActivationRemoteState.jsonAdapter)
// .add(VisaCardActivationStatus.jsonAdapter)
.add(
NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java)
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
@ -84,8 +84,8 @@ class MoshiModule {
val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters()
return Moshi.Builder().apply {
add(VisaActivationRemoteState.jsonAdapter)
add(VisaCardActivationStatus.jsonAdapter)
// add(VisaActivationRemoteState.jsonAdapter)
// add(VisaCardActivationStatus.jsonAdapter)
adapters.forEach { this.add(it) }
typedAdapters.forEach { add(it.key, it.value) }
addLast(KotlinJsonAdapterFactory())

View file

@ -4,11 +4,11 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -34,7 +34,7 @@ object YieldSupplyModule {
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<YieldMarketToken>(),
types = listTypes<YieldSupplyMarketTokenDto>(),
defaultValue = emptyList(),
),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },

View file

@ -9,7 +9,7 @@ import java.math.BigDecimal
/**
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
*
* @see [com.tangem.domain.tokens.model.NetworkStatus]
* @see [com.tangem.domain.models.network.NetworkStatus]
*/
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed interface NetworkStatusDM {
@ -41,8 +41,8 @@ sealed interface NetworkStatusDM {
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: Map<String, YieldSupplyStatus?> = emptyMap(),
@Json(name = "amounts") val amounts: List<CurrencyAmount>,
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: List<YieldSupplyStatus>,
) : NetworkStatusDM
/**
@ -107,10 +107,44 @@ sealed interface NetworkStatusDM {
}
}
@JsonClass(generateAdapter = true)
data class CurrencyAmount(
@Json(name = "id") val id: CurrencyId,
@Json(name = "amount") val amount: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class YieldSupplyStatus(
@Json(name = "id") val id: CurrencyId,
@Json(name = "is_active") val isActive: Boolean,
@Json(name = "is_initialized") val isInitialized: Boolean,
@Json(name = "is_allowed_to_spend") val isAllowedToSpend: Boolean,
)
@JsonClass(generateAdapter = true)
data class CurrencyId(
@Json(name = "value") val value: String,
) {
companion object Companion {
const val CONTRACT_ADDRESS_DELIMITER = '\u2693' // ⚓
fun createCoinId(coinId: String): CurrencyId {
return CurrencyId(value = coinId)
}
fun createTokenId(rawTokenId: String?, contractAddress: String): CurrencyId {
return CurrencyId(
value = buildString {
if (rawTokenId != null) {
append(rawTokenId)
}
append(CONTRACT_ADDRESS_DELIMITER)
append(contractAddress)
},
)
}
}
}
}

View file

@ -12,5 +12,7 @@ interface TangemPayStorage {
suspend fun getOrderId(customerWalletAddress: String): String?
suspend fun clear(customerWalletAddress: String)
suspend fun clearOrderId(customerWalletAddress: String)
suspend fun clearAll(customerWalletAddress: String)
}

View file

@ -1,21 +1,21 @@
package com.tangem.datasource.local.yieldsupply
import androidx.datastore.core.DataStore
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
internal class DefaultYieldMarketsStore(
private val persistenceStore: DataStore<List<YieldMarketToken>>,
private val persistenceStore: DataStore<List<YieldSupplyMarketTokenDto>>,
) : YieldMarketsStore {
override fun get(): Flow<List<YieldMarketToken>> = persistenceStore.data
override fun get(): Flow<List<YieldSupplyMarketTokenDto>> = persistenceStore.data
override suspend fun getSyncOrNull(): List<YieldMarketToken>? {
override suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>? {
return persistenceStore.data.firstOrNull()
}
override suspend fun store(items: List<YieldMarketToken>) {
override suspend fun store(items: List<YieldSupplyMarketTokenDto>) {
persistenceStore.updateData { _ -> items }
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.local.yieldsupply
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import kotlinx.coroutines.flow.Flow
interface YieldMarketsStore {
fun get(): Flow<List<YieldMarketToken>>
fun get(): Flow<List<YieldSupplyMarketTokenDto>>
suspend fun getSyncOrNull(): List<YieldMarketToken>?
suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>?
suspend fun store(items: List<YieldMarketToken>)
suspend fun store(items: List<YieldSupplyMarketTokenDto>)
}

View file

@ -0,0 +1,5 @@
package com.tangem.datasource.utils
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class SerializeNulls

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.utils
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
/**
* Factory to serialize nulls in Moshi if the class is annotated with [SerializeNulls].
*
[REDACTED_AUTHOR]
*/
internal object SerializeNullsFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = Types.getRawType(type)
if (!rawType.isAnnotationPresent(SerializeNulls::class.java)) {
return null
}
val nextAdapter: JsonAdapter<Any> = moshi.nextAdapter(this, type, annotations)
return nextAdapter.serializeNulls()
}
}

View file

@ -64,9 +64,15 @@ class NetworkStatusDMSerializationTest {
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amounts = mapOf("ETH" to BigDecimal("1.2345")),
yieldSupplyStatuses = mapOf(
"ETH" to NetworkStatusDM.YieldSupplyStatus(
amounts = listOf(
NetworkStatusDM.CurrencyAmount(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
amount = BigDecimal("1.2345"),
),
),
yieldSupplyStatuses = listOf(
NetworkStatusDM.YieldSupplyStatus(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
@ -91,9 +97,15 @@ class NetworkStatusDMSerializationTest {
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amounts = mapOf("ETH" to BigDecimal("1.2345")),
yieldSupplyStatuses = mapOf(
"ETH" to NetworkStatusDM.YieldSupplyStatus(
amounts = listOf(
NetworkStatusDM.CurrencyAmount(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
amount = BigDecimal("1.2345"),
),
),
yieldSupplyStatuses = listOf(
NetworkStatusDM.YieldSupplyStatus(
id = NetworkStatusDM.CurrencyId.createCoinId("ethereum"),
isActive = false,
isInitialized = false,
isAllowedToSpend = false,

View file

@ -0,0 +1,51 @@
package com.tangem.datasource.utils
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
// --- DTO ---
@SerializeNulls
@JsonClass(generateAdapter = true)
data class UserWithNulls(val id: String?, val name: String?)
@JsonClass(generateAdapter = true)
data class UserWithoutNulls(val id: String?, val name: String?)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SerializeNullsFactoryTest {
private val moshi = Moshi.Builder()
.add(SerializeNullsFactory)
.build()
@Test
fun `should serialize nulls for annotated class`() {
val adapter = moshi.adapter(UserWithNulls::class.java)
val json = adapter.toJson(UserWithNulls(id = null, name = "John"))
assertThat(json).isEqualTo("""{"id":null,"name":"John"}""")
}
@Test
fun `should skip nulls for non-annotated class`() {
val adapter = moshi.adapter(UserWithoutNulls::class.java)
val json = adapter.toJson(UserWithoutNulls(id = null, name = "John"))
assertThat(json).isEqualTo("""{"name":"John"}""")
}
@Test
fun `should deserialize annotated class correctly`() {
val adapter = moshi.adapter(UserWithNulls::class.java)
val json = """{"id":null,"name":"Jane"}"""
val result = adapter.fromJson(json)
assertThat(result).isEqualTo(UserWithNulls(id = null, name = "Jane"))
}
}