Updated on 2026-08-14
This commit is contained in:
commit
c306b16a7b
1308 changed files with 41564 additions and 8590 deletions
1
core/ab-tests/.gitignore
vendored
Normal file
1
core/ab-tests/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
31
core/ab-tests/build.gradle.kts
Normal file
31
core/ab-tests/build.gradle.kts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.abtests"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Amplitude experiment */
|
||||
implementation(deps.amplitude.experiment)
|
||||
}
|
||||
8
core/ab-tests/detekt-baseline-debug.xml
Normal file
8
core/ab-tests/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>NullableToStringCall:AmplitudeABTestsManager.kt$AmplitudeABTestsManager$${variant.key}</ID>
|
||||
<ID>NullableToStringCall:AmplitudeABTestsManager.kt$AmplitudeABTestsManager$${variant.payload}</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.core.abtests.di
|
||||
|
||||
import android.app.Application
|
||||
import com.tangem.core.abtests.BuildConfig
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ABTestsManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideABTestsManager(
|
||||
application: Application,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ABTestsManager {
|
||||
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||
StubABTestsManager()
|
||||
} else {
|
||||
AmplitudeABTestsManager(
|
||||
application = application,
|
||||
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.abtests.manager
|
||||
|
||||
interface ABTestsManager {
|
||||
|
||||
fun init()
|
||||
|
||||
fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?)
|
||||
|
||||
fun removeUserProperties()
|
||||
|
||||
fun getValue(key: String, defaultValue: String): String
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.core.abtests.manager.impl
|
||||
|
||||
import android.app.Application
|
||||
import com.amplitude.experiment.Experiment
|
||||
import com.amplitude.experiment.ExperimentClient
|
||||
import com.amplitude.experiment.ExperimentConfig
|
||||
import com.amplitude.experiment.ExperimentUser
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AmplitudeABTestsManager(
|
||||
val application: Application,
|
||||
val apiKeyProvider: Provider<String>,
|
||||
val scope: CoroutineScope,
|
||||
) : ABTestsManager {
|
||||
|
||||
private lateinit var client: ExperimentClient
|
||||
|
||||
override fun init() {
|
||||
if (::client.isInitialized) {
|
||||
Timber.w("AB Tests manager already initialized, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
client = Experiment.initializeWithAmplitudeAnalytics(
|
||||
application = application,
|
||||
apiKey = apiKeyProvider(),
|
||||
config = ExperimentConfig
|
||||
.builder()
|
||||
.automaticFetchOnAmplitudeIdentityChange(true)
|
||||
.build(),
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
client.fetch().get()
|
||||
val allVariants = client.all()
|
||||
logAllVariants(allVariants)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Failed to fetch AB test variants")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
|
||||
val userProperties = mutableMapOf<String, Any>()
|
||||
batch?.let { userProperties[AnalyticsParam.BATCH] = it }
|
||||
productType?.let { userProperties[AnalyticsParam.PRODUCT_TYPE] = it }
|
||||
firmware?.let { userProperties[AnalyticsParam.FIRMWARE] = it }
|
||||
|
||||
client.setUser(
|
||||
ExperimentUser
|
||||
.builder()
|
||||
.userId(userId)
|
||||
.userProperties(userProperties)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeUserProperties() {
|
||||
client.setUser(ExperimentUser())
|
||||
}
|
||||
|
||||
override fun getValue(key: String, defaultValue: String): String {
|
||||
return client.variant(key).value ?: defaultValue
|
||||
}
|
||||
|
||||
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
Timber.d("AB Tests: Fetched ${allVariants.size} variants")
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
|
||||
if (allVariants.isEmpty()) {
|
||||
Timber.d("No variants available")
|
||||
} else {
|
||||
allVariants.entries.forEachIndexed { index, (key, variant) ->
|
||||
Timber.d("[${index + 1}/${allVariants.size}] Key: $key")
|
||||
Timber.d(" → Value: ${variant.value ?: "null"}")
|
||||
Timber.d(" → Payload: ${variant.payload}")
|
||||
Timber.d(" → Key: ${variant.key}")
|
||||
Timber.d("-".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SEPARATOR_LENGTH = 50
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.abtests.manager.impl
|
||||
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
|
||||
internal class StubABTestsManager : ABTestsManager {
|
||||
|
||||
override fun init() {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun removeUserProperties() {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun getValue(key: String, defaultValue: String): String {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,7 @@ sealed class AnalyticsParam {
|
|||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object HotWallet : ScreensSources("Hot Wallet")
|
||||
data object TangemPay : ScreensSources("Tangem Pay")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
return paramsInterceptors.remove(interceptorId)
|
||||
}
|
||||
|
||||
override fun setUserId(userWalletId: String) {
|
||||
override fun setUserId(userId: String) {
|
||||
analyticsScope.launch {
|
||||
val userIdHash = userWalletId.hexToBytes()
|
||||
val userIdHash = userId.hexToBytes()
|
||||
.calculateSha256()
|
||||
.toHexString()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
package com.tangem.core.analytics.utils
|
||||
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AnalyticsContextProxy {
|
||||
interface TrackingContextProxy {
|
||||
|
||||
fun setContext(scanResponse: ScanResponse)
|
||||
|
||||
fun setContext(userWallet: UserWallet)
|
||||
|
||||
fun addContext(userWallet: UserWallet)
|
||||
|
||||
fun setHotWalletContext()
|
||||
|
||||
fun eraseContext()
|
||||
|
||||
fun addContext(scanResponse: ScanResponse)
|
||||
|
||||
fun addHotWalletContext()
|
||||
|
||||
fun removeContext()
|
||||
}
|
||||
|
|
@ -49,12 +49,8 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
|
|
@ -93,9 +89,9 @@ fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
|||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
entries.forEach { entry ->
|
||||
add(entry)
|
||||
if (index != entries.lastIndex) add(",\n") else add("\n")
|
||||
add(",\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
|
|
@ -117,5 +113,15 @@ fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
|||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
|
||||
// Remove redundant public visibility modifiers
|
||||
val generatedFile = File(outputPackageDir, "com/tangem/core/configtoggle/$generatedFileName.kt")
|
||||
if (generatedFile.exists()) {
|
||||
val content = generatedFile.readText()
|
||||
val fixedContent = content
|
||||
.replace("public object ", "object ")
|
||||
.replace("public val ", "val ")
|
||||
generatedFile.writeText(fixedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
core/config-toggles/detekt-baseline-debug.xml
Normal file
10
core/config-toggles/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>DoubleMutabilityForCollection:DevExcludedBlockchainsManager.kt$DevExcludedBlockchainsManager$private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()</ID>
|
||||
<ID>DoubleMutabilityForCollection:DevFeatureTogglesManager.kt$DevFeatureTogglesManager$private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()</ID>
|
||||
<ID>Indentation:ExcludedBlockchainToggles.kt$ExcludedBlockchainToggles$ </ID>
|
||||
<ID>Indentation:FeatureToggles.kt$FeatureToggles$ </ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "scroll",
|
||||
"name": "plasma",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
@ -19,6 +19,10 @@
|
|||
"name": "STAKING_CARDANO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_ETH_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -50,5 +54,9 @@
|
|||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "FEED_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
|
|
|||
|
|
@ -90,10 +90,6 @@ dependencies {
|
|||
implementation(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
17
core/datasource/detekt-baseline-debug.xml
Normal file
17
core/datasource/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>CastNullableToNonNullableType:ApiResponseCallAdapterFactory.kt$ApiResponseCallAdapterFactory$as</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultWalletManagersStore.kt$DefaultWalletManagersStore${ it.wallet.blockchain == walletManager.wallet.blockchain && it.wallet.publicKey.derivationPath == walletManager.wallet.publicKey.derivationPath }</ID>
|
||||
<ID>NestedScopeFunctions:PendingActionConverter.kt$PendingActionConverter$let { amount -> PendingAction.PendingActionArgs.Amount( required = amount.required, minimum = amount.minimum, maximum = amount.maximum, ) }</ID>
|
||||
<ID>NestedScopeFunctions:PendingActionConverter.kt$PendingActionConverter$let { duration -> PendingAction.PendingActionArgs.Duration( required = duration.required, minimum = duration.minimum, maximum = duration.maximum, ) }</ID>
|
||||
<ID>NestedScopeFunctions:PendingActionConverter.kt$PendingActionConverter$let { tronResource -> PendingAction.PendingActionArgs.TronResource( required = tronResource.required, options = tronResource.options, ) }</ID>
|
||||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withConnectTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withReadTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withWriteTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$apiConfigs + (apiConfig to environment)</ID>
|
||||
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$val apiConfig = apiConfigs.keys.firstOrNull { it.id.name == id } ?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")</ID>
|
||||
<ID>UnusedImports:NetworkModule.kt$import com.tangem.datasource.api.common.config.MoonPay</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -24,9 +24,12 @@ sealed class ApiConfig {
|
|||
Express,
|
||||
TangemTech,
|
||||
StakeKit,
|
||||
P2PEthPool,
|
||||
TangemPay,
|
||||
BlockAid,
|
||||
YieldSupply,
|
||||
MoonPay,
|
||||
News,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -34,9 +37,12 @@ sealed class ApiConfig {
|
|||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is P2PEthPool -> ID.P2PEthPool
|
||||
is TangemPay -> ID.TangemPay
|
||||
is BlockAid -> ID.BlockAid
|
||||
is YieldSupply -> ID.YieldSupply
|
||||
is MoonPay -> ID.MoonPay
|
||||
is News -> ID.News
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
|
||||
/**
|
||||
* MoonPay [ApiConfig]
|
||||
*/
|
||||
internal class MoonPay : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createMockEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.moonpay.com/",
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMockEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
/**
|
||||
* News [ApiConfig]
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class News(
|
||||
private val authProvider: AuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createDevEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
DEBUG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = PROD_BASE_URL,
|
||||
headers = createHeaders(ApiEnvironment.PROD),
|
||||
)
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = DEV_BASE_URL,
|
||||
headers = createHeaders(ApiEnvironment.DEV),
|
||||
)
|
||||
|
||||
private fun createHeaders(environment: ApiEnvironment) = buildMap {
|
||||
putAll(
|
||||
RequestHeader.TangemApiKeyHeader(
|
||||
authProvider = authProvider,
|
||||
apiEnvironment = Provider { environment },
|
||||
).values,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val PROD_BASE_URL = "https://api.tangem.org/"
|
||||
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
/**
|
||||
* P2P.org Ethereum Pooled Staking API configuration
|
||||
*/
|
||||
internal class P2PEthPool(
|
||||
private val p2pAuthProvider: P2PEthPoolAuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createTestEnvironment(),
|
||||
createMockEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://api-test.p2p.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMockEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createHeaders() = buildMap {
|
||||
put(key = "Authorization", value = ProviderSuspend { "Bearer ${p2pAuthProvider.getApiKey()}" })
|
||||
put(key = "accept", value = ProviderSuspend { "application/json" })
|
||||
put(key = "Content-Type", value = ProviderSuspend { "application/json" })
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ internal class TangemPay(
|
|||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
baseUrl = "https://api.dev.us.paera.com/bff/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@ sealed class ApiResponse<T : Any> {
|
|||
* Represents a successful response from the API
|
||||
*
|
||||
* @property data the data returned by the API
|
||||
* @property code the HTTP status code of the response
|
||||
* @property headers the headers returned by the API
|
||||
*/
|
||||
data class Success<T : Any>(
|
||||
val data: T,
|
||||
val code: ApiResponseError.HttpException.Code = ApiResponseError.HttpException.Code.OK,
|
||||
override val headers: Map<String, List<String>> = emptyMap(),
|
||||
) : ApiResponse<T>()
|
||||
|
||||
|
|
@ -37,11 +39,20 @@ sealed class ApiResponse<T : Any> {
|
|||
* Wraps data in a [ApiResponse.Success] instance
|
||||
*
|
||||
* @param data the data to wrap
|
||||
* @param code the HTTP status code of the response
|
||||
* @param headers the headers returned by the API
|
||||
* @return a [ApiResponse.Success] instance containing the provided data
|
||||
*/
|
||||
internal fun <T : Any> apiSuccess(data: T, headers: Map<String, List<String>>): ApiResponse<T> {
|
||||
return ApiResponse.Success(data, headers)
|
||||
internal fun <T : Any> apiSuccess(
|
||||
data: T,
|
||||
code: ApiResponseError.HttpException.Code?,
|
||||
headers: Map<String, List<String>>,
|
||||
): ApiResponse<T> {
|
||||
return ApiResponse.Success(
|
||||
data = data,
|
||||
code = code ?: ApiResponseError.HttpException.Code.OK,
|
||||
headers = headers,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@ sealed class ApiResponseError : Exception() {
|
|||
val errorBody: String?,
|
||||
) : ApiResponseError() {
|
||||
|
||||
// TODO: extract Code from HttpException
|
||||
// region Error Codes
|
||||
enum class Code(val numericCode: Int) {
|
||||
// 2xx Success
|
||||
OK(numericCode = 200),
|
||||
CREATED(numericCode = 201),
|
||||
ACCEPTED(numericCode = 202),
|
||||
// 3xx Server Errors
|
||||
NOT_MODIFIED(numericCode = 304),
|
||||
// 4xx Server Errors
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
|
|||
val headers = headers().toMultimap()
|
||||
val body = body()
|
||||
|
||||
val code = ApiResponseError.HttpException.Code.entries
|
||||
.firstOrNull { it.numericCode == code() }
|
||||
|
||||
return if (isSuccessful && body != null) {
|
||||
apiSuccess(data = body, headers = headers)
|
||||
apiSuccess(data = body, code = code, headers = headers)
|
||||
} else {
|
||||
val code = ApiResponseError.HttpException.Code.entries
|
||||
.firstOrNull { it.numericCode == code() }
|
||||
val e = try {
|
||||
if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.api.ethpool
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
|
||||
import com.tangem.datasource.api.ethpool.models.response.*
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* P2P.org Ethereum Pooled Staking API client
|
||||
*
|
||||
* Documentation: https://docs.p2p.org/
|
||||
*
|
||||
* Base URL: https://api.p2p.org (prod) / https://api-test.p2p.org (testnet)
|
||||
*/
|
||||
interface P2PEthPoolApi {
|
||||
|
||||
/**
|
||||
* Get list of available vaults
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/vaults")
|
||||
suspend fun getVaults(
|
||||
@Path("network") network: String = "mainnet",
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
|
||||
/**
|
||||
* Prepare deposit transaction
|
||||
*
|
||||
* Create unsigned transaction for depositing ETH into a vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Deposit parameters (delegator address, vault address, amount)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/deposit")
|
||||
suspend fun createDepositTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolDepositRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
|
||||
|
||||
/**
|
||||
* Prepare unstake transaction
|
||||
*
|
||||
* Create unsigned transaction to initiate unstaking process.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Unstake parameters (staker public key, stake transaction hash)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/unstake")
|
||||
suspend fun createUnstakeTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolUnstakeRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
|
||||
|
||||
/**
|
||||
* Prepare withdrawal transaction
|
||||
*
|
||||
* Create unsigned transaction to withdraw available funds from exit queue.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Withdrawal parameters (staker address)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/withdraw")
|
||||
suspend fun createWithdrawTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolWithdrawRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
|
||||
|
||||
/**
|
||||
* Broadcast signed transaction
|
||||
*
|
||||
* Submit a signed transaction to the blockchain network.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Signed transaction in hexadecimal format
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/transaction/send")
|
||||
suspend fun broadcastTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolBroadcastRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolBroadcastResponse>>
|
||||
|
||||
/**
|
||||
* Get account summary
|
||||
*
|
||||
* Retrieve staking balance, rewards, and exit queue information for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}")
|
||||
suspend fun getAccountInfo(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
|
||||
|
||||
/**
|
||||
* Get rewards history
|
||||
*
|
||||
* Retrieve historical rewards data for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
* @param period Optional period filter (30, 60, or 90 days)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards")
|
||||
suspend fun getRewards(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
@Query("period") period: Int? = null,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolRewardsResponse>>
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for broadcasting signed transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/transaction/send
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolBroadcastRequest(
|
||||
@Json(name = "signedTransaction")
|
||||
val signedTransaction: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating deposit transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolDepositRequest(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating unstake transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
*
|
||||
* Note: Documentation seems to contain Bitcoin-related fields (possibly copy-paste error).
|
||||
* Using as-is per specification.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnstakeRequest(
|
||||
@Json(name = "stakerPublicKey")
|
||||
val stakerPublicKey: String,
|
||||
@Json(name = "stakeTransactionHash")
|
||||
val stakeTransactionHash: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating withdrawal transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawRequest(
|
||||
@Json(name = "stakerAddress")
|
||||
val stakerAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolAccountResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "stake")
|
||||
val stake: P2PEthPoolStakeDTO,
|
||||
@Json(name = "availableToUnstake")
|
||||
val availableToUnstake: BigDecimal,
|
||||
@Json(name = "availableToWithdraw")
|
||||
val availableToWithdraw: BigDecimal,
|
||||
@Json(name = "exitQueue")
|
||||
val exitQueue: P2PEthPoolExitQueueDTO,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolStakeDTO(
|
||||
@Json(name = "assets")
|
||||
val assets: BigDecimal,
|
||||
@Json(name = "totalEarnedAssets")
|
||||
val totalEarnedAssets: BigDecimal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitQueueDTO(
|
||||
@Json(name = "total")
|
||||
val total: Double,
|
||||
@Json(name = "requests")
|
||||
val requests: List<P2PEthPoolExitRequestDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitRequestDTO(
|
||||
@Json(name = "ticket")
|
||||
val ticket: String,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
@Json(name = "timestamp")
|
||||
val timestamp: Long,
|
||||
@Json(name = "withdrawalTimestamp")
|
||||
val withdrawalTimestamp: Long,
|
||||
@Json(name = "isClaimable")
|
||||
val isClaimable: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/transaction/send
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolBroadcastResponse(
|
||||
@Json(name = "hash")
|
||||
val hash: String,
|
||||
@Json(name = "status")
|
||||
val status: P2PEthPoolTxStatusDTO,
|
||||
@Json(name = "blockNumber")
|
||||
val blockNumber: Int,
|
||||
@Json(name = "transactionIndex")
|
||||
val transactionIndex: Int,
|
||||
@Json(name = "gasUsed")
|
||||
val gasUsed: String,
|
||||
@Json(name = "cumulativeGasUsed")
|
||||
val cumulativeGasUsed: String,
|
||||
@Json(name = "effectiveGasPrice")
|
||||
val effectiveGasPrice: String?,
|
||||
@Json(name = "from")
|
||||
val from: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction status from P2P API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
@Json(name = "success")
|
||||
SUCCESS,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolDepositResponse(
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "unsignedTransaction")
|
||||
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Error response structure for P2P.org API
|
||||
*
|
||||
* All P2P API endpoints return errors in this format
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorResponse(
|
||||
@Json(name = "error")
|
||||
val error: P2PEthPoolErrorDetailsDTO,
|
||||
@Json(name = "result")
|
||||
val result: Any? = null, // null on error
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorDetailsDTO(
|
||||
@Json(name = "code")
|
||||
val code: Int, // Error code (e.g., 127106, 101111)
|
||||
@Json(name = "message")
|
||||
val message: String, // Human-readable error message
|
||||
@Json(name = "name")
|
||||
val name: String, // Error name/type
|
||||
@Json(name = "errors")
|
||||
val errors: List<String>? = null, // Optional validation errors array
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Unified response wrapper for all P2P.org API responses
|
||||
*
|
||||
* All P2P API endpoints return responses in this format:
|
||||
* ```json
|
||||
* {
|
||||
* "error": null | { code, message, name, errors },
|
||||
* "result": { ... } | null
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param T The type of the result data
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolResponse<T>(
|
||||
@Json(name = "error")
|
||||
val error: P2PEthPoolErrorDetailsDTO?,
|
||||
@Json(name = "result")
|
||||
val result: T?,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardsResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "rewards")
|
||||
val rewards: List<P2PEthPoolRewardDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardDTO(
|
||||
@Json(name = "date")
|
||||
val date: DateTime,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "balance")
|
||||
val balance: BigDecimal,
|
||||
@Json(name = "rewards")
|
||||
val rewards: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Unsigned transaction structure
|
||||
*
|
||||
* Used in deposit, withdraw responses
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnsignedTxDTO(
|
||||
@Json(name = "serializeTx")
|
||||
val serializeTx: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
@Json(name = "value")
|
||||
val value: String,
|
||||
@Json(name = "nonce")
|
||||
val nonce: Int,
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
@Json(name = "gasLimit")
|
||||
val gasLimit: BigDecimal,
|
||||
@Json(name = "maxFeePerGas")
|
||||
val maxFeePerGas: BigDecimal,
|
||||
@Json(name = "maxPriorityFeePerGas")
|
||||
val maxPriorityFeePerGas: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
*
|
||||
* Note: Contains Bitcoin-related fields (likely documentation error).
|
||||
* Using as-is per specification.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnstakeResponse(
|
||||
@Json(name = "stakerPublicKey")
|
||||
val stakerPublicKey: String,
|
||||
@Json(name = "stakeTransactionHash")
|
||||
val stakeTransactionHash: String,
|
||||
@Json(name = "unstakeTransactionHex")
|
||||
val unstakeTransactionHex: String, // unsigned
|
||||
@Json(name = "unstakeFee")
|
||||
val unstakeFee: Double,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/vaults
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolVaultsResponse(
|
||||
@Json(name = "network")
|
||||
val network: P2PEthPoolNetworkDTO,
|
||||
@Json(name = "vaults")
|
||||
val vaults: List<P2PEthPoolVaultDTO>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Network identifier in P2P API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolNetworkDTO {
|
||||
@Json(name = "mainnet")
|
||||
MAINNET,
|
||||
|
||||
@Json(name = "hoodi")
|
||||
HOODI,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolVaultDTO(
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "displayName")
|
||||
val displayName: String,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "baseApy")
|
||||
val baseApy: Double,
|
||||
@Json(name = "capacity")
|
||||
val capacity: Double,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
@Json(name = "feePercent")
|
||||
val feePercent: Double,
|
||||
@Json(name = "isPrivate")
|
||||
val isPrivate: Boolean,
|
||||
@Json(name = "isGenesis")
|
||||
val isGenesis: Boolean,
|
||||
@Json(name = "isSmoothingPool")
|
||||
val isSmoothingPool: Boolean,
|
||||
@Json(name = "isErc20")
|
||||
val isErc20: Boolean,
|
||||
@Json(name = "tokenName")
|
||||
val tokenName: String?,
|
||||
@Json(name = "tokenSymbol")
|
||||
val tokenSymbol: String?,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawResponse(
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "unsignedTransaction")
|
||||
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
@Json(name = "tickets")
|
||||
val tickets: List<String>,
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.datasource.api.express.models
|
||||
|
||||
object TangemExpressValues {
|
||||
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.datasource.api.moonpay
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface MoonPayApi {
|
||||
|
||||
@GET("v4/ip_address/")
|
||||
suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus
|
||||
|
||||
@GET("v3/currencies/")
|
||||
suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies>
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayUserStatus(
|
||||
@Json(name = "isBuyAllowed")
|
||||
val isBuyAllowed: Boolean,
|
||||
@Json(name = "isSellAllowed")
|
||||
val isSellAllowed: Boolean,
|
||||
@Json(name = "isAllowed")
|
||||
val isMoonpayAllowed: Boolean,
|
||||
@Json(name = "alpha3")
|
||||
val countryCode: String,
|
||||
@Json(name = "state")
|
||||
val stateCode: String,
|
||||
)
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrencies(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "code") val code: String,
|
||||
@Json(name = "supportsLiveMode") val supportsLiveMode: Boolean = false,
|
||||
@Json(name = "isSuspended") val isSuspended: Boolean = true,
|
||||
@Json(name = "isSupportedInUS") val isSupportedInUS: Boolean = false,
|
||||
@Json(name = "isSellSupported") val isSellSupported: Boolean = false,
|
||||
@Json(name = "notAllowedUSStates") val notAllowedUSStates: List<String> = emptyList(),
|
||||
@Json(name = "metadata") val metadata: MoonPayCurrenciesMetadata? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrenciesMetadata(
|
||||
@Json(name = "contractAddress") val contractAddress: String?,
|
||||
@Json(name = "networkCode") val networkCode: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.datasource.api.news
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsCategoriesResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsDetailsResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsListResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface NewsApi {
|
||||
|
||||
@GET(NEWS_PATH)
|
||||
suspend fun getNews(
|
||||
@Query("page") page: Int? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("lang") language: String? = null,
|
||||
@Query("asOf") snapshot: String? = null,
|
||||
@Query("tokenIds") tokenIds: List<String>? = null,
|
||||
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||
): ApiResponse<NewsListResponse>
|
||||
|
||||
@GET("$NEWS_PATH/{newsId}")
|
||||
suspend fun getNewsDetails(
|
||||
@Path("newsId") newsId: Int,
|
||||
@Query("lang") language: String? = null,
|
||||
): ApiResponse<NewsDetailsResponse>
|
||||
|
||||
@GET("$NEWS_PATH/trending")
|
||||
suspend fun getTrendingNews(
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("lang") language: String? = null,
|
||||
): ApiResponse<NewsTrendingResponse>
|
||||
|
||||
@GET("$NEWS_PATH/categories")
|
||||
suspend fun getCategories(): ApiResponse<NewsCategoriesResponse>
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val NEWS_PATH = "api/v1/news"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsArticleDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "score") val score: Double,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "isTrending") val isTrending: Boolean,
|
||||
@Json(name = "categories") val categories: List<NewsCategoryDto>,
|
||||
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "newsUrl") val newsUrl: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsCategoryDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsRelatedTokenDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "symbol") val symbol: String,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsOriginalArticleDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "sourceName") val sourceName: String,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "publishedAt") val publishedAt: String,
|
||||
@Json(name = "url") val url: String,
|
||||
@Json(name = "imageUrl") val imageUrl: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsCategoriesResponse(
|
||||
@Json(name = "items") val items: List<NewsCategoryDto>,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsDetailsResponse(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "score") val score: Double,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "isTrending") val isTrending: Boolean,
|
||||
@Json(name = "categories") val categories: List<NewsCategoryDto>,
|
||||
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "newsUrl") val newsUrl: String,
|
||||
@Json(name = "shortContent") val shortContent: String,
|
||||
@Json(name = "content") val content: String,
|
||||
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsListResponse(
|
||||
@Json(name = "meta") val meta: NewsListMetaDto,
|
||||
@Json(name = "items") val items: List<NewsArticleDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsListMetaDto(
|
||||
@Json(name = "page") val page: Int,
|
||||
@Json(name = "limit") val limit: Int,
|
||||
@Json(name = "total") val total: Long,
|
||||
@Json(name = "hasNext") val hasNext: Boolean,
|
||||
@Json(name = "asOf") val asOf: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsTrendingResponse(
|
||||
@Json(name = "meta") val meta: NewsTrendingMetaDto,
|
||||
@Json(name = "items") val items: List<NewsArticleDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsTrendingMetaDto(
|
||||
@Json(name = "limit") val limit: Int,
|
||||
)
|
||||
|
|
@ -7,6 +7,7 @@ import retrofit2.http.Body
|
|||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
|
|
@ -146,4 +147,22 @@ interface TangemPayApi {
|
|||
@Header("Authorization") authHeader: String,
|
||||
@Body body: CardDetailsRequest,
|
||||
): ApiResponse<CardDetailsResponse>
|
||||
|
||||
@PUT("v1/customer/card/pin")
|
||||
suspend fun setPin(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: SetPinRequest,
|
||||
): ApiResponse<SetPinResponse>
|
||||
|
||||
@POST("v1/customer/card/freeze")
|
||||
suspend fun freezeCard(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: FreezeUnfreezeCardRequest,
|
||||
): ApiResponse<FreezeUnfreezeCardResponse>
|
||||
|
||||
@POST("v1/customer/card/unfreeze")
|
||||
suspend fun unfreezeCard(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: FreezeUnfreezeCardRequest,
|
||||
): ApiResponse<FreezeUnfreezeCardResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FreezeUnfreezeCardRequest(@Json(name = "card_id") val cardId: String)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SetPinRequest(
|
||||
@Json(name = "pin") val pin: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
@Json(name = "iv") val iv: String,
|
||||
)
|
||||
|
|
@ -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 SetPinResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "result") val result: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
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 BalanceResponse(
|
||||
@Json(name = "fiat") val fiat: FiatBalance,
|
||||
@Json(name = "crypto") val crypto: CryptoBalance,
|
||||
@Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FiatBalance(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CryptoBalance(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "chain_id") val chainId: Int,
|
||||
@Json(name = "deposit_address") val depositAddress: String,
|
||||
@Json(name = "token_contract_address") val tokenContractAddress: String,
|
||||
@Json(name = "balance") val balance: BigDecimal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AvailableForWithdrawal(
|
||||
@Json(name = "amount") val amount: BigDecimal,
|
||||
@Json(name = "currency") val currency: String,
|
||||
)
|
||||
|
|
@ -1,20 +1,8 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class CardBalanceResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "result") val result: BalanceResponse?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -2,7 +2,6 @@ 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(
|
||||
|
|
@ -19,7 +18,7 @@ data class CustomerMeResponse(
|
|||
@Json(name = "kyc") val kyc: Kyc?,
|
||||
@Json(name = "depositAddress") val depositAddress: String?,
|
||||
@Json(name = "card") val card: Card?,
|
||||
@Json(name = "balance") val balance: Balance?,
|
||||
@Json(name = "balance") val balance: BalanceResponse?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -28,10 +27,49 @@ data class CustomerMeResponse(
|
|||
@Json(name = "cid") val cid: String,
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "status") val status: Status,
|
||||
@Json(name = "updated_at") val updatedAt: String,
|
||||
@Json(name = "payment_account_id") val paymentAccountId: String,
|
||||
)
|
||||
) {
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "new")
|
||||
NEW,
|
||||
|
||||
@Json(name = "ready_for_manufacturing")
|
||||
READY_FOR_MANUFACTURING,
|
||||
|
||||
@Json(name = "manufacturing")
|
||||
MANUFACTURING,
|
||||
|
||||
@Json(name = "sent_to_delivery")
|
||||
SENT_TO_DELIVERY,
|
||||
|
||||
@Json(name = "delivered")
|
||||
DELIVERED,
|
||||
|
||||
@Json(name = "activating")
|
||||
ACTIVATING,
|
||||
|
||||
@Json(name = "active")
|
||||
ACTIVE,
|
||||
|
||||
@Json(name = "blocked")
|
||||
BLOCKED,
|
||||
|
||||
@Json(name = "deactivating")
|
||||
DEACTIVATING,
|
||||
|
||||
@Json(name = "deactivated")
|
||||
DEACTIVATED,
|
||||
|
||||
@Json(name = "canceled")
|
||||
CANCELED,
|
||||
|
||||
@Json(name = "unknown")
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaymentAccount(
|
||||
|
|
@ -60,14 +98,4 @@ data class CustomerMeResponse(
|
|||
@Json(name = "card_status") val cardStatus: String,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Balance(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class FreezeUnfreezeCardResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "order_id") val orderId: String,
|
||||
@Json(name = "status") val status: Status,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "NEW")
|
||||
NEW,
|
||||
|
||||
@Json(name = "PROCESSING")
|
||||
PROCESSING,
|
||||
|
||||
@Json(name = "COMPLETED")
|
||||
COMPLETED,
|
||||
|
||||
@Json(name = "CANCELED")
|
||||
CANCELED,
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ data class TangemPayTxHistoryResponse(
|
|||
@Json(name = "memo") val memo: String? = null,
|
||||
@Json(name = "receipt") val receipt: Boolean,
|
||||
@Json(name = "merchant_name") val merchantName: String,
|
||||
@Json(name = "merchant_category") val merchantCategory: String,
|
||||
@Json(name = "merchant_category") val merchantCategory: String?,
|
||||
@Json(name = "merchant_category_code") val merchantCategoryCode: String,
|
||||
@Json(name = "merchant_id") val merchantId: String? = null,
|
||||
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
|
||||
|
|
|
|||
|
|
@ -50,9 +50,6 @@ interface TangemTechApi {
|
|||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("v1/user-tokens")
|
||||
suspend fun markUserWallerWasCreated(@Body body: MarkUserWalletWasCreatedBody): ApiResponse<Unit>
|
||||
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("v1/referral/{walletId}")
|
||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
||||
|
|
@ -137,6 +134,9 @@ interface TangemTechApi {
|
|||
|
||||
@GET("v1/user-wallets/wallets/by-app/{app_id}")
|
||||
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
|
||||
|
||||
@POST("v1/user-wallets/wallets")
|
||||
suspend fun createWallet(@Body body: WalletIdBody): ApiResponse<Unit>
|
||||
// endregion
|
||||
|
||||
// promo
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.api.tangemTech.converters
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CardInfoBody
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
object WalletIdBodyConverter {
|
||||
|
||||
fun convert(userWallet: UserWallet, publicKeys: Map<String, String>? = null): WalletIdBody {
|
||||
return WalletIdBody(
|
||||
walletId = userWallet.walletId.stringValue,
|
||||
name = userWallet.name,
|
||||
walletType = WalletType.from(userWallet),
|
||||
cards = publicKeys?.map { publicKeyById ->
|
||||
CardInfoBody(
|
||||
cardId = publicKeyById.key,
|
||||
cardPublicKey = publicKeyById.value,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MarkUserWalletWasCreatedBody(
|
||||
@Json(name = "user_wallet_id") val userWalletId: String,
|
||||
)
|
||||
|
|
@ -3,6 +3,9 @@ package com.tangem.datasource.api.tangemTech.models
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.common.extensions.calculateHashCode
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType.NONE
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
|
|
@ -10,8 +13,10 @@ data class UserTokensResponse(
|
|||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
@Json(name = "sort") val sort: SortType,
|
||||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||
@Json(name = "tokens") val tokens: List<Token> = emptyList(),
|
||||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||
@Json(name = "name") val walletName: String? = null,
|
||||
@Json(name = "type") val walletType: WalletType? = null,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -68,4 +73,8 @@ data class UserTokensResponse(
|
|||
@Json(name = "marketcap")
|
||||
MARKETCAP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun GroupType?.orDefault(): GroupType = this ?: NONE
|
||||
|
||||
fun SortType?.orDefault(): SortType = this ?: SortType.MANUAL
|
||||
|
|
@ -4,8 +4,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
data class WalletBody(
|
||||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||
@Json(name = "name") val name: String? = null,
|
||||
@Json(name = "type") val type: WalletType? = null,
|
||||
)
|
||||
|
|
@ -7,5 +7,6 @@ import com.squareup.moshi.JsonClass
|
|||
data class WalletIdBody(
|
||||
@Json(name = "id") val walletId: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "cards") val cards: List<CardInfoBody>,
|
||||
@Json(name = "type") val walletType: WalletType? = null,
|
||||
@Json(name = "cards") val cards: List<CardInfoBody>? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class WalletType {
|
||||
@Json(name = "card")
|
||||
COLD,
|
||||
|
||||
@Json(name = "mobile")
|
||||
HOT,
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(userWallet: UserWallet?): WalletType? {
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> COLD
|
||||
is UserWallet.Hot -> HOT
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,23 @@ data class GetWalletAccountsResponse(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
@Json(name = "sort") val sort: SortType,
|
||||
@Json(name = "version") val version: Int? = 0,
|
||||
@Json(name = "group") val group: GroupType?,
|
||||
@Json(name = "sort") val sort: SortType?,
|
||||
@Json(name = "totalAccounts") val totalAccounts: Int,
|
||||
)
|
||||
}
|
||||
|
||||
/** Flattens the tokens from all wallet accounts into a single list */
|
||||
fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
|
||||
return accounts.flatMap { it.tokens.orEmpty() }
|
||||
}
|
||||
|
||||
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
|
||||
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
group = wallet.group ?: GroupType.NONE,
|
||||
sort = wallet.sort ?: SortType.MANUAL,
|
||||
tokens = flattenTokens(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
|
||||
import com.tangem.datasource.local.accounts.DefaultAccountTokenMigrationStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
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 AccountsMigrationStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountTokenMigrationStore(): AccountTokenMigrationStore {
|
||||
return DefaultAccountTokenMigrationStore(
|
||||
runtimeStateStore = RuntimeStateStore(emptyMap()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.AuthProvider
|
|||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
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
|
||||
|
|
@ -39,6 +40,12 @@ internal object ApiConfigsModule {
|
|||
return StakeKit(stakeKitAuthProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideP2PEthPoolConfig(p2pAuthProvider: P2PEthPoolAuthProvider): ApiConfig {
|
||||
return P2PEthPool(p2pAuthProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemTechConfig(
|
||||
|
|
@ -51,6 +58,10 @@ internal object ApiConfigsModule {
|
|||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideNewsConfig(authProvider: AuthProvider): ApiConfig = News(authProvider = authProvider)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideYieldSupplyConfig(
|
||||
|
|
@ -74,4 +85,10 @@ internal object ApiConfigsModule {
|
|||
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
|
||||
return BlockAid(environmentConfigStorage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideMoonPayConfig(): ApiConfig {
|
||||
return MoonPay()
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,17 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
|||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.MoonPay
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.moonpay.MoonPayApi
|
||||
import com.tangem.datasource.api.news.NewsApi
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
|
|
@ -71,6 +75,15 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolApi(retrofitApiBuilder: RetrofitApiBuilder): P2PEthPoolApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.P2PEthPool,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi {
|
||||
|
|
@ -130,4 +143,22 @@ internal object NetworkModule {
|
|||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoonPayApi(retrofitApiBuilder: RetrofitApiBuilder): MoonPayApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.MoonPay,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsApi(retrofitApiBuilder: RetrofitApiBuilder): NewsApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.News,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
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 NewsStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsDetailsStore(): NewsDetailsStore {
|
||||
return DefaultNewsDetailsStore(store = RuntimeSharedStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTrendingNewsStore(): TrendingNewsStore {
|
||||
return DefaultTrendingNewsStore(store = RuntimeSharedStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,13 @@ import com.squareup.moshi.Moshi
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.token.DefaultP2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.DefaultStakingActionsStore
|
||||
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
|
||||
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
|
||||
import com.tangem.datasource.local.token.StakingActionsStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
|
|
@ -73,4 +76,24 @@ internal object StakingStoreModule {
|
|||
fun provideStakingActionsStore(): StakingActionsStore {
|
||||
return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolVaultsStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): P2PEthPoolVaultsStore {
|
||||
return DefaultP2PEthPoolVaultsStore(
|
||||
dataStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = listTypes<P2PEthPoolVault>(),
|
||||
defaultValue = emptyList(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_vaults") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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.TangemPayCardFrozenStateStore
|
||||
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 TangemPayStoresModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayCardFrozenStateStore(): TangemPayCardFrozenStateStore {
|
||||
return DefaultTangemPayCardFrozenStateStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.datasource.di.exchangeservice
|
||||
|
||||
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ExchangeServiceLoaderModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
|
||||
}
|
||||
|
|
@ -197,6 +197,7 @@ internal class RetrofitApiBuilder @Inject constructor(
|
|||
|
||||
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
|
||||
// ApiConfig.ID.StakeKit,
|
||||
ApiConfig.ID.MoonPay,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package com.tangem.datasource.exchangeservice.swap
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
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.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
|
||||
|
||||
/**
|
||||
* Default implementation of [ExpressServiceLoader]
|
||||
*
|
||||
* @property tangemExpressApi express api
|
||||
* @property expressAssetsStore local storage
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
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(userWallet: UserWallet, userTokens: List<LeastTokenInfo>) {
|
||||
withContext(dispatchers.io) {
|
||||
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(userWallet.walletId, response)
|
||||
|
||||
initializationStatus.update { response.lceContent() }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
|
||||
initializationStatus.update { e.lceError() }
|
||||
}
|
||||
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>> {
|
||||
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
|
||||
}
|
||||
|
||||
@Suppress("SuspendFunWithFlowReturnType")
|
||||
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
|
||||
val initializationStatus = initializationStatuses.value[userWalletId]
|
||||
if (initializationStatus != null) return initializationStatus
|
||||
|
||||
val cached = expressAssetsStore.getSyncOrNull(userWalletId)
|
||||
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
|
||||
|
||||
initializationStatuses.update { statuses ->
|
||||
statuses.toMutableMap().apply {
|
||||
put(key = userWalletId, value = default)
|
||||
}
|
||||
}
|
||||
|
||||
return default
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Express service loader
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ExpressServiceLoader {
|
||||
|
||||
/** 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>>>
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.accounts
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AccountTokenMigrationStore {
|
||||
|
||||
fun get(userWalletId: UserWalletId): Flow<Pair<String, String>?>
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, value: Pair<String, String>)
|
||||
|
||||
suspend fun remove(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.accounts
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultAccountTokenMigrationStore(
|
||||
private val runtimeStateStore: RuntimeStateStore<Map<UserWalletId, Pair<String, String>>>,
|
||||
) : AccountTokenMigrationStore {
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<Pair<String, String>?> {
|
||||
return runtimeStateStore.get().map { it[userWalletId] }
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, value: Pair<String, String>) {
|
||||
runtimeStateStore.update { it + (userWalletId to value) }
|
||||
}
|
||||
|
||||
override suspend fun remove(userWalletId: UserWalletId) {
|
||||
runtimeStateStore.update { it - userWalletId }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.local.config.environment
|
|||
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||
import com.tangem.datasource.local.config.environment.models.P2PKeys
|
||||
|
||||
data class EnvironmentConfig(
|
||||
val moonPayApiKey: String = "",
|
||||
|
|
@ -16,6 +17,7 @@ data class EnvironmentConfig(
|
|||
val express: ExpressModel? = null,
|
||||
val devExpress: ExpressModel? = null,
|
||||
val stakeKitApiKey: String? = null,
|
||||
val p2pApiKey: P2PKeys? = null,
|
||||
val blockAidApiKey: String? = null,
|
||||
val tangemApiKey: String? = null,
|
||||
val tangemApiKeyDev: String? = null,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
|||
apiKey = value.bscQuiknodeApiKey,
|
||||
subdomain = value.bscQuiknodeSubdomain,
|
||||
),
|
||||
quickNodePlasmaCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeApiKey,
|
||||
subdomain = value.quiknodeSubdomain,
|
||||
),
|
||||
infuraProjectId = value.infuraProjectId,
|
||||
tronGridApiKey = value.tronGridApiKey,
|
||||
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
p2pApiKey = value.p2pApiKey,
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
tangemApiKey = value.tangemApiKey,
|
||||
tangemApiKeyDev = value.tangemApiKeyDev,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?,
|
||||
@Json(name = "polygonScanApiKey") val polygonScanApiKey: String?,
|
||||
@Json(name = "stakeKitApiKey") val stakeKitApiKey: String?,
|
||||
@Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?,
|
||||
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
|
||||
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
|
||||
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
|
||||
|
|
@ -97,6 +98,12 @@ data class TonCenterKeys(
|
|||
@Json(name = "testnet") val testnet: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "hoodi") val hoodi: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockToken(
|
||||
@Json(name = "jsonRpc") val jsonRPC: String?,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
package com.tangem.datasource.local.datastore.core
|
||||
|
||||
@Deprecated(
|
||||
message = "Use RuntimeSharedStore instead",
|
||||
replaceWith = ReplaceWith("RuntimeSharedStore"),
|
||||
)
|
||||
internal interface StringKeyDataStore<Value : Any> : DataStore<String, Value>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.local.news.details
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultNewsDetailsStore(
|
||||
private val store: RuntimeSharedStore<Map<Int, DetailedArticle>>,
|
||||
) : NewsDetailsStore {
|
||||
|
||||
override fun getAll(): Flow<List<DetailedArticle>> {
|
||||
return store.get().map { it.values.toList() }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(id: Int): DetailedArticle? {
|
||||
return store.getSyncOrNull()?.get(id)
|
||||
}
|
||||
|
||||
override suspend fun store(id: Int, article: DetailedArticle) {
|
||||
store.update(emptyMap()) { current ->
|
||||
current + (id to article)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun store(articles: Map<Int, DetailedArticle>) {
|
||||
if (articles.isEmpty()) return
|
||||
|
||||
store.update(emptyMap()) { current ->
|
||||
current + articles
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
store.store(emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.local.news.details
|
||||
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NewsDetailsStore {
|
||||
|
||||
fun getAll(): Flow<List<DetailedArticle>>
|
||||
|
||||
suspend fun getSyncOrNull(id: Int): DetailedArticle?
|
||||
|
||||
suspend fun store(id: Int, article: DetailedArticle)
|
||||
|
||||
suspend fun store(articles: Map<Int, DetailedArticle>)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.local.news.trending
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private typealias TrendingCache = Map<String, List<ShortArticle>>
|
||||
|
||||
internal class DefaultTrendingNewsStore(
|
||||
private val store: RuntimeSharedStore<TrendingCache>,
|
||||
) : TrendingNewsStore {
|
||||
|
||||
override fun get(key: String): Flow<List<ShortArticle>> {
|
||||
return store.get().map { it[key].orEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(key: String): List<ShortArticle>? {
|
||||
return store.getSyncOrNull()?.get(key)
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: List<ShortArticle>) {
|
||||
store.update(emptyMap()) { current ->
|
||||
current + (key to value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
store.store(emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.local.news.trending
|
||||
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TrendingNewsStore {
|
||||
|
||||
fun get(key: String): Flow<List<ShortArticle>>
|
||||
|
||||
suspend fun getSyncOrNull(key: String): List<ShortArticle>?
|
||||
|
||||
suspend fun store(key: String, value: List<ShortArticle>)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -121,6 +121,8 @@ object PreferencesKeys {
|
|||
|
||||
val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") }
|
||||
|
||||
val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") }
|
||||
|
||||
// region Notifications
|
||||
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
|
||||
|
||||
|
|
@ -171,6 +173,9 @@ object PreferencesKeys {
|
|||
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
|
||||
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
|
||||
|
||||
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
|
||||
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
internal class DefaultP2PEthPoolVaultsStore(
|
||||
private val dataStore: DataStore<List<P2PEthPoolVault>>,
|
||||
) : P2PEthPoolVaultsStore {
|
||||
|
||||
override fun get(): Flow<List<P2PEthPoolVault>> {
|
||||
return dataStore.data
|
||||
}
|
||||
|
||||
override suspend fun getSync(): List<P2PEthPoolVault> {
|
||||
return dataStore.data.firstOrNull().orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun store(vaults: List<P2PEthPoolVault>) {
|
||||
dataStore.updateData { vaults }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store for P2P Ethereum pooled staking vaults
|
||||
* (similar to StakingYieldsStore for StakeKit yields)
|
||||
*
|
||||
* Vault is ETH-specific concept for pooled staking.
|
||||
* For other blockchains, P2P may use different structures.
|
||||
*/
|
||||
interface P2PEthPoolVaultsStore {
|
||||
|
||||
/**
|
||||
* Get all stored vaults as Flow
|
||||
*/
|
||||
fun get(): Flow<List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Get all stored vaults synchronously
|
||||
*/
|
||||
suspend fun getSync(): List<P2PEthPoolVault>
|
||||
|
||||
/**
|
||||
* Store vaults from P2P API
|
||||
*/
|
||||
suspend fun store(vaults: List<P2PEthPoolVault>)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DefaultTangemPayCardFrozenStateStore(
|
||||
private val dataStore: StringKeyDataStore<TangemPayCardFrozenState>,
|
||||
) : TangemPayCardFrozenStateStore {
|
||||
override suspend fun getSyncOrNull(key: String): TangemPayCardFrozenState? {
|
||||
return dataStore.getSyncOrNull(key)
|
||||
}
|
||||
|
||||
override fun get(key: String): Flow<TangemPayCardFrozenState> {
|
||||
return dataStore.get(key)
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: TangemPayCardFrozenState) {
|
||||
dataStore.store(key, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TangemPayCardFrozenStateStore {
|
||||
|
||||
suspend fun getSyncOrNull(key: String): TangemPayCardFrozenState?
|
||||
|
||||
fun get(key: String): Flow<TangemPayCardFrozenState>
|
||||
|
||||
suspend fun store(key: String, value: TangemPayCardFrozenState)
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
||||
interface TangemPayStorage {
|
||||
|
||||
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
|
||||
|
||||
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
|
||||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
|
||||
|
|
@ -14,5 +18,9 @@ interface TangemPayStorage {
|
|||
|
||||
suspend fun clearOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun clearAll(customerWalletAddress: String)
|
||||
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
|
||||
|
||||
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
|
||||
|
||||
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
}
|
||||
|
|
@ -71,6 +71,9 @@ class ApiConfigTest {
|
|||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.datasource.api.common.config.managers
|
|||
|
||||
import android.os.Build
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.*
|
||||
|
|
@ -14,7 +13,9 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD
|
|||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
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
|
||||
|
|
@ -39,6 +40,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private val appVersionProvider = mockk<AppVersionProvider>()
|
||||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||
private val p2pEthPoolAuthProvider = mockk<P2PEthPoolAuthProvider>()
|
||||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
private val appInfoProvider = mockk<AppInfoProvider>()
|
||||
private val tangemApiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||
|
|
@ -58,6 +60,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { appVersionProvider.versionName } 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
|
||||
every { appAuthProvider.getApiKey(any()) } returns tangemApiKeyProvider
|
||||
coEvery { tangemApiKeyProvider.invoke() } returns TANGEM_API_KEY
|
||||
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
|
||||
|
|
@ -109,6 +112,9 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +127,9 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.StakeKit -> createStakeKitModel()
|
||||
ApiConfig.ID.TangemPay -> createTangemPayModel()
|
||||
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
|
||||
ApiConfig.ID.MoonPay -> createMoonPayModel()
|
||||
ApiConfig.ID.P2PEthPool -> createP2PModel()
|
||||
ApiConfig.ID.News -> createNewsModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +248,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
id = ApiConfig.ID.TangemPay,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
baseUrl = "https://api.dev.us.paera.com/bff/",
|
||||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
|
|
@ -263,6 +272,54 @@ internal class ProdApiConfigsManagerTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createMoonPayModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.MoonPay,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.moonpay.com/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.P2PEthPool,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
headers = mapOf(
|
||||
"Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" },
|
||||
"accept" to ProviderSuspend { "application/json" },
|
||||
"Content-Type" to ProviderSuspend { "application/json" },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNewsModel(): TestModel {
|
||||
val (environment, baseUrl) = when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
DEBUG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV to "[REDACTED_ENV_URL]"
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD to "https://tangem.com/"
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.News,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = environment,
|
||||
baseUrl = baseUrl,
|
||||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend { TANGEM_API_KEY },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
|
|
@ -281,6 +338,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
const val VERSION_NAME = "debug"
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val P2P_API_KEY = "p2p_api_key"
|
||||
const val APP_CARD_ID = "app_card_id"
|
||||
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.datasource.api.tangemTech.converters
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.tangemTech.models.CardInfoBody
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WalletIdBodyConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN user wallet with cards WHEN convert THEN should return correct WalletIdBody`() {
|
||||
// GIVEN
|
||||
val walletId = UserWalletId("1234567890abcdef")
|
||||
val walletName = "Test Wallet"
|
||||
val userWallet = UserWallet.Cold(
|
||||
walletId = walletId,
|
||||
name = walletName,
|
||||
cardsInWallet = setOf("card1", "card2"),
|
||||
isMultiCurrency = true,
|
||||
hasBackupError = false,
|
||||
scanResponse = mockk(),
|
||||
)
|
||||
val publicKeys = mapOf(
|
||||
"card1" to "public_key_1",
|
||||
"card2" to "public_key_2",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
WalletIdBody(
|
||||
walletId = walletId.stringValue,
|
||||
name = walletName,
|
||||
walletType = WalletType.COLD,
|
||||
cards = listOf(
|
||||
CardInfoBody(
|
||||
cardId = "card1",
|
||||
cardPublicKey = "public_key_1",
|
||||
),
|
||||
CardInfoBody(
|
||||
cardId = "card2",
|
||||
cardPublicKey = "public_key_2",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN user wallet without cards WHEN convert THEN should return WalletIdBody with empty cards list`() {
|
||||
// GIVEN
|
||||
val walletId = UserWalletId("1234567890abcdef")
|
||||
val walletName = "Test Wallet"
|
||||
val userWallet = UserWallet.Cold(
|
||||
walletId = walletId,
|
||||
name = walletName,
|
||||
cardsInWallet = emptySet(),
|
||||
isMultiCurrency = true,
|
||||
hasBackupError = false,
|
||||
scanResponse = mockk(),
|
||||
)
|
||||
val publicKeys = emptyMap<String, String>()
|
||||
|
||||
// WHEN
|
||||
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
WalletIdBody(
|
||||
walletId = walletId.stringValue,
|
||||
walletType = WalletType.COLD,
|
||||
name = walletName,
|
||||
cards = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.datasource.local.logs
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -12,7 +12,7 @@ import org.junit.jupiter.params.provider.MethodSource
|
|||
internal class LogsSanitizerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
@ProvideTestModels
|
||||
fun sanitize(model: TestModel) {
|
||||
// Act
|
||||
val actual = LogsSanitizer.sanitize(model.input)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceCurrent
|
||||
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
|
@ -14,11 +14,14 @@ internal class DefaultRouter(
|
|||
private val navigation: StackNavigation<Route>
|
||||
get() = navigationProvider.getOrCreate()
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
navigation.pushNew(route, onComplete)
|
||||
}
|
||||
|
||||
override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) {
|
||||
navigation.replaceCurrent(route, { onComplete(true) })
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
val newRoutes = routes.toList()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ class DummyRouter : Router {
|
|||
onComplete(true)
|
||||
}
|
||||
|
||||
override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) {
|
||||
onComplete(true)
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
onComplete(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ interface Router {
|
|||
*/
|
||||
fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
|
||||
/**
|
||||
* Replaces a current route with a new one.
|
||||
*
|
||||
* @param route The route to replace a current one.
|
||||
* @param onComplete The callback to be invoked when the operation is complete.
|
||||
*/
|
||||
fun replaceCurrent(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
|
||||
/**
|
||||
* Replaces ***all*** routes in the navigation stack with the specified [routes].
|
||||
*
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue