Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-29 21:44:25 +03:00
commit bf0cb06cb3
297 changed files with 4771 additions and 2376 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.211.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 java.util.Properties
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins { plugins {
alias(deps.plugins.android.application) alias(deps.plugins.android.application)
@ -258,6 +259,10 @@ dependencies {
implementation(deps.decompose.ext.compose) implementation(deps.decompose.ext.compose)
implementation(deps.moshi.adapters) implementation(deps.moshi.adapters)
implementation(deps.moshi.kotlin)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
/** Testing libraries */ /** Testing libraries */
testImplementation(deps.test.coroutine) testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit) testImplementation(deps.test.junit)
@ -275,6 +280,7 @@ dependencies {
/** Chucker */ /** Chucker */
debugImplementation(deps.chucker) debugImplementation(deps.chucker)
debugPGImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub) mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub)
@ -296,5 +302,4 @@ dependencies {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava // excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group = "com.google.guava", module = "listenablefuture") 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. -dontusemixedcaseclassnames
# You can control the set of applied configuration files using the -flattenpackagehierarchy
# proguardFiles setting in build.gradle.kts. -adaptclassstrings
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following # firebase
# and specify the fully qualified class name to the JavaScript interface -keep public class com.google.firebase.** { *; }
# class: -keep class com.google.android.gms.internal.** { *; }
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -keepclasseswithmembers class com.google.firebase.FirebaseException
# public *;
#}
# Uncomment this to preserve the line number information for # hedera sdk
# debugging stack traces. -keep class com.hedera.hashgraph.sdk.** { *; }
#-keepattributes SourceFile,LineNumberTable -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 # trustwallet sdk
# hide the original source file name. -keep class wallet.core.jni.** { *; }
#-renamesourcefileattribute SourceFile
# 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 6f955d192f7edd876b9f096a30431adeb1594aeb

View file

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

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.onramp.* 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.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
@ -229,4 +230,10 @@ internal object OnrampDomainModule {
): FetchOnrampCountriesUseCase { ): FetchOnrampCountriesUseCase {
return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver) 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) return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
} }
@Provides
@Singleton
fun provideShouldShowRingPromoUseCase(promoRepository: PromoRepository): ShouldShowRingPromoUseCase {
return ShouldShowRingPromoUseCase(promoRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase { fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase {

View file

@ -26,7 +26,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge 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.domain.wallets.models.UserWalletId
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask 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.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.tap.derivationsFinder import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask 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.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask 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 // endregion
companion object { 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.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge 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.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.domain.sdk.mocks.MockProvider
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
@ -207,4 +209,12 @@ class MockTangemSdkManager(
): CompletionResult<VisaCardActivationResponse> { ): CompletionResult<VisaCardActivationResponse> {
error("Not implemented") 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 { private fun getWalletProductType(card: CardDTO): ProductType {
if (RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX)) { return when {
return ProductType.Ring card.isVisa -> ProductType.Visa
} RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX) -> ProductType.Ring
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed card.settings.isKeysImportAllowed -> ProductType.Wallet2
) { else -> ProductType.Wallet
ProductType.Wallet2
} else {
ProductType.Wallet
} }
} }

View file

@ -1,16 +1,20 @@
package com.tangem.tap.domain.tasks.visa package com.tangem.tap.domain.tasks.visa
import com.reown.util.hexToBytes
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toHexString
import com.tangem.common.map 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.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage 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.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.* import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository 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.SignHashCommand
import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import timber.log.Timber
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.jvm.Throws import kotlin.jvm.Throws
import kotlin.time.measureTimedValue
@Suppress("LongParameterList") @Suppress("LongParameterList")
class VisaCardActivationTask @AssistedInject constructor( class VisaCardActivationTask @AssistedInject constructor(
@ -36,9 +43,15 @@ class VisaCardActivationTask @AssistedInject constructor(
private val otpStorage: VisaOTPStorage, private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository, private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository, private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) : CardSessionRunnable<VisaCardActivationResponse> { ) : CardSessionRunnable<VisaCardActivationResponse> {
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val cardId: String,
val session: CardSession,
)
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) { override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
coroutineScope.launch { coroutineScope.launch {
callback(runSuspend(session)) callback(runSuspend(session))
@ -52,33 +65,47 @@ class VisaCardActivationTask @AssistedInject constructor(
return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message)) return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message))
} }
return if (challengeToSign != null) { val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
signAuthorizationChallenge(session, challengeToSign)
} else {
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
signOrder(session, activationOrder) val context = SessionContext(
} visaActivationRepository = visaActivationRepository,
} cardId = card.cardId,
session = session,
)
private suspend fun signAuthorizationChallenge( val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
session: CardSession, if (challengeToSign != null) {
challengeToSign: VisaAuthChallenge.Card, context.signAuthorizationChallenge(challengeToSign)
): CompletionResult<VisaCardActivationResponse> { } else {
val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes()) val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
val result = suspendCancellableCoroutine { continuation -> .getOrElse {
attestationCommand.run(session = session) { attestationResponse -> return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
continuation.resume(attestationResponse) }
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 -> { is CompletionResult.Success -> {
Timber.i("AttestCardKeyCommand success")
processSignedAuthorizationChallenge( processSignedAuthorizationChallenge(
session = session,
signedChallenge = challengeToSign.toSignedChallenge( signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(), signedChallenge = result.data.cardSignature.toHexString(),
salt = result.data.salt.toHexString(), salt = result.data.salt.toHexString(),
@ -86,13 +113,13 @@ class VisaCardActivationTask @AssistedInject constructor(
) )
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
Timber.e("AttestCardKeyCommand failure ${result.error}")
CompletionResult.Failure(result.error) CompletionResult.Failure(result.error)
} }
} }
} }
private suspend fun processSignedAuthorizationChallenge( private suspend fun SessionContext.processSignedAuthorizationChallenge(
session: CardSession,
signedChallenge: VisaAuthSignedChallenge, signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> { ): CompletionResult<VisaCardActivationResponse> {
return coroutineScope { return coroutineScope {
@ -107,12 +134,14 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await() otpTaskDeferred.await()
signOrder(session, order) signOrder(order)
} }
} }
@Throws(TangemSdkError::class) @Throws(TangemSdkError::class)
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder { private suspend fun SessionContext.getActivationOrderToSign(
signedChallenge: VisaAuthSignedChallenge,
): ActivationOrder {
val tokens = runCatching { val tokens = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge) visaAuthRepository.getAccessTokens(signedChallenge)
}.getOrElse { }.getOrElse {
@ -121,12 +150,12 @@ class VisaCardActivationTask @AssistedInject constructor(
) )
} }
visaAuthTokenStorage.store(tokens) visaAuthTokenStorage.store(cardId, tokens)
return visaActivationRepository.getActivationOrderToSign() return visaActivationRepository.getActivationOrderToSign()
} }
private suspend fun createWallet(session: CardSession): CompletionResult<Unit> { private suspend fun SessionContext.createWallet(session: CardSession): CompletionResult<Unit> {
coroutineScope { ensureActive() } coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
@ -135,122 +164,152 @@ class VisaCardActivationTask @AssistedInject constructor(
createOTP(session) createOTP(session)
} else { } else {
val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve) val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve)
val result = suspendCancellableCoroutine { continuation ->
createWalletTask.run(session) { createWalletResult -> val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
continuation.resume(createWalletResult) 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 -> { is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
createOTP(session) createOTP(session)
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
CompletionResult.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() } coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) return if (otpStorage.hasSavedOTP(cardId)) {
val otp = otpStorage.getOTP(card.cardId)
return if (otp != null) {
CompletionResult.Success(Unit) CompletionResult.Success(Unit)
} else { } else {
val otpCommand = GenerateOTPCommand() val otpCommand = GenerateOTPCommand()
val result = suspendCancellableCoroutine { continuation -> val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
otpCommand.run(session) { otpResult -> suspendCancellableCoroutine { continuation ->
continuation.resume(otpResult) otpCommand.run(session) { otpResult ->
continuation.resume(otpResult)
}
} }
} }
when (result) { Timber.i("GenerateOTPCommand time: ${timedResult.duration}")
when (val result = timedResult.value) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
otpStorage.saveOTP(card.cardId, result.data.rootOTP) Timber.i("GenerateOTPCommand success")
otpStorage.saveOTP(cardId, result.data.rootOTP)
CompletionResult.Success(Unit) CompletionResult.Success(Unit)
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
Timber.e("GenerateOTPCommand failure ${result.error}")
CompletionResult.Failure(result.error) CompletionResult.Failure(result.error)
} }
} }
} }
} }
private suspend fun signOrder( private suspend fun SessionContext.signOrder(order: ActivationOrder): CompletionResult<VisaCardActivationResponse> {
session: CardSession, val card =
order: ActivationOrder, session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = val wallet =
card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve }
val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey) ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { signResult -> val task = SignHashCommand(
continuation.resume(signResult) 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 -> { is CompletionResult.Success -> {
Timber.i("SignHashCommand success")
handleSignedOrder( handleSignedOrder(
session = session,
activationOrder = order, activationOrder = order,
response = result.data, response = result.data,
) )
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
Timber.e("SignHashCommand failure ${result.error}")
CompletionResult.Failure(result.error) CompletionResult.Failure(result.error)
} }
} }
} }
private suspend fun handleSignedOrder( private suspend fun SessionContext.handleSignedOrder(
session: CardSession,
activationOrder: ActivationOrder, activationOrder: ActivationOrder,
response: SignHashResponse, response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> { ): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val signedOrder = SignedActivationOrder( val signedOrder = SignedActivationOrder(
activationOrder = activationOrder, activationOrder = activationOrder,
signature = response.signature.toHexString(), 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), TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
) )
val activationResponse = VisaCardActivationResponse( return setupAccessCode().map {
signedActivationOrder = signedOrder, val card =
rootOTP = VisaRootOTP(otp.toHexString()), 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()) val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.isAccessCodeSet) { if (card.isAccessCodeSet) {
return CompletionResult.Success(Unit) return CompletionResult.Success(Unit)
} }
Timber.i("Setting access code")
val task = SetUserCodeCommand.changeAccessCode(accessCode) val task = SetUserCodeCommand.changeAccessCode(accessCode)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { setAccessCodeResult -> val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
continuation.resume(setAccessCodeResult) 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 -> { is CompletionResult.Success -> {
Timber.i("SetUserCodeCommand success")
CompletionResult.Success(Unit) CompletionResult.Success(Unit)
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
Timber.i("SetUserCodeCommand failure ${result.error}")
CompletionResult.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 package com.tangem.tap.domain.userWalletList.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
@ -7,16 +8,24 @@ import com.tangem.domain.wallets.models.UserWalletId
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class UserWalletSensitiveInformation( internal data class UserWalletSensitiveInformation(
@Json(name = "wallets")
val wallets: List<CardDTO.Wallet>, val wallets: List<CardDTO.Wallet>,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class UserWalletPublicInformation( internal data class UserWalletPublicInformation(
@Json(name = "name")
val name: String, val name: String,
@Json(name = "walletId")
val walletId: UserWalletId, val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String, val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>, val cardsInWallet: Set<String>,
@Json(name = "scanResponse")
val scanResponse: ScanResponse, val scanResponse: ScanResponse,
@Json(name = "isMultiCurrency")
val isMultiCurrency: Boolean, val isMultiCurrency: Boolean,
@Json(name = "hasBackupError")
val hasBackupError: Boolean = false, 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.card.CardWallet
import com.tangem.common.core.CardSession import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toHexString
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -24,10 +25,16 @@ import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor( internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository, private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository, private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthTokenStorage: VisaAuthTokenStorage,
) { ) {
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val cardId: String,
val session: CardSession,
)
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> { suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan") Timber.i("Attempting to handle Visa card scan")
@ -36,6 +43,14 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) 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 wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
val activationInput = val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet) VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
@ -43,13 +58,10 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(activationStatus) return CompletionResult.Success(activationStatus)
} }
return deriveKey(wallet, session) return context.deriveKey(wallet)
} }
private suspend fun deriveKey( private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult<VisaCardActivationStatus> {
wallet: CardWallet,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while first scan") Timber.e("Failed to create derivation path while first scan")
@ -64,17 +76,16 @@ internal class VisaCardScanHandler @Inject constructor(
continuation.resume(result) continuation.resume(result)
} }
} }
return handleDerivationResponse(derivationTaskResult, session) return handleDerivationResponse(derivationTaskResult)
} }
private suspend fun handleDerivationResponse( private suspend fun SessionContext.handleDerivationResponse(
result: CompletionResult<ExtendedPublicKey>, result: CompletionResult<ExtendedPublicKey>,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> { ): CompletionResult<VisaCardActivationStatus> {
return when (result) { return when (result) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
Timber.i("Start task for loading challenge for Visa wallet") Timber.i("Start task for loading challenge for Visa wallet")
handleWalletAuthorization(session) handleWalletAuthorization()
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
CompletionResult.Failure(result.error) 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") Timber.i("Started handling authorization using Visa wallet")
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while handling wallet authorization") 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 { val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization") Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure( return CompletionResult.Failure(
@ -108,7 +120,6 @@ internal class VisaCardScanHandler @Inject constructor(
} }
Timber.i("Requesting challenge for wallet authorization") Timber.i("Requesting challenge for wallet authorization")
// Will be changed later after backend implementation
val challengeResponse = runCatching { val challengeResponse = runCatching {
visaAuthRepository.getCustomerWalletAuthChallenge( visaAuthRepository.getCustomerWalletAuthChallenge(
cardId = card.cardId, cardId = card.cardId,
@ -122,14 +133,12 @@ internal class VisaCardScanHandler @Inject constructor(
publicKey = wallet.publicKey, publicKey = wallet.publicKey,
derivationPath = derivationPath, derivationPath = derivationPath,
nonce = challengeResponse.challenge, nonce = challengeResponse.challenge,
session = session,
) )
return when (signChallengeResult) { return when (signChallengeResult) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
Timber.i("Challenge signed with Wallet public key") Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens( handleWalletAuthorizationTokens(
session = session,
signedChallenge = challengeResponse signedChallenge = challengeResponse
.toSignedChallenge(signChallengeResult.data.signature.toHexString()), .toSignedChallenge(signChallengeResult.data.signature.toHexString()),
) )
@ -141,8 +150,7 @@ internal class VisaCardScanHandler @Inject constructor(
} }
} }
private suspend fun handleWalletAuthorizationTokens( private suspend fun SessionContext.handleWalletAuthorizationTokens(
session: CardSession,
signedChallenge: VisaAuthSignedChallenge, signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> { ): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching { val authorizationTokensResponse = runCatching {
@ -151,17 +159,20 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i( Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", "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") Timber.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) 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()) val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign") Timber.i("Requesting authorization challenge to sign")
@ -178,7 +189,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Received challenge to sign: ${challengeResponse.challenge}") 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) { val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> { 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() val activationRemoteState = visaActivationRepository.getActivationRemoteState()
@ -239,47 +253,31 @@ internal class VisaCardScanHandler @Inject constructor(
) )
} }
private suspend fun signChallengeWithWallet( private suspend fun SessionContext.signChallengeWithWallet(
publicKey: ByteArray, publicKey: ByteArray,
derivationPath: DerivationPath, derivationPath: DerivationPath,
nonce: String, nonce: String,
session: CardSession,
): CompletionResult<SignHashResponse> { ): CompletionResult<SignHashResponse> {
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath) val signHashCommand = SignHashCommand(
val result = suspendCancellableCoroutine { hash = nonce.hexToBytes(),
walletPublicKey = publicKey,
derivationPath = derivationPath,
)
return suspendCancellableCoroutine {
signHashCommand.run(session) { result -> signHashCommand.run(session) { result ->
it.resume(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( private suspend fun SessionContext.signChallengeWithCard(
session: CardSession,
challenge: String, challenge: String,
): CompletionResult<AttestCardKeyResponse> { ): CompletionResult<AttestCardKeyResponse> {
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray()) val signHashCommand = AttestCardKeyCommand(challenge = challenge.hexToBytes())
val result = suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
signHashCommand.run(session) { result -> signHashCommand.run(session) { result ->
continuation.resume(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") @Json(name = "type")
val type: WCSignType, val type: WCSignType,
) : WcRequestData { ) : WcRequestData {
@JsonClass(generateAdapter = false)
enum class WCSignType { enum class WCSignType {
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE, MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE,
} }

View file

@ -1,3 +1,16 @@
package com.tangem.tap.domain.walletconnect2.domain.models 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 package com.tangem.tap.domain.walletconnect2.domain.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Session( data class Session(
@Json(name = "topic")
val topic: String, val topic: String,
@Json(name = "accounts")
val accounts: List<Account>, 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.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList") @Suppress("LongParameterList")
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class WcBinanceCancelOrder( data class WcBinanceCancelOrder(
@Json(name = "account_number") @Json(name = "account_number")
accountNumber: String, val accountNumber: String,
@Json(name = "chain_id") @Json(name = "chain_id")
chainId: String, val chainId: String,
@Json(name = "data") @Json(name = "data")
data: String?, val data: String?,
@Json(name = "memo") @Json(name = "memo")
memo: String?, val memo: String?,
@Json(name = "sequence") @Json(name = "sequence")
sequence: String, val sequence: String,
@Json(name = "source") @Json(name = "source")
source: String, val source: String,
@Json(name = "msgs") @Json(name = "msgs")
msgs: List<Message>, val msgs: List<Message>,
) : WcBinanceOrder<WcBinanceCancelOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) { ) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Message( data class Message(
@Json(name = "refid")
val refid: String, val refid: String,
@Json(name = "sender")
val sender: String, val sender: String,
@Json(name = "symbol")
val symbol: String, 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.google.gson.JsonObject
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList") @Suppress("LongParameterList")
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class WcBinanceTradeOrder( data class WcBinanceTradeOrder(
@Json(name = "account_number") @Json(name = "account_number")
accountNumber: String, val accountNumber: String,
@Json(name = "chain_id") @Json(name = "chain_id")
chainId: String, val chainId: String,
data: String?, @Json(name = "data")
memo: String?, val data: String?,
sequence: String, @Json(name = "memo")
source: String, val memo: String?,
msgs: List<Message>, @Json(name = "sequence")
) : WcBinanceOrder<WcBinanceTradeOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) { 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) { enum class MessageKey(val key: String) {
ID("id"), ID("id"),
ORDER_TYPE("ordertype"), ORDER_TYPE("ordertype"),
@ -30,14 +37,23 @@ class WcBinanceTradeOrder(
TIME_INFORCE("timeinforce"), TIME_INFORCE("timeinforce"),
} }
@JsonClass(generateAdapter = true)
data class Message( data class Message(
@Json(name = "id")
val id: String, val id: String,
@Json(name = "orderType")
val orderType: Int, val orderType: Int,
@Json(name = "price")
val price: Long, val price: Long,
@Json(name = "quantity")
val quantity: Long, val quantity: Long,
@Json(name = "sender")
val sender: String, val sender: String,
@Json(name = "side")
val side: Int, val side: Int,
@Json(name = "symbol")
val symbol: String, val symbol: String,
@Json(name = "timeInforce")
val timeInforce: Int, 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.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList") @Suppress("LongParameterList")
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class WcBinanceTransferOrder( data class WcBinanceTransferOrder(
@Json(name = "account_number") @Json(name = "account_number")
accountNumber: String, val accountNumber: String,
@Json(name = "chain_id") @Json(name = "chain_id")
chainId: String, val chainId: String,
data: String?, @Json(name = "data")
memo: String?, val data: String?,
sequence: String, @Json(name = "memo")
source: String, val memo: String?,
msgs: List<Message>, @Json(name = "sequence")
) : WcBinanceOrder<WcBinanceTransferOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) { val sequence: String,
@Json(name = "source")
val source: String,
@Json(name = "msgs")
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Message( data class Message(
@Json(name = "inputs")
val inputs: List<Item>, val inputs: List<Item>,
@Json(name = "outputs")
val outputs: List<Item>, val outputs: List<Item>,
) { ) {
@JsonClass(generateAdapter = true)
data class Item( data class Item(
@Json(name = "address")
val address: String, val address: String,
@Json(name = "coins")
val coins: List<Coin>, val coins: List<Coin>,
) { ) {
@JsonClass(generateAdapter = true)
data class Coin( data class Coin(
@Json(name = "amount")
val amount: Long, val amount: Long,
@Json(name = "denom")
val denom: String, 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,7 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.material.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -27,13 +27,12 @@ internal fun SettingsScreensScaffold(
addBottomInsets: Boolean = true, addBottomInsets: Boolean = true,
fab: @Composable () -> Unit = {}, fab: @Composable () -> Unit = {},
) { ) {
val state = rememberScaffoldState(snackbarHostState = snackbarHostState)
val backgroundColor = TangemTheme.colors.background.secondary val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick) BackHandler(onBack = onBackClick)
Scaffold( Scaffold(
scaffoldState = state, snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { topBar = {
EmptyTopBarWithNavigation( EmptyTopBarWithNavigation(
modifier = Modifier.statusBarsPadding(), modifier = Modifier.statusBarsPadding(),
@ -43,7 +42,7 @@ internal fun SettingsScreensScaffold(
}, },
modifier = modifier, modifier = modifier,
contentWindowInsets = WindowInsetsZero, contentWindowInsets = WindowInsetsZero,
backgroundColor = backgroundColor, containerColor = backgroundColor,
floatingActionButton = { floatingActionButton = {
Box(modifier = Modifier.navigationBarsPadding()) { Box(modifier = Modifier.navigationBarsPadding()) {
fab() fab()
@ -89,6 +88,7 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun EmptyTopBarWithNavigation( internal fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit, onBackClick: () -> Unit,
@ -108,8 +108,9 @@ internal fun EmptyTopBarWithNavigation(
) )
} }
}, },
backgroundColor = backgroundColor, colors = TopAppBarDefaults.topAppBarColors(
elevation = 0.dp, containerColor = backgroundColor,
),
) )
} }

View file

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

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

View file

@ -4,8 +4,8 @@ import android.content.DialogInterface
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier

View file

@ -1,14 +1,20 @@
package com.tangem.tap.network.auth package com.tangem.tap.network.auth
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider 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 import javax.inject.Inject
internal class DefaultVisaAuthProvider @Inject constructor( internal class DefaultVisaAuthProvider @Inject constructor(
private val authStorage: VisaAuthTokenStorage, private val userWalletsListManager: UserWalletsListManager,
) : TangemVisaAuthProvider { ) : TangemVisaAuthProvider {
override suspend fun getAuthHeader(): String { override suspend fun getAuthHeader(cardId: String): String {
return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!" 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 package com.tangem.tap.network.exchangeServices.mercuryo
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
@ -11,21 +12,25 @@ interface MercuryoApi {
suspend fun currencies(@Path("apiVersion") apiVersion: String): MercuryoCurrenciesResponse suspend fun currencies(@Path("apiVersion") apiVersion: String): MercuryoCurrenciesResponse
} }
@JsonClass(generateAdapter = true)
data class MercuryoCurrenciesResponse( data class MercuryoCurrenciesResponse(
val status: Int, @Json(name = "status") val status: Int,
val data: Data, @Json(name = "data") val data: Data,
) { ) {
@JsonClass(generateAdapter = true)
data class Data( data class Data(
val fiat: List<String>, @Json(name = "fiat") val fiat: List<String>,
val crypto: List<String>, @Json(name = "crypto") val crypto: List<String>,
val config: Config, @Json(name = "config") val config: Config,
) )
@JsonClass(generateAdapter = true)
data class Config( data class Config(
@Json(name = "crypto_currencies") @Json(name = "crypto_currencies")
val cryptoCurrencies: List<MercuryoCryptoCurrency>, val cryptoCurrencies: List<MercuryoCryptoCurrency>,
) )
@JsonClass(generateAdapter = true)
data class MercuryoCryptoCurrency( data class MercuryoCryptoCurrency(
@Json(name = "currency") @Json(name = "currency")
val currencySymbol: String, val currencySymbol: String,

View file

@ -149,5 +149,7 @@ internal val Blockchain.mercuryoNetwork: String?
Blockchain.Clore -> null Blockchain.Clore -> null
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> null Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> null
Blockchain.Bitrock, Blockchain.BitrockTestnet -> 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) @JsonClass(generateAdapter = true)
data class MoonPayUserStatus( data class MoonPayUserStatus(
@Json(name = "isBuyAllowed")
val isBuyAllowed: Boolean, val isBuyAllowed: Boolean,
@Json(name = "isSellAllowed")
val isSellAllowed: Boolean, val isSellAllowed: Boolean,
@Json(name = "isAllowed") @Json(name = "isAllowed")
val isMoonpayAllowed: Boolean, val isMoonpayAllowed: Boolean,
@ -34,18 +36,18 @@ data class MoonPayUserStatus(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MoonPayCurrencies( data class MoonPayCurrencies(
val type: String, @Json(name = "type") val type: String,
val code: String, @Json(name = "code") val code: String,
val supportsLiveMode: Boolean = false, @Json(name = "supportsLiveMode") val supportsLiveMode: Boolean = false,
val isSuspended: Boolean = true, @Json(name = "isSuspended") val isSuspended: Boolean = true,
val isSupportedInUS: Boolean = false, @Json(name = "isSupportedInUS") val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false, @Json(name = "isSellSupported") val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(), @Json(name = "notAllowedUSStates") val notAllowedUSStates: List<String> = emptyList(),
val metadata: MoonPayCurrenciesMetadata? = null, @Json(name = "metadata") val metadata: MoonPayCurrenciesMetadata? = null,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MoonPayCurrenciesMetadata( data class MoonPayCurrenciesMetadata(
val contractAddress: String?, @Json(name = "contractAddress") val contractAddress: String?,
val networkCode: String?, @Json(name = "networkCode") val networkCode: String?,
) )

View file

@ -150,4 +150,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
VanarChainTestnet -> null VanarChainTestnet -> null
OdysseyChain, OdysseyChainTestnet -> null OdysseyChain, OdysseyChainTestnet -> null
Bitrock, BitrockTestnet -> 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" android:id="@+id/pb_state"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="20dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:progressBackgroundTint="@color/icon_informative" android:paddingVertical="8dp"
android:progressTint="@color/icon_primary_1" /> android:indeterminate="false"
android:progressDrawable="@drawable/progress_rounded_shape" />
</merge> </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.cartesian.marker.DefaultCartesianMarker
import com.patrykandpatrick.vico.core.common.Dimensions import com.patrykandpatrick.vico.core.common.Dimensions
import com.patrykandpatrick.vico.core.common.LayeredComponent 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.component.TextComponent
import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.core.common.shape.Shape
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -39,7 +38,28 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
object : DefaultCartesianMarker( object : DefaultCartesianMarker(
label = TextComponent(textSizeSp = 0f), 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, indicatorSizeDp = INDICATOR_SIZE_DP,
guideline = guideline, guideline = guideline,
valueFormatter = object : CartesianMarkerValueFormatter { 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 val indicatorPadding = Dimensions.of(3.dp)
private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f
private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f

View file

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

View file

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

View file

@ -21,11 +21,11 @@
}, },
{ {
"name": "ONBOARDING_CODE_REFACTORING_ENABLED", "name": "ONBOARDING_CODE_REFACTORING_ENABLED",
"version": "undefined" "version": "5.21.0"
}, },
{ {
"name": "NAVIGATION_REFACTORING", "name": "NAVIGATION_REFACTORING",
"version": "undefined" "version": "5.21.0"
}, },
{ {
"name": "SWAP_STORIES_ENABLED", "name": "SWAP_STORIES_ENABLED",
@ -34,5 +34,9 @@
{ {
"name": "VISA_ONBOARDING_ENABLED", "name": "VISA_ONBOARDING_ENABLED",
"version": "undefined" "version": "undefined"
},
{
"name": "ONRAMP_HOT_TOKENS_ENABLED",
"version": "undefined"
} }
] ]

View file

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

View file

@ -40,6 +40,7 @@ sealed class ApiConfig {
companion object { companion object {
internal const val DEBUG_BUILD_TYPE = "debug" 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 INTERNAL_BUILD_TYPE = "internal"
internal const val MOCKED_BUILD_TYPE = "mocked" internal const val MOCKED_BUILD_TYPE = "mocked"
internal const val EXTERNAL_BUILD_TYPE = "external" internal const val EXTERNAL_BUILD_TYPE = "external"

View file

@ -1,10 +1,21 @@
package com.tangem.datasource.api.common.config package com.tangem.datasource.api.common.config
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/** /**
* Api environment * Api environment
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@JsonClass(generateAdapter = false)
enum class ApiEnvironment { 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 { fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) { return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE -> ApiEnvironment.DEV DEBUG_BUILD_TYPE,
DEBUG_PG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE, INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE, MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE -> ApiEnvironment.STAGE

View file

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

View file

@ -2,5 +2,5 @@ package com.tangem.datasource.api.common.visa
interface TangemVisaAuthProvider { 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 package com.tangem.datasource.api.express.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AssetsRequestBody( data class AssetsRequestBody(
@Json(name = "tokensList") val tokensList: List<LeastTokenInfo>?, @Json(name = "tokensList") val tokensList: List<LeastTokenInfo>?,
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,13 @@
package com.tangem.datasource.api.stakekit.models.request 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) { enum class RevenueOption(val value: String) {
SUPPORTS_FEE("supportsFee"), @Json(name = "SUPPORTS_FEE") SUPPORTS_FEE("supportsFee"),
SUPPORTS_REV_SHARE("supportsRevShare"),
@Json(name = "SUPPORTS_REV_SHARE") SUPPORTS_REV_SHARE("supportsRevShare"),
; ;
override fun toString(): String { override fun toString(): String {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FeaturesResponse( data class FeaturesResponse(
@Json(name = "send") val isNewSendEnabled: Boolean, @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 package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class QuotesResponse( data class QuotesResponse(
@Json(name = "quotes") @Json(name = "quotes")
val quotes: Map<String, Quote>, val quotes: Map<String, Quote>,
) { ) {
@JsonClass(generateAdapter = true)
data class Quote( data class Quote(
@Json(name = "price") @Json(name = "price")
val price: BigDecimal?, val price: BigDecimal?,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UserTokensSettingsResponse( data class UserTokensSettingsResponse(
@Json(name = "group") @Json(name = "group")
val group: UserTokensResponse.GroupType, val group: UserTokensResponse.GroupType,

View file

@ -1,13 +1,16 @@
package com.tangem.datasource.api.tangemTech.models.v2 package com.tangem.datasource.api.tangemTech.models.v2
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@JsonClass(generateAdapter = true)
data class UserTokensResponseV2( data class UserTokensResponseV2(
@Json(name = "accounts") @Json(name = "accounts")
val accounts: List<TokensAccount>, val accounts: List<TokensAccount>,
) { ) {
@JsonClass(generateAdapter = true)
data class TokensAccount( data class TokensAccount(
@Json(name = "id") @Json(name = "id")
val id: Int, val id: Int,

View file

@ -1,10 +1,21 @@
package com.tangem.datasource.api.visa package com.tangem.datasource.api.visa
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import java.util.concurrent.TimeUnit
interface TangemVisaApi { interface TangemVisaApi {
@GET("activation-status") @GET("activation-status")
suspend fun getRemoteActivationStatus(): CardActivationRemoteStateResponse suspend fun getRemoteActivationStatus(
@Header("Authorization") authHeader: String,
): CardActivationRemoteStateResponse
@ReadTimeout(duration = 20, TimeUnit.MINUTES)
@GET("activation-status")
suspend fun getRemoteActivationStatusLongPoll(
@Header("Authorization") authHeader: String,
): CardActivationRemoteStateResponse
} }

View file

@ -5,7 +5,6 @@ import com.tangem.datasource.api.common.config.*
import com.tangem.datasource.api.common.config.Express import com.tangem.datasource.api.common.config.Express
import com.tangem.datasource.api.common.config.StakeKit import com.tangem.datasource.api.common.config.StakeKit
import com.tangem.datasource.api.common.config.TangemTech import com.tangem.datasource.api.common.config.TangemTech
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider
@ -47,6 +46,5 @@ internal object ApiConfigsModule {
@Provides @Provides
@IntoSet @IntoSet
fun provideTangemVisaConfig(tangemVisaAuthProvider: TangemVisaAuthProvider): ApiConfig = fun provideTangemVisaConfig(): ApiConfig = TangemVisa()
TangemVisa(tangemVisaAuthProvider)
} }

View file

@ -194,7 +194,7 @@ internal object NetworkModule {
clientBuilder = { clientBuilder = {
addInterceptor( addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore), NetworkLogsSaveInterceptor(appLogsStore),
) ).applyTimeoutAnnotations()
}, },
) )
} }
@ -215,7 +215,7 @@ internal object NetworkModule {
clientBuilder = { clientBuilder = {
addInterceptor( addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore), NetworkLogsSaveInterceptor(appLogsStore),
) ).applyTimeoutAnnotations()
}, },
) )
} }

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.hotcrypto.DefaultHotCryptoLoader
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface HotCryptoLoaderModule {
@Binds
@Singleton
fun bindExpressServiceLoader(defaultHotCryptoLoader: DefaultHotCryptoLoader): HotCryptoLoader
}

View file

@ -0,0 +1,81 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import javax.inject.Inject
/**
* Default implementation of [HotCryptoLoader]
*
* @property tangemTechApi tangem tech api
* @property hotCryptoResponseStore store of [HotCryptoResponse]
* @property appPreferencesStore app preferences store
* @property dispatchers dispatchers
* @property onrampFeatureToggles onramp feature toggles
*
[REDACTED_AUTHOR]
*/
internal class DefaultHotCryptoLoader @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val hotCryptoResponseStore: HotCryptoResponseStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
) : HotCryptoLoader {
override suspend fun fetch(tokens: List<UserTokensResponse.Token>) {
updateInternal { hotToken ->
tokens.none {
it.id == hotToken.id && it.contractAddress == hotToken.contractAddress &&
it.networkId == hotToken.networkId
}
}
}
override suspend fun update(currencies: List<CryptoCurrency>) {
updateInternal { hotToken ->
currencies.none {
it.id.rawCurrencyId == hotToken.id &&
(it as? CryptoCurrency.Token)?.contractAddress == hotToken.contractAddress &&
it.network.id.value == hotToken.networkId
}
}
}
private suspend fun updateInternal(filterPredicate: (HotCryptoResponse.Token) -> Boolean) {
if (!onrampFeatureToggles.isHotTokensEnabled) return
runCatching(dispatchers.io) {
tangemTechApi.getHotCrypto(currencyId = getAppCurrencyId()).getOrThrow()
}
.onSuccess {
Timber.d("HotCrypto is successfully updated")
hotCryptoResponseStore.store(
value = it.copy(tokens = it.tokens.filter(filterPredicate)),
)
}
.onFailure {
Timber.e(it, "Unable to fetch hot crypto")
}
}
private suspend fun getAppCurrencyId(): String {
return appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
?.id
?: "usd"
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Hot crypto loader
*
[REDACTED_AUTHOR]
*/
interface HotCryptoLoader {
/**
* Fetch hot crypto
*
* @param tokens already added tokens
*/
suspend fun fetch(tokens: List<UserTokensResponse.Token>)
/**
* Update hot crypto
*
* @param currencies already added crypto currencies
*/
suspend fun update(currencies: List<CryptoCurrency>)
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.exchangeservice.hotcrypto
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.datasource.local.datastore.RuntimeStateStore
import javax.inject.Inject
import javax.inject.Singleton
/**
* Store of [HotCryptoResponse]
*
[REDACTED_AUTHOR]
*/
@Singleton
class HotCryptoResponseStore @Inject constructor() : RuntimeStateStore<HotCryptoResponse> by RuntimeStateStore(
defaultValue = HotCryptoResponse(imageHost = null, tokens = emptyList()),
)

View file

@ -1,8 +1,16 @@
package com.tangem.datasource.local.card package com.tangem.datasource.local.card
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UsedCardInfo( data class UsedCardInfo(
@Json(name = "cardId")
val cardId: String, val cardId: String,
@Json(name = "isScanned")
val isScanned: Boolean = false, val isScanned: Boolean = false,
@Json(name = "isActivationStarted")
val isActivationStarted: Boolean = false, val isActivationStarted: Boolean = false,
@Json(name = "isActivationFinished")
val isActivationFinished: Boolean = false, val isActivationFinished: Boolean = false,
) )

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