Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-04 18:30:02 +03:00
commit 2066204261
463 changed files with 6404 additions and 3051 deletions

View file

@ -1,44 +0,0 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ENV ANDROID_HOME=/opt/android-sdk
ENV BUNDLE_PATH=vendor/bundle
ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/34.0.0:$BUNDLE_PATH/bin
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
RUN apt-get update && apt-get install -y \
openjdk-17-jdk \
wget \
unzip \
curl \
git \
ruby \
ruby-dev \
build-essential \
&& apt-get clean
RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \
wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \
unzip /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools/latest && \
mv $ANDROID_HOME/cmdline-tools/latest/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ && \
rm -rf $ANDROID_HOME/cmdline-tools/latest/cmdline-tools && \
rm -f /tmp/cmdline-tools.zip && \
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-31" "platforms;android-34" "build-tools;34.0.0"
RUN wget https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-aarch64.zip -O /tmp/android-sdk-tools-static-aarch64.zip && \
unzip /tmp/android-sdk-tools-static-aarch64.zip -d /tmp/android-sdk-tools-static-arm && \
cp -r /tmp/android-sdk-tools-static-arm/build-tools/* $ANDROID_HOME/build-tools/34.0.0/ && \
rm -rf /tmp/android-sdk-tools-static-arm /tmp/android-sdk-tools-static-aarch64.zip
RUN gem install bundler:2.5.23
RUN gem install fastlane -v 2.225.0 -N -V
RUN gem install fastlane-plugin-firebase_app_distribution -v 0.9.1 -N -V
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs=4 --retry=3 --verbose
RUN bundle exec fastlane -v
CMD ["bash"]

View file

@ -1,4 +1,5 @@
import java.util.Properties
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.application)
@ -258,6 +259,10 @@ dependencies {
implementation(deps.decompose.ext.compose)
implementation(deps.moshi.adapters)
implementation(deps.moshi.kotlin)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
/** Testing libraries */
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
@ -275,6 +280,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
debugPGImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
@ -296,5 +302,4 @@ dependencies {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group = "com.google.guava", module = "listenablefuture")
}
}

223
app/proguard-rules.pro vendored
View file

@ -1,21 +1,208 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-dontusemixedcaseclassnames
-flattenpackagehierarchy
-adaptclassstrings
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# firebase
-keep public class com.google.firebase.** { *; }
-keep class com.google.android.gms.internal.** { *; }
-keepclasseswithmembers class com.google.firebase.FirebaseException
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# hedera sdk
-keep class com.hedera.hashgraph.sdk.** { *; }
-keep interface com.hedera.hashgraph.sdk.** { *; }
-dontwarn com.esaulpaugh.headlong.**
-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings
-dontwarn io.grpc.stub.**
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# trustwallet sdk
-keep class wallet.core.jni.** { *; }
# solana
-keep class org.p2p.solanaj.** { *; }
-keep class com.tangem.blockchain.blockchains.solana.solanaj.model.** { *; }
# binance
-keep class com.tangem.blockchain.blockchains.binance.client.** { *; }
# shadow gson
-keep,allowobfuscation,allowshrinking class shadow.com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends shadow.com.google.gson.reflect.TypeToken
# polkadot
-keep class io.emeraldpay.polkaj.api.RpcRequest { *; }
-keep class io.emeraldpay.polkaj.api.RpcResponse { *; }
-keep class io.emeraldpay.polkaj.api.RpcResponseError { *; }
# kethereum
-keep class org.kethereum.bip32.model.ExtendedKey**
-keepclassmembers class org.kethereum.bip32.model.ExtendedKey** { *; }
# some crypto
-dontwarn net.i2p.crypto.**
-dontwarn java.net.http.**
-dontwarn lombok.**
-keep class org.spongycastle.** { *; }
-dontwarn org.apache.hc.core5.**
-dontwarn org.apache.hc.client5.**
-dontwarn org.apache.log4j.config.**
-dontwarn javax.naming.**
-dontwarn javax.xml.stream.**
-dontwarn javax.script.**
-dontwarn org.java_websocket.client.WebSocketClient
-dontwarn org.java_websocket.handshake.ServerHandshake
-dontwarn aQute.bnd.annotation.spi.ServiceProvider
-dontwarn org.threeten.bp.**
-keep class org.threeten.bp.*
-keepclassmembers class org.threeten.bp.** { *; }
# joda time
# These aren't necessary if including joda-convert
-dontwarn org.joda.convert.FromString
-dontwarn org.joda.convert.ToString
-keepnames class org.joda.** implements java.io.Serializable
-keepclassmembers class org.joda.** implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# joda time
# The name of @JsonClass types is used to look up the generated adapter.
-keepnames @com.squareup.moshi.JsonClass class *
-keepclassmembers class * {
@com.squareup.moshi.FromJson <methods>;
@com.squareup.moshi.ToJson <methods>;
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep class kotlin.Metadata { *; }
-keepclassmembers class kotlin.Metadata {
public <methods>;
}
# Proguard configuration for Jackson 2.x
-keep class com.fasterxml.** { *; }
-keep class com.fasterxml.jackson.databind.ObjectMapper {
public <methods>;
protected <methods>;
}
-keep class com.fasterxml.jackson.databind.ObjectWriter {
public ** writeValueAsString(**);
}
-keepnames class com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.databind.**
-keep class * implements com.fasterxml.jackson.core.type.TypeReference
-keep class kotlin.reflect.**
-keep public class kotlin.reflect.jvm.internal.impl.** { public *; }
# Kotlin serialization looks up the generated serializer classes through a function on companion
# objects. The companions are looked up reflectively so we need to explicitly keep these functions.
-keepclasseswithmembers class **.*$Companion {
kotlinx.serialization.KSerializer serializer(...);
}
# If a companion has the serializer function, keep the companion field on the original type so that
# the reflective lookup succeeds.
-if class **.*$Companion {
kotlinx.serialization.KSerializer serializer(...);
}
-keepclassmembers class <1>.<2> {
<1>.<2>$Companion Companion;
}
-keepnames class * implements android.os.Parcelable {
public static final ** CREATOR;
}
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-dontwarn okhttp3.**
-dontwarn okio.**
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod, *Annotation*, Exceptions
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
-keepattributes AnnotationDefault
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
-keepclassmembernames interface * {
@retrofit.http.* <methods>;
}
# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit
# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions.*
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
# public tangem android sdk
-keep class com.tangem.operations.** { *; }
-keep class com.tangem.crypto.** { *; }
-keep class com.tangem.common.card.** { *; }
# TODO remove (easy to fix in sdk)
-keep class com.tangem.common.json.** { *; }
-keep class com.tangem.common.SuccessResponse { *; }
-keep class com.tangem.common.UserCode { *; }
-keep class com.tangem.common.UserCodeType { *; }
# non-sensitive enums
-keep enum com.tangem.domain.apptheme.model.AppThemeMode { *; }
-keep class com.reown.walletkit.client.Wallet$Model { *; }
-keep class com.reown.walletkit.client.Wallet { *; }
-keep class **.R
-keep class **.R$* {
<fields>;
}

@ -1 +1 @@
Subproject commit 72d9d4b3c3f8e6604078798c4b5567327ed9b48a
Subproject commit 104b52520e8721eb775e2733deb361eed5067a63

View file

@ -222,7 +222,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
appLogsStore.deleteLastLogFile()
if (!BuildConfig.TESTER_MENU_ENABLED) {
appLogsStore.deleteLastLogFile()
}
// Temporally logs are not saved
// scope.launch {

View file

@ -35,7 +35,10 @@ class TangemAppLoggerInitializer(
}
if (PERMITTED_PRIORITY.contains(priority)) {
appLogsStore.saveLogMessage(message)
appLogsStore.saveLogMessage(
tag = tag ?: "TangemAppLogger",
message = message,
)
}
}
}

View file

@ -33,22 +33,24 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)
secureStorage.store(
json.encodeToByteArray(throwOnInvalidSequence = true),
VISA_AUTH_TOKENS_KEY,
createKey(cardId),
)
}
override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) {
secureStorage.get(VISA_AUTH_TOKENS_KEY)
override suspend fun get(cardId: String): VisaAuthTokens? = withContext(dispatcherProvider.io) {
secureStorage.get(createKey(cardId))
?.decodeToString(throwOnInvalidSequence = true)
?.let(tokensAdapter::fromJson)
}
private companion object {
const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens"
override fun remove(cardId: String) {
secureStorage.delete(createKey(cardId))
}
private fun createKey(cardId: String): String = "visa_auth_tokens_$cardId"
}

View file

@ -15,6 +15,6 @@ internal class TangemBlockchainSDKLogger(
) : BlockchainSDKLogger {
override fun log(level: BlockchainSDKLogger.Level, message: String) {
appLogsStore.saveLogMessage(message)
appLogsStore.saveLogMessage(tag = "BlockchainSDK_${level.name}", message)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.*
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
@ -22,14 +21,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object CardDomainModule {
@Provides
@Singleton
fun provideSetAccessCodeRequestPolicyUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
): SetAccessCodeRequestPolicyUseCase {
return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
@ -229,4 +230,10 @@ internal object OnrampDomainModule {
): FetchOnrampCountriesUseCase {
return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetHotCryptoTokensUseCase(hotCryptoRepository: HotCryptoRepository): GetHotCryptoUseCase {
return GetHotCryptoUseCase(hotCryptoRepository)
}
}

View file

@ -19,12 +19,6 @@ internal object PromoDomainModule {
return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
}
@Provides
@Singleton
fun provideShouldShowRingPromoUseCase(promoRepository: PromoRepository): ShouldShowRingPromoUseCase {
return ShouldShowRingPromoUseCase(promoRepository)
}
@Provides
@Singleton
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase {

View file

@ -182,12 +182,6 @@ internal object SettingsDomainModule {
): NeverRequestPermissionUseCase {
return NeverRequestPermissionUseCase(repository = permissionRepository)
}
@Provides
@Singleton
fun provideShouldSaveAccessCodesUseCase(settingsRepository: SettingsRepository): ShouldSaveAccessCodesUseCase {
return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository)
}
// endregion
@Provides

View file

@ -26,7 +26,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask
@ -35,16 +35,19 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
@ -494,6 +497,17 @@ internal class DefaultTangemSdkManager(
}
}
override suspend fun visaCustomerWalletApprove(
visaDataForApprove: VisaDataForApprove,
): CompletionResult<SignHashResponse> {
return runTaskAsyncReturnOnMain(
runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove,
),
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
// endregion
companion object {

View file

@ -19,13 +19,15 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.tap.domain.sdk.mocks.MockProvider
@Suppress("TooManyFunctions")
@ -207,4 +209,12 @@ class MockTangemSdkManager(
): CompletionResult<VisaCardActivationResponse> {
error("Not implemented")
}
override suspend fun visaCustomerWalletApprove(
visaDataForApprove: VisaDataForApprove,
): CompletionResult<SignHashResponse> {
error("Not implemented")
}
// endregion
}

View file

@ -311,15 +311,12 @@ private class ScanWalletProcessor(
}
private fun getWalletProductType(card: CardDTO): ProductType {
if (RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX)) {
return ProductType.Ring
}
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed
) {
ProductType.Wallet2
} else {
ProductType.Wallet
return when {
card.isVisa -> ProductType.Visa
RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX) -> ProductType.Ring
card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed -> ProductType.Wallet2
else -> ProductType.Wallet
}
}

View file

@ -1,16 +1,20 @@
package com.tangem.tap.domain.tasks.visa
import com.reown.util.hexToBytes
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
import com.tangem.crypto.CryptoUtils
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.datasource.local.visa.hasSavedOTP
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
@ -20,12 +24,15 @@ import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.jvm.Throws
import kotlin.time.measureTimedValue
@Suppress("LongParameterList")
class VisaCardActivationTask @AssistedInject constructor(
@ -36,9 +43,15 @@ class VisaCardActivationTask @AssistedInject constructor(
private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) : CardSessionRunnable<VisaCardActivationResponse> {
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val cardId: String,
val session: CardSession,
)
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
coroutineScope.launch {
callback(runSuspend(session))
@ -52,33 +65,47 @@ class VisaCardActivationTask @AssistedInject constructor(
return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message))
}
return if (challengeToSign != null) {
signAuthorizationChallenge(session, challengeToSign)
} else {
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
signOrder(session, activationOrder)
}
}
val context = SessionContext(
visaActivationRepository = visaActivationRepository,
cardId = card.cardId,
session = session,
)
private suspend fun signAuthorizationChallenge(
session: CardSession,
challengeToSign: VisaAuthChallenge.Card,
): CompletionResult<VisaCardActivationResponse> {
val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes())
val result = suspendCancellableCoroutine { continuation ->
attestationCommand.run(session = session) { attestationResponse ->
continuation.resume(attestationResponse)
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
if (challengeToSign != null) {
context.signAuthorizationChallenge(challengeToSign)
} else {
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
context.signOrder(activationOrder)
}
}
Timber.i("VisaCardActivationTask all time: ${timedResult.duration}")
return timedResult.value
}
return when (result) {
private suspend fun SessionContext.signAuthorizationChallenge(
challengeToSign: VisaAuthChallenge.Card,
): CompletionResult<VisaCardActivationResponse> {
val attestationCommand = AttestCardKeyCommand(challenge = CryptoUtils.generateRandomBytes(length = 16))
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
suspendCancellableCoroutine { continuation ->
attestationCommand.run(session = session) { attestationResponse ->
continuation.resume(attestationResponse)
}
}
}
Timber.i("AttestCardKeyCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("AttestCardKeyCommand success")
processSignedAuthorizationChallenge(
session = session,
signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(),
salt = result.data.salt.toHexString(),
@ -86,13 +113,13 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("AttestCardKeyCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
}
private suspend fun processSignedAuthorizationChallenge(
session: CardSession,
private suspend fun SessionContext.processSignedAuthorizationChallenge(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
return coroutineScope {
@ -107,12 +134,14 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await()
signOrder(session, order)
signOrder(order)
}
}
@Throws(TangemSdkError::class)
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder {
private suspend fun SessionContext.getActivationOrderToSign(
signedChallenge: VisaAuthSignedChallenge,
): ActivationOrder {
val tokens = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge)
}.getOrElse {
@ -121,12 +150,12 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
visaAuthTokenStorage.store(tokens)
visaAuthTokenStorage.store(cardId, tokens)
return visaActivationRepository.getActivationOrderToSign()
}
private suspend fun createWallet(session: CardSession): CompletionResult<Unit> {
private suspend fun SessionContext.createWallet(session: CardSession): CompletionResult<Unit> {
coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
@ -135,122 +164,152 @@ class VisaCardActivationTask @AssistedInject constructor(
createOTP(session)
} else {
val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve)
val result = suspendCancellableCoroutine { continuation ->
createWalletTask.run(session) { createWalletResult ->
continuation.resume(createWalletResult)
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
suspendCancellableCoroutine { continuation ->
createWalletTask.run(session) { createWalletResult ->
continuation.resume(createWalletResult)
}
}
}
when (result) {
Timber.i("CreateWalletTask time: ${timedResult.duration}")
when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
createOTP(session)
}
is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
}
}
private suspend fun createOTP(session: CardSession): CompletionResult<Unit> {
private suspend fun SessionContext.createOTP(session: CardSession): CompletionResult<Unit> {
coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val otp = otpStorage.getOTP(card.cardId)
return if (otp != null) {
return if (otpStorage.hasSavedOTP(cardId)) {
CompletionResult.Success(Unit)
} else {
val otpCommand = GenerateOTPCommand()
val result = suspendCancellableCoroutine { continuation ->
otpCommand.run(session) { otpResult ->
continuation.resume(otpResult)
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
suspendCancellableCoroutine { continuation ->
otpCommand.run(session) { otpResult ->
continuation.resume(otpResult)
}
}
}
when (result) {
Timber.i("GenerateOTPCommand time: ${timedResult.duration}")
when (val result = timedResult.value) {
is CompletionResult.Success -> {
otpStorage.saveOTP(card.cardId, result.data.rootOTP)
Timber.i("GenerateOTPCommand success")
otpStorage.saveOTP(cardId, result.data.rootOTP)
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("GenerateOTPCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
}
}
private suspend fun signOrder(
session: CardSession,
order: ActivationOrder,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
private suspend fun SessionContext.signOrder(order: ActivationOrder): CompletionResult<VisaCardActivationResponse> {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { signResult ->
continuation.resume(signResult)
card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val task = SignHashCommand(
hash = order.hash.hexToBytes(),
walletPublicKey = wallet.publicKey,
derivationPath = VisaUtilities.visaDefaultDerivationPath,
)
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
suspendCancellableCoroutine { continuation ->
task.run(session) { signResult ->
continuation.resume(signResult)
}
}
}
return when (result) {
Timber.i("SignHashCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("SignHashCommand success")
handleSignedOrder(
session = session,
activationOrder = order,
response = result.data,
)
}
is CompletionResult.Failure -> {
Timber.e("SignHashCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
}
private suspend fun handleSignedOrder(
session: CardSession,
private suspend fun SessionContext.handleSignedOrder(
activationOrder: ActivationOrder,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val signedOrder = SignedActivationOrder(
activationOrder = activationOrder,
signature = response.signature.toHexString(),
)
val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure(
val otp = otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(
TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
)
val activationResponse = VisaCardActivationResponse(
signedActivationOrder = signedOrder,
rootOTP = VisaRootOTP(otp.toHexString()),
)
return setupAccessCode().map {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
return setupAccessCode(session).map { activationResponse }
VisaCardActivationResponse(
signedActivationOrder = signedOrder,
rootOTP = VisaRootOTP(otp.toHexString()),
newCardDTO = CardDTO(card),
)
}
}
private suspend fun setupAccessCode(session: CardSession): CompletionResult<Unit> {
private suspend fun SessionContext.setupAccessCode(): CompletionResult<Unit> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.isAccessCodeSet) {
return CompletionResult.Success(Unit)
}
Timber.i("Setting access code")
val task = SetUserCodeCommand.changeAccessCode(accessCode)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { setAccessCodeResult ->
continuation.resume(setAccessCodeResult)
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
suspendCancellableCoroutine { continuation ->
task.run(session) { setAccessCodeResult ->
continuation.resume(setAccessCodeResult)
}
}
}
return when (result) {
Timber.i("SetUserCodeCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("SetUserCodeCommand success")
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.i("SetUserCodeCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}

View file

@ -0,0 +1,197 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.VisaActivationError
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
class VisaCustomerWalletApproveTask(
private val visaDataForApprove: VisaDataForApprove,
) : CardSessionRunnable<SignHashResponse> {
override fun run(session: CardSession, callback: CompletionCallback<SignHashResponse>) {
val card = session.environment.card ?: run {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) {
callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve")))
return
}
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
callback(
CompletionResult.Failure(
TangemSdkError.Underlying("Use tangem wallet specified during visa registration"),
),
)
return
}
if (card.settings.isHDWalletAllowed) {
proceedApprove(card, session, callback)
} else {
proceedApproveWithLegacyCard(card, session, callback)
}
}
private fun proceedApprove(card: Card, session: CardSession, callback: CompletionCallback<SignHashResponse>) {
val cardDTO = CardDTO(card)
val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run {
proceedApproveWithLegacyCard(
card = card,
session = session,
callback = callback,
)
return
}
val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run {
callback(
CompletionResult.Failure(
TangemSdkError.Underlying("Failed to generate derivation path with provided derivation style"),
),
)
return
}
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
callback(CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message)))
return
}
val derivationTask = DeriveWalletPublicKeyTask(
walletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
)
derivationTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
processDerivedKey(
wallet = wallet,
extendedPublicKey = result.data,
derivationPath = derivationPath,
session = session,
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun processDerivedKey(
wallet: CardWallet,
extendedPublicKey: ExtendedPublicKey,
derivationPath: DerivationPath,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val validationResult = VisaWalletPublicKeyUtility.validateExtendedPublicKey(
targetAddress = visaDataForApprove.targetAddress,
extendedPublicKey = extendedPublicKey,
)
validationResult.onLeft {
callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message)))
return
}
signApproveData(
targetWalletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
session = session,
callback = callback,
)
}
private fun proceedApproveWithLegacyCard(
card: Card,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val publicKey = findKeyWithoutDerivation(
targetAddress = visaDataForApprove.targetAddress,
card = CardDTO(card),
).getOrElse {
callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message)))
return
}
signApproveData(
targetWalletPublicKey = publicKey,
derivationPath = null,
session = session,
callback = callback,
)
}
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val signTask = SignHashCommand(
hash = visaDataForApprove.approveHash.hexToBytes(),
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
scanCard(
signHashResponse = result.data,
session = session,
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun scanCard(
signHashResponse: SignHashResponse,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val scanTask = ScanTask()
scanTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(signHashResponse))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.userWalletList.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
@ -7,16 +8,24 @@ import com.tangem.domain.wallets.models.UserWalletId
@JsonClass(generateAdapter = true)
internal data class UserWalletSensitiveInformation(
@Json(name = "wallets")
val wallets: List<CardDTO.Wallet>,
)
@JsonClass(generateAdapter = true)
internal data class UserWalletPublicInformation(
@Json(name = "name")
val name: String,
@Json(name = "walletId")
val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>,
@Json(name = "scanResponse")
val scanResponse: ScanResponse,
@Json(name = "isMultiCurrency")
val isMultiCurrency: Boolean,
@Json(name = "hasBackupError")
val hasBackupError: Boolean = false,
)

View file

@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -24,10 +25,16 @@ import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
) {
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val cardId: String,
val session: CardSession,
)
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan")
@ -36,6 +43,14 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
}
val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
val context = SessionContext(
visaActivationRepository = visaActivationRepository,
cardId = card.cardId,
session = session,
)
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
@ -43,13 +58,10 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(activationStatus)
}
return deriveKey(wallet, session)
return context.deriveKey(wallet)
}
private suspend fun deriveKey(
wallet: CardWallet,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult<VisaCardActivationStatus> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while first scan")
@ -64,17 +76,16 @@ internal class VisaCardScanHandler @Inject constructor(
continuation.resume(result)
}
}
return handleDerivationResponse(derivationTaskResult, session)
return handleDerivationResponse(derivationTaskResult)
}
private suspend fun handleDerivationResponse(
private suspend fun SessionContext.handleDerivationResponse(
result: CompletionResult<ExtendedPublicKey>,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
return when (result) {
is CompletionResult.Success -> {
Timber.i("Start task for loading challenge for Visa wallet")
handleWalletAuthorization(session)
handleWalletAuthorization()
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
@ -82,9 +93,8 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet")
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while handling wallet authorization")
@ -93,6 +103,8 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(
@ -108,7 +120,6 @@ internal class VisaCardScanHandler @Inject constructor(
}
Timber.i("Requesting challenge for wallet authorization")
// Will be changed later after backend implementation
val challengeResponse = runCatching {
visaAuthRepository.getCustomerWalletAuthChallenge(
cardId = card.cardId,
@ -122,14 +133,12 @@ internal class VisaCardScanHandler @Inject constructor(
publicKey = wallet.publicKey,
derivationPath = derivationPath,
nonce = challengeResponse.challenge,
session = session,
)
return when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
session = session,
signedChallenge = challengeResponse
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
)
@ -141,8 +150,7 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
private suspend fun handleWalletAuthorizationTokens(
session: CardSession,
private suspend fun SessionContext.handleWalletAuthorizationTokens(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching {
@ -151,17 +159,20 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization(session)
return handleCardAuthorization()
}
visaAuthTokenStorage.store(authorizationTokensResponse)
visaAuthTokenStorage.store(
cardId = cardId,
tokens = authorizationTokensResponse,
)
Timber.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.handleCardAuthorization(): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
@ -178,7 +189,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge)
val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> {
@ -210,7 +221,10 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
visaAuthTokenStorage.store(authorizationTokensResponse)
visaAuthTokenStorage.store(
cardId = card.cardId,
tokens = authorizationTokensResponse,
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
@ -239,47 +253,31 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
private suspend fun signChallengeWithWallet(
private suspend fun SessionContext.signChallengeWithWallet(
publicKey: ByteArray,
derivationPath: DerivationPath,
nonce: String,
session: CardSession,
): CompletionResult<SignHashResponse> {
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
val result = suspendCancellableCoroutine {
val signHashCommand = SignHashCommand(
hash = nonce.hexToBytes(),
walletPublicKey = publicKey,
derivationPath = derivationPath,
)
return suspendCancellableCoroutine {
signHashCommand.run(session) { result ->
it.resume(result)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(result.data)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun signChallengeWithCard(
session: CardSession,
private suspend fun SessionContext.signChallengeWithCard(
challenge: String,
): CompletionResult<AttestCardKeyResponse> {
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())
val result = suspendCancellableCoroutine { continuation ->
val signHashCommand = AttestCardKeyCommand(challenge = challenge.hexToBytes())
return suspendCancellableCoroutine { continuation ->
signHashCommand.run(session) { result ->
continuation.resume(result)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(result.data)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}

View file

@ -52,6 +52,8 @@ data class WcSignMessage(
@Json(name = "type")
val type: WCSignType,
) : WcRequestData {
@JsonClass(generateAdapter = false)
enum class WCSignType {
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE,
}

View file

@ -1,3 +1,16 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?)
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Account(
@Json(name = "chainId")
val chainId: String,
@Json(name = "walletAddress")
val walletAddress: String,
@Json(name = "derivationPath")
val derivationPath: String?,
)

View file

@ -1,6 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Session(
@Json(name = "topic")
val topic: String,
@Json(name = "accounts")
val accounts: List<Account>,
)

View file

@ -2,29 +2,34 @@ package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
@JsonClass(generateAdapter = true)
class WcBinanceCancelOrder(
data class WcBinanceCancelOrder(
@Json(name = "account_number")
accountNumber: String,
val accountNumber: String,
@Json(name = "chain_id")
chainId: String,
val chainId: String,
@Json(name = "data")
data: String?,
val data: String?,
@Json(name = "memo")
memo: String?,
val memo: String?,
@Json(name = "sequence")
sequence: String,
val sequence: String,
@Json(name = "source")
source: String,
val source: String,
@Json(name = "msgs")
msgs: List<Message>,
) : WcBinanceOrder<WcBinanceCancelOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "refid")
val refid: String,
@Json(name = "sender")
val sender: String,
@Json(name = "symbol")
val symbol: String,
)
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
open class WcBinanceOrder<T>(
val accountNumber: String,
val chainId: String,
val data: String?,
val memo: String?,
val sequence: String,
val source: String,
val msgs: List<T>,
) : WcRequestData
data class WcBinanceTxConfirmParam(
val ok: Boolean,
val errorMsg: String?,
) : WcRequestData

View file

@ -4,21 +4,28 @@ import com.github.salomonbrys.kotson.jsonSerializer
import com.google.gson.JsonObject
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
@JsonClass(generateAdapter = true)
class WcBinanceTradeOrder(
data class WcBinanceTradeOrder(
@Json(name = "account_number")
accountNumber: String,
val accountNumber: String,
@Json(name = "chain_id")
chainId: String,
data: String?,
memo: String?,
sequence: String,
source: String,
msgs: List<Message>,
) : WcBinanceOrder<WcBinanceTradeOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
val chainId: String,
@Json(name = "data")
val data: String?,
@Json(name = "memo")
val memo: String?,
@Json(name = "sequence")
val sequence: String,
@Json(name = "source")
val source: String,
@Json(name = "msgs")
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = false)
enum class MessageKey(val key: String) {
ID("id"),
ORDER_TYPE("ordertype"),
@ -30,14 +37,23 @@ class WcBinanceTradeOrder(
TIME_INFORCE("timeinforce"),
}
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "id")
val id: String,
@Json(name = "orderType")
val orderType: Int,
@Json(name = "price")
val price: Long,
@Json(name = "quantity")
val quantity: Long,
@Json(name = "sender")
val sender: String,
@Json(name = "side")
val side: Int,
@Json(name = "symbol")
val symbol: String,
@Json(name = "timeInforce")
val timeInforce: Int,
)
}

View file

@ -2,33 +2,48 @@ package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
@JsonClass(generateAdapter = true)
class WcBinanceTransferOrder(
data class WcBinanceTransferOrder(
@Json(name = "account_number")
accountNumber: String,
val accountNumber: String,
@Json(name = "chain_id")
chainId: String,
data: String?,
memo: String?,
sequence: String,
source: String,
msgs: List<Message>,
) : WcBinanceOrder<WcBinanceTransferOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
val chainId: String,
@Json(name = "data")
val data: String?,
@Json(name = "memo")
val memo: String?,
@Json(name = "sequence")
val sequence: String,
@Json(name = "source")
val source: String,
@Json(name = "msgs")
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "inputs")
val inputs: List<Item>,
@Json(name = "outputs")
val outputs: List<Item>,
) {
@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "address")
val address: String,
@Json(name = "coins")
val coins: List<Coin>,
) {
@JsonClass(generateAdapter = true)
data class Coin(
@Json(name = "amount")
val amount: Long,
@Json(name = "denom")
val denom: String,
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@JsonClass(generateAdapter = true)
data class WcBinanceTxConfirmParam(
@Json(name = "ok")
val ok: Boolean,
@Json(name = "errorMsg")
val errorMsg: String?,
) : WcRequestData

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
@ -11,13 +12,16 @@ import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -34,17 +38,24 @@ import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
private val scanCardProcessor: ScanCardProcessor,
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private var previousBiometricsRequestPolicy: Boolean = false
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.CardSettings.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for CardSettingsViewModel")
@ -52,12 +63,32 @@ internal class CardSettingsViewModel @Inject constructor(
val screenState: MutableStateFlow<CardSettingsScreenState> = MutableStateFlow(getInitialState())
init {
updateAccessCodeRequestPolicy()
cardSettingsInteractor.scannedScanResponse
.filterNotNull()
.onEach(::updateCardDetails)
.launchIn(viewModelScope)
}
override fun onCleared() {
// Restore the previous value of access code request policy
cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy
}
private fun updateAccessCodeRequestPolicy() {
runBlocking {
// !!!IMPORTANT!!!: Do not forget to restore the previous value in onCleared() method
previousBiometricsRequestPolicy = cardSdkConfigRepository.isBiometricsRequestPolicy
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet $userWalletId not found") }
cardSdkConfigRepository.isBiometricsRequestPolicy = userWallet.scanResponse.card.isAccessCodeSet &&
settingsRepository.shouldSaveAccessCodes()
}
}
private fun getInitialState() = CardSettingsScreenState(
cardDetails = null,
onElementClick = ::handleClickingItem,

View file

@ -4,7 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -27,13 +27,12 @@ internal fun SettingsScreensScaffold(
addBottomInsets: Boolean = true,
fab: @Composable () -> Unit = {},
) {
val state = rememberScaffoldState(snackbarHostState = snackbarHostState)
val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick)
Scaffold(
scaffoldState = state,
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
EmptyTopBarWithNavigation(
modifier = Modifier.statusBarsPadding(),
@ -43,7 +42,7 @@ internal fun SettingsScreensScaffold(
},
modifier = modifier,
contentWindowInsets = WindowInsetsZero,
backgroundColor = backgroundColor,
containerColor = backgroundColor,
floatingActionButton = {
Box(modifier = Modifier.navigationBarsPadding()) {
fab()
@ -89,6 +88,7 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
@ -108,8 +108,9 @@ internal fun EmptyTopBarWithNavigation(
)
}
},
backgroundColor = backgroundColor,
elevation = 0.dp,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = backgroundColor,
),
)
}

View file

@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -15,6 +18,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.Settings
@ -71,7 +75,7 @@ private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifie
@Composable
private fun EmptyScreen(state: WalletConnectScreenState) {
if (state.isLoading) {
LinearProgressIndicator(
TangemLinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = TangemTheme.colors.icon.accent,
)
@ -102,7 +106,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
@Composable
private fun WalletConnectSessions(state: WalletConnectScreenState) {
if (state.isLoading) {
LinearProgressIndicator(
TangemLinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(2.dp),

View file

@ -12,10 +12,10 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.ShouldSaveAccessCodesUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
@ -39,11 +39,11 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class HomeViewModel @Inject constructor(
private val shouldSaveAccessCodesUseCase: ShouldSaveAccessCodesUseCase,
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
private val scanCardProcessor: ScanCardProcessor,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -73,7 +73,7 @@ internal class HomeViewModel @Inject constructor(
private fun scanCard() {
viewModelScope.launch {
setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = shouldSaveAccessCodesUseCase())
cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes()
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,

View file

@ -13,6 +13,7 @@ import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
@ -42,6 +43,12 @@ object OnboardingHelper {
store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response)
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isVisaWallet() -> {
if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
response.visaCardActivationStatus !is VisaCardActivationStatus.Activated
}
response.cardTypesResolver.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
@ -66,7 +73,8 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
scanResponse.productType == ProductType.Ring ||
scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported
scanResponse.productType == ProductType.Wallet ||
scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled &&
newOnboardingSupportTypes
) {

View file

@ -4,8 +4,8 @@ import android.content.DialogInterface
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View file

@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View file

@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View file

@ -1,14 +1,20 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import javax.inject.Inject
internal class DefaultVisaAuthProvider @Inject constructor(
private val authStorage: VisaAuthTokenStorage,
private val userWalletsListManager: UserWalletsListManager,
) : TangemVisaAuthProvider {
override suspend fun getAuthHeader(): String {
return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!"
override suspend fun getAuthHeader(cardId: String): String {
val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId }
val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated
?: return "Error in the app!"
val accessToken = status.visaAuthTokens.accessToken
return "Bearer $accessToken"
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Path
@ -11,21 +12,25 @@ interface MercuryoApi {
suspend fun currencies(@Path("apiVersion") apiVersion: String): MercuryoCurrenciesResponse
}
@JsonClass(generateAdapter = true)
data class MercuryoCurrenciesResponse(
val status: Int,
val data: Data,
@Json(name = "status") val status: Int,
@Json(name = "data") val data: Data,
) {
@JsonClass(generateAdapter = true)
data class Data(
val fiat: List<String>,
val crypto: List<String>,
val config: Config,
@Json(name = "fiat") val fiat: List<String>,
@Json(name = "crypto") val crypto: List<String>,
@Json(name = "config") val config: Config,
)
@JsonClass(generateAdapter = true)
data class Config(
@Json(name = "crypto_currencies")
val cryptoCurrencies: List<MercuryoCryptoCurrency>,
)
@JsonClass(generateAdapter = true)
data class MercuryoCryptoCurrency(
@Json(name = "currency")
val currencySymbol: String,

View file

@ -149,5 +149,7 @@ internal val Blockchain.mercuryoNetwork: String?
Blockchain.Clore -> null
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> null
Blockchain.Bitrock, Blockchain.BitrockTestnet -> null
Blockchain.Sonic, Blockchain.SonicTestnet -> null
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> null
}
}

View file

@ -22,7 +22,9 @@ interface MoonPayApi {
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
@Json(name = "isBuyAllowed")
val isBuyAllowed: Boolean,
@Json(name = "isSellAllowed")
val isSellAllowed: Boolean,
@Json(name = "isAllowed")
val isMoonpayAllowed: Boolean,
@ -34,18 +36,18 @@ data class MoonPayUserStatus(
@JsonClass(generateAdapter = true)
data class MoonPayCurrencies(
val type: String,
val code: String,
val supportsLiveMode: Boolean = false,
val isSuspended: Boolean = true,
val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(),
val metadata: MoonPayCurrenciesMetadata? = null,
@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(
val contractAddress: String?,
val networkCode: String?,
@Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "networkCode") val networkCode: String?,
)

View file

@ -150,4 +150,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
VanarChainTestnet -> null
OdysseyChain, OdysseyChainTestnet -> null
Bitrock, BitrockTestnet -> null
Sonic, SonicTestnet -> null
ApeChain, ApeChainTestnet -> null
}

View file

@ -0,0 +1,17 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="4dp"/>
<solid android:color="@color/icon_informative" />
</shape>
</item>
<item android:id="@android:id/progress">
<scale android:scaleWidth="100%">
<shape>
<corners android:radius="4dp" />
<solid android:color="@color/icon_primary_1" />
</shape>
</scale>
</item>
</layer-list>

View file

@ -4,9 +4,10 @@
android:id="@+id/pb_state"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:progressBackgroundTint="@color/icon_informative"
android:progressTint="@color/icon_primary_1" />
android:paddingVertical="8dp"
android:indeterminate="false"
android:progressDrawable="@drawable/progress_rounded_shape" />
</merge>

View file

@ -0,0 +1,9 @@
package com.tangem.common.timemeasure
import android.os.SystemClock
import kotlin.time.AbstractLongTimeSource
import kotlin.time.DurationUnit
object RealtimeMonotonicTimeSource : AbstractLongTimeSource(DurationUnit.NANOSECONDS) {
override fun read(): Long = SystemClock.elapsedRealtimeNanos()
}

View file

@ -15,7 +15,6 @@ import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerValueForma
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
import com.patrykandpatrick.vico.core.common.Dimensions
import com.patrykandpatrick.vico.core.common.LayeredComponent
import com.patrykandpatrick.vico.core.common.component.Component
import com.patrykandpatrick.vico.core.common.component.TextComponent
import com.patrykandpatrick.vico.core.common.shape.Shape
import com.tangem.core.ui.res.TangemTheme
@ -39,7 +38,28 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
object : DefaultCartesianMarker(
label = TextComponent(textSizeSp = 0f),
indicator = ::indicator,
indicator = { color ->
val composeColor = Color(color)
LayeredComponent(
rear = shapeComponent(
color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA),
shape = Shape.Pill,
),
front = LayeredComponent(
rear = shapeComponent(
color = composeColor,
shape = Shape.Pill,
),
front = shapeComponent(
color = Color.White,
shape = Shape.Pill,
),
padding = indicatorPadding,
),
padding = indicatorPadding,
)
},
indicatorSizeDp = INDICATOR_SIZE_DP,
guideline = guideline,
valueFormatter = object : CartesianMarkerValueFormatter {
@ -81,29 +101,6 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
}
}
private fun indicator(color: Int): Component {
val composeColor = Color(color)
return LayeredComponent(
rear = shapeComponent(
color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA),
shape = Shape.Pill,
),
front = LayeredComponent(
rear = shapeComponent(
color = composeColor,
shape = Shape.Pill,
),
front = shapeComponent(
color = Color.White,
shape = Shape.Pill,
),
padding = indicatorPadding,
),
padding = indicatorPadding,
)
}
private val indicatorPadding = Dimensions.of(3.dp)
private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f
private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f

View file

@ -0,0 +1,45 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@Composable
fun ColumnScope.ExpressHideButton(isTerminal: Boolean, isAutoDisposable: Boolean, onClick: () -> Unit) {
AnimatedVisibility(
visible = isTerminal && !isAutoDisposable,
label = "Hide button visibility animation",
) {
Text(
text = stringResourceSafe(R.string.express_status_hide_button_text),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.button,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 14.dp)
.clip(RoundedCornerShape(12.dp))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = onClick,
)
.padding(vertical = 10.dp),
)
}
}

View file

@ -59,6 +59,11 @@ fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
SpacerH12()
ExpressStatusBlock(state = state.info.status)
ExpressStatusNotificationBlock(state = state.info.notification)
ExpressHideButton(
isTerminal = state.activeStatus.isTerminal,
isAutoDisposable = state.activeStatus.isAutoDisposable,
onClick = state.info.onDisposeExpressStatus,
)
SpacerH24()
}
}

View file

@ -30,6 +30,7 @@ data class ExpressTransactionStateInfoUM(
val timestampFormatted: TextReference,
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val onDisposeExpressStatus: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toFiatAmount: TextReference?,

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -11,7 +13,6 @@ android {
}
dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
@ -23,6 +24,7 @@ dependencies {
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
/** Core modules */
implementation(projects.core.datasource)

View file

@ -14,5 +14,13 @@
{
"name": "vanar-chain",
"version": "undefined"
},
{
"name": "sonic",
"version": "undefined"
},
{
"name": "apechain",
"version": "undefined"
}
]

View file

@ -21,7 +21,7 @@
},
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",
"version": "undefined"
"version": "5.21.0"
},
{
"name": "NAVIGATION_REFACTORING",
@ -34,5 +34,13 @@
{
"name": "VISA_ONBOARDING_ENABLED",
"version": "undefined"
},
{
"name": "ONRAMP_HOT_TOKENS_ENABLED",
"version": "undefined"
},
{
"name": "STAKING_TON_ENABLED",
"version": "undefined"
}
]

View file

@ -1,3 +1,5 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
@ -29,6 +31,7 @@ dependencies {
implementation(projects.domain.staking.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.models)
implementation(projects.features.onramp.api)
/** Tangem libraries */
implementation(deps.tangem.blockchain)
@ -53,6 +56,8 @@ dependencies {
implementation(deps.okHttp.prettyLogging)
implementation(deps.retrofit)
implementation(deps.retrofit.moshi)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
/** Time */
implementation(deps.jodatime)
@ -62,6 +67,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
debugPGImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)

View file

@ -40,6 +40,7 @@ sealed class ApiConfig {
companion object {
internal const val DEBUG_BUILD_TYPE = "debug"
internal const val DEBUG_PG_BUILD_TYPE = "debugPG"
internal const val INTERNAL_BUILD_TYPE = "internal"
internal const val MOCKED_BUILD_TYPE = "mocked"
internal const val EXTERNAL_BUILD_TYPE = "external"

View file

@ -1,10 +1,21 @@
package com.tangem.datasource.api.common.config
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Api environment
*
[REDACTED_AUTHOR]
*/
@JsonClass(generateAdapter = false)
enum class ApiEnvironment {
DEV, STAGE, PROD
@Json(name = "DEV")
DEV,
@Json(name = "STAGE")
STAGE,
@Json(name = "PROD")
PROD,
}

View file

@ -68,7 +68,9 @@ internal class Express(
fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE -> ApiEnvironment.DEV
DEBUG_BUILD_TYPE,
DEBUG_PG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE

View file

@ -1,11 +1,8 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.utils.ProviderSuspend
internal class TangemVisa(
private val authProvider: TangemVisaAuthProvider,
) : ApiConfig() {
internal class TangemVisa : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
@ -19,7 +16,5 @@ internal class TangemVisa(
headers = createHeaders(),
)
private fun createHeaders() = mapOf(
"Authorization" to ProviderSuspend { authProvider.getAuthHeader() },
)
private fun createHeaders() = mapOf<String, ProviderSuspend<String>>()
}

View file

@ -2,5 +2,5 @@ package com.tangem.datasource.api.common.visa
interface TangemVisaAuthProvider {
suspend fun getAuthHeader(): String
suspend fun getAuthHeader(cardId: String): String
}

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AssetsRequestBody(
@Json(name = "tokensList") val tokensList: List<LeastTokenInfo>?,
)

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ExchangeSentRequestBody(
@Json(name = "txId")
val txId: String,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LeastTokenInfo(
@Json(name = "contractAddress")
val contractAddress: String,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PairsRequestBody(
@Json(name = "from")
val from: List<LeastTokenInfo>,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Asset(
@Json(name = "contractAddress")
val contractAddress: String,

View file

@ -1,11 +1,17 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ExchangeDataResponseWithTxDetails(
@Json(name = "dataResponse")
val dataResponse: ExchangeDataResponse,
@Json(name = "txDetails")
val txDetails: TxDetails,
)
@JsonClass(generateAdapter = true)
data class ExchangeDataResponse(
@Json(name = "fromAmount")
val fromAmount: String,
@ -29,6 +35,7 @@ data class ExchangeDataResponse(
val signature: String,
)
@JsonClass(generateAdapter = true)
data class TxDetails(
@Json(name = "payoutAddress")
val payoutAddress: String,
@ -70,6 +77,7 @@ data class TxDetails(
val gas: String?,
)
@JsonClass(generateAdapter = false)
enum class TxType {
@Json(name = "send")
SEND,

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class ExchangeProvider(
@Json(name = "id")
val id: String,
@ -32,6 +34,7 @@ data class ExchangeProvider(
val slippage: BigDecimal?,
)
@JsonClass(generateAdapter = false)
enum class ExchangeProviderType {
@Json(name = "dex")
DEX,

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class ExchangeQuoteResponse(
@Json(name = "fromAmount")

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ExchangeSentResponseBody(
@Json(name = "txId")
val txId: String,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ExchangeStatusResponse(
@Json(name = "providerId")
@ -26,6 +28,7 @@ data class ExchangeStatusResponse(
val refundContractAddress: String? = null,
)
@JsonClass(generateAdapter = false)
enum class ExchangeStatus {
@Json(name = "new")
@ -71,6 +74,7 @@ enum class ExchangeStatus {
Unknown,
}
@JsonClass(generateAdapter = true)
data class ExchangeStatusError(
@Json(name = "code")
val code: Int,

View file

@ -1,13 +1,16 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class ExpressErrorResponse(
@Json(name = "error")
val error: ExpressError,
)
@JsonClass(generateAdapter = true)
data class ExpressError(
@Json(name = "code")
val code: Int,
@ -19,6 +22,7 @@ data class ExpressError(
val value: ExpressErrorValue?,
)
@JsonClass(generateAdapter = true)
data class ExpressErrorValue(
@Json(name = "minAmount")
val minAmount: String?,

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
@JsonClass(generateAdapter = true)
data class SwapPair(
@Json(name = "from")
val from: LeastTokenInfo,
@ -15,6 +17,7 @@ data class SwapPair(
)
@JsonClass(generateAdapter = true)
data class SwapPairProvider(
@Json(name = "providerId")
val providerId: String,
@ -23,6 +26,7 @@ data class SwapPairProvider(
val rateTypes: List<RateType>,
)
@JsonClass(generateAdapter = false)
enum class RateType {
@Json(name = "float")
FLOAT,

View file

@ -3,8 +3,11 @@ package com.tangem.datasource.api.onramp.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OnrampDataResponseWithTxDetails(
@Json(name = "dataResponse")
val dataResponse: OnrampDataResponse,
@Json(name = "txDetails")
val txDetails: OnrampTxDetails,
)

View file

@ -60,6 +60,7 @@ data class OnrampStatusResponse(
val countryCode: String,
)
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "created")
Created,

View file

@ -1,11 +1,13 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
@JsonClass(generateAdapter = true)
data class PendingActionRequestBody(
@Json(name = "type")
val type: StakingActionType,
@ -19,6 +21,7 @@ data class PendingActionRequestBody(
val gasArgs: GasArgs? = null, // used only in estimate_gas request
)
@JsonClass(generateAdapter = true)
data class ActionRequestBody(
@Json(name = "integrationId")
val integrationId: String,
@ -32,6 +35,7 @@ data class ActionRequestBody(
val gasArgs: GasArgs? = null, // used only in estimate_gas request
)
@JsonClass(generateAdapter = true)
data class ActionRequestBodyArgs(
@Json(name = "amount")
val amount: String,
@ -55,6 +59,7 @@ data class ActionRequestBodyArgs(
val inputToken: TokenDTO? = null,
)
@JsonClass(generateAdapter = true)
data class SignatureVerification(
@Json(name = "message")
val message: String,
@ -62,6 +67,7 @@ data class SignatureVerification(
val signed: String,
)
@JsonClass(generateAdapter = false)
enum class TronResource {
@Json(name = "ENERGY")
ENERGY,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Address(
@Json(name = "address")
val address: String,
@ -11,6 +13,7 @@ data class Address(
val explorerUrl: String? = null,
) {
@JsonClass(generateAdapter = true)
data class AdditionalAddresses(
// cosmos-specific
@Json(name = "cosmosPubKey")

View file

@ -1,14 +1,17 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class ConstructTransactionRequestBody(
@Json(name = "gasArgs")
val gasArgs: GasArgs? = null,
@Json(name = "ledgerWalletAPICompatible")
val ledgerWalletAPICompatible: Boolean? = null,
) {
@JsonClass(generateAdapter = true)
data class GasArgs(
// cosmos-specific
@Json(name = "gasPrice")

View file

@ -1,19 +1,23 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class MultipleYieldBalancesRequestBody(
@Json(name = "addresses") val addresses: Address,
@Json(name = "args") val args: MultipleYieldBalancesRequestArgs,
@Json(name = "integrationId") val integrationId: String,
) {
@JsonClass(generateAdapter = true)
data class Address(
@Json(name = "address") val address: String,
@Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null,
@Json(name = "explorerUrl") val explorerUrl: String,
) {
@JsonClass(generateAdapter = true)
data class AdditionalAddresses(
@Json(name = "cosmosPubKey") val cosmosPubKey: String? = null,
@Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null,
@ -25,6 +29,7 @@ data class MultipleYieldBalancesRequestBody(
)
}
@JsonClass(generateAdapter = true)
data class MultipleYieldBalancesRequestArgs(
@Json(name = "validatorAddresses") val validatorAddresses: List<String>,
)

View file

@ -1,8 +1,13 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class RevenueOption(val value: String) {
SUPPORTS_FEE("supportsFee"),
SUPPORTS_REV_SHARE("supportsRevShare"),
@Json(name = "SUPPORTS_FEE") SUPPORTS_FEE("supportsFee"),
@Json(name = "SUPPORTS_REV_SHARE") SUPPORTS_REV_SHARE("supportsRevShare"),
;
override fun toString(): String {

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SubmitTransactionHashRequestBody(
@Json(name = "hash")
val hash: String,

View file

@ -1,13 +1,16 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class YieldBalanceRequestBody(
@Json(name = "addresses") val addresses: Address,
@Json(name = "args") val args: YieldBalanceRequestArgs,
@Json(name = "integrationId") val integrationId: String,
) {
@JsonClass(generateAdapter = true)
data class YieldBalanceRequestArgs(
@Json(name = "validatorAddresses") val validatorAddresses: List<String>,
)

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class NetworkTypeDTO {
@Json(name = "avalanche-c")
AVALANCHE_C,
@ -201,5 +203,8 @@ enum class NetworkTypeDTO {
@Json(name = "tron")
TRON,
@Json(name = "ton")
TON,
UNKNOWN,
}

View file

@ -41,6 +41,7 @@ data class BalanceDTO(
val providerId: String?,
) {
@JsonClass(generateAdapter = false)
enum class BalanceTypeDTO {
@Json(name = "available")
AVAILABLE,

View file

@ -85,7 +85,7 @@ data class YieldDTO(
@Json(name = "preferred")
val preferred: Boolean?,
) {
@JsonClass(generateAdapter = true)
@JsonClass(generateAdapter = false)
enum class ValidatorStatusDTO {
@Json(name = "active")
ACTIVE,
@ -99,6 +99,9 @@ data class YieldDTO(
@Json(name = "jailed")
JAILED,
@Json(name = "full")
FULL,
UNKNOWN,
}
}
@ -153,6 +156,7 @@ data class YieldDTO(
val enabled: Boolean?,
)
@JsonClass(generateAdapter = false)
enum class RewardScheduleDTO {
@Json(name = "block")
BLOCK,
@ -178,6 +182,7 @@ data class YieldDTO(
UNKNOWN,
}
@JsonClass(generateAdapter = false)
enum class RewardClaimingDTO {
@Json(name = "auto")
AUTO,
@ -189,6 +194,7 @@ data class YieldDTO(
}
}
@JsonClass(generateAdapter = false)
enum class RewardTypeDTO {
@Json(name = "apy")
APY, // compound rate

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.response.model.action
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class StakingActionStatusDTO {
@Json(name = "CANCELED")
CANCELED,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.response.model.action
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class StakingActionTypeDTO {
@Json(name = "STAKE")
STAKE,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.response.model.error
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class StakeKitErrorResponse(
@Json(name = "message")
val message: String? = null,
@ -26,6 +28,7 @@ class StakeKitErrorResponse(
val tags: List<String>? = null,
)
@JsonClass(generateAdapter = true)
data class StakeKitErrorDetailsDTO(
@Json(name = "arguments")
val arguments: String? = null,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class StakingTransactionStatusDTO {
@Json(name = "NOT_FOUND")
NOT_FOUND,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class StakingTransactionTypeDTO {
@Json(name = "SWAP")
SWAP,

View file

@ -122,6 +122,9 @@ interface TangemTechApi {
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
@GET("hot_crypto")
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>
companion object {
val marketsQuoteFields = listOf(
"price",

View file

@ -1,14 +1,17 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class CoinsResponse(
@Json(name = "imageHost") val imageHost: String?,
@Json(name = "coins") val coins: List<Coin>,
@Json(name = "total") val total: Int,
) {
@JsonClass(generateAdapter = true)
data class Coin(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@ -17,6 +20,7 @@ data class CoinsResponse(
@Json(name = "networks") val networks: List<Network> = listOf(),
) {
@JsonClass(generateAdapter = true)
data class Network(
@Json(name = "networkId") val networkId: String,
@Json(name = "contractAddress") val contractAddress: String? = null,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CreateUserTokensAccountBody(
@Json(name = "accountId")
val id: Int,

View file

@ -17,7 +17,7 @@ data class CurrenciesResponse(
@Json(name = "rateBTC") val rateBTC: String,
@Json(name = "unit") val unit: String, // $, €, ₽
@Json(name = "type") val type: String,
val iconSmallUrl: String? = null,
val iconMediumUrl: String? = null,
@Json(name = "iconSmallUrl") val iconSmallUrl: String? = null,
@Json(name = "iconMediumUrl") val iconMediumUrl: String? = null,
)
}

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FeaturesResponse(
@Json(name = "send") val isNewSendEnabled: Boolean,
)

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class HotCryptoResponse(
@Json(name = "imageHost") val imageHost: String? = null,
@Json(name = "tokens") val tokens: List<Token>,
) {
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
@Json(name = "network_id") val networkId: String,
@Json(name = "current_price") val currentPrice: BigDecimal,
@Json(name = "price_change_percentage_24h") val priceChangePercentage: BigDecimal,
@Json(name = "contractAddress") val contractAddress: String? = null, // specific for token
@Json(name = "decimal_count") val decimalCount: Int? = null, // specific for token
)
}

View file

@ -1,13 +1,16 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class QuotesResponse(
@Json(name = "quotes")
val quotes: Map<String, Quote>,
) {
@JsonClass(generateAdapter = true)
data class Quote(
@Json(name = "price")
val price: BigDecimal?,

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// rates.keys = networkId's
@JsonClass(generateAdapter = true)
data class RatesResponse(
@Json(name = "rates") val rates: Map<String, Double>,
)

View file

@ -1,30 +1,36 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.LocalDate
@JsonClass(generateAdapter = true)
data class ReferralResponse(
@Json(name = "conditions") val conditions: Conditions,
@Json(name = "referral") val referral: Referral?,
@Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?,
) {
@JsonClass(generateAdapter = true)
data class Conditions(
@Json(name = "discount") val discount: Discount,
@Json(name = "awards") val awards: List<Award>,
@Json(name = "tosLink") val tosLink: String,
) {
@JsonClass(generateAdapter = true)
data class Discount(
@Json(name = "amount") val amount: Int,
@Json(name = "type") val discountType: String,
)
@JsonClass(generateAdapter = true)
data class Award(
@Json(name = "amount") val amount: Int,
@Json(name = "token") val token: Token,
) {
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@ -36,6 +42,7 @@ data class ReferralResponse(
}
}
@JsonClass(generateAdapter = true)
data class Referral(
@Json(name = "shareLink") val shareLink: String,
@Json(name = "address") val address: String,
@ -44,11 +51,13 @@ data class ReferralResponse(
@Json(name = "termsAcceptedAt") val termsAcceptedAt: String?,
)
@JsonClass(generateAdapter = true)
data class ExpectedAwards(
@Json(name = "numberOfWallets") val numberOfWallets: Int,
@Json(name = "list") val list: List<AwardItem>,
) {
@JsonClass(generateAdapter = true)
data class AwardItem(
@Json(name = "currency") val currency: String,
@Json(name = "paymentDate") val paymentDate: LocalDate,

View file

@ -1,10 +1,12 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Sales response
*/
@JsonClass(generateAdapter = true)
data class SalesResponse(
@Json(name = "sales") val sales: List<Sales>,
)
@ -17,6 +19,7 @@ data class SalesResponse(
* @property product product that is sales
* @property notification optional notification for product
*/
@JsonClass(generateAdapter = true)
data class Sales(
@Json(name = "id") val id: String,
@Json(name = "state") val state: String,
@ -31,12 +34,14 @@ data class Sales(
* @property code code that shows what product it is
* @property name product name
*/
@JsonClass(generateAdapter = true)
data class Product(
@Json(name = "id") val id: String,
@Json(name = "code") val code: String,
@Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class Notification(
@Json(name = "type") val type: String,
@Json(name = "title") val title: String,

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Shop response
@ -9,6 +10,7 @@ import com.squareup.moshi.Json
*
[REDACTED_AUTHOR]
*/
@JsonClass(generateAdapter = true)
data class ShopResponse(
@Json(name = "canOrder") val isOrderingAvailable: Boolean,
)

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/** Body for make user referral */
@JsonClass(generateAdapter = true)
data class StartReferralBody(
@Json(name = "walletId") val walletId: String,
@Json(name = "networkId") val networkId: String,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UpdateUserTokensAccountBody(
@Json(name = "accountTitle")
val title: String,

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UserTokensAccountResponse(
@Json(name = "accountId")
val id: Int,

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.extensions.calculateHashCode
@JsonClass(generateAdapter = true)
data class UserTokensResponse(
@Json(name = "version") val version: Int = 0,
@Json(name = "group") val group: GroupType,
@ -10,6 +12,7 @@ data class UserTokensResponse(
@Json(name = "tokens") val tokens: List<Token> = emptyList(),
) {
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String? = null,
@Json(name = "networkId") val networkId: String,
@ -36,6 +39,7 @@ data class UserTokensResponse(
)
}
@JsonClass(generateAdapter = false)
enum class GroupType {
@Json(name = "none")
NONE,
@ -47,6 +51,7 @@ data class UserTokensResponse(
NETWORK,
}
@JsonClass(generateAdapter = false)
enum class SortType {
@Json(name = "balance")
BALANCE,

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