Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-24 11:42:34 +03:00
commit c8024b166d
656 changed files with 18270 additions and 4656 deletions

View file

@ -109,6 +109,8 @@ dependencies {
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.common)
implementation(projects.common.routing)
@ -152,6 +154,7 @@ dependencies {
implementation(projects.data.nft)
implementation(projects.data.onramp)
implementation(projects.data.networks)
implementation(projects.data.quotes)
/** Features */
implementation(projects.features.onboarding)
@ -273,6 +276,7 @@ dependencies {
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.coil)
implementation(deps.coil.gif)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
@ -318,6 +322,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

@ -1 +1 @@
Subproject commit 29f73d909e14cb3760118b197b1929a6088bc40e
Subproject commit 7a607593e34f0977474613f077532215eaf0ffe6

View file

@ -1,6 +1,9 @@
package com.tangem.tap
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil.ImageLoader
@ -71,9 +74,7 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
import org.rekotlin.Store
import kotlin.collections.set
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@ -228,12 +229,32 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
// endregion
private val appScope = MainScope()
override fun onCreate() {
enableStrictModeInDebug()
super.onCreate()
init()
}
updateLogFiles()
private fun enableStrictModeInDebug() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build(),
)
}
}
private fun updateLogFiles() {
@ -260,18 +281,31 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) },
async {
featureTogglesManager.init()
},
async {
excludedBlockchainsManager.init()
},
)
}
loadNativeLibraries()
appScope.launch {
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
launch(Dispatchers.IO) {
loadNativeLibraries()
walletConnect2Repository.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
)
updateLogFiles()
}
}
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
@ -287,9 +321,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
appStateHolder.mainStore = store
walletConnect2Repository.init(projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId)
appStateHolder.mainStore = store
}
private fun createReduxStore(): Store<AppState> {

View file

@ -1,8 +1,11 @@
package com.tangem.tap.common.images
import android.content.Context
import android.os.Build
import android.util.Log
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.util.Logger
@ -25,6 +28,15 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
.build()
}
}
.components {
// According to Coil ImageDecoder API is faster and supports animated WebP and HEIF
// https://coil-kt.github.io/coil/gifs/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.memoryCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder(context)

View file

@ -20,18 +20,22 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaAuthTokenStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
)
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
)
}
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)

View file

@ -20,12 +20,14 @@ class DefaultVisaOTPStorage @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaOTPStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
}
override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) {
secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId)

View file

@ -24,7 +24,7 @@ internal class RuntimeUserWalletsStore(
return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key }
}
override suspend fun getSyncStrict(key: UserWalletId): UserWallet {
override fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}

View file

@ -5,6 +5,7 @@ import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -73,6 +74,7 @@ internal object ManageTokensDomainModule {
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
@ -84,6 +86,7 @@ internal object ManageTokensDomainModule {
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -5,6 +5,8 @@ import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -49,8 +51,16 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository)
fun provideGetTokenQuotesUseCase(
quotesRepository: QuotesRepository,
singleQuoteSupplier: SingleQuoteSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(
quotesRepository = quotesRepository,
singleQuoteSupplier = singleQuoteSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@Provides
@ -63,6 +73,7 @@ object MarketsDomainModule {
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
@ -73,6 +84,7 @@ object MarketsDomainModule {
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -35,6 +35,16 @@ internal object NFTDomainModule {
nftRepository = nftRepository,
)
@Provides
@Singleton
fun providesRefreshAllUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
): RefreshAllNFTUseCase = RefreshAllNFTUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
)
@Provides
@Singleton
fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase =
@ -47,7 +57,7 @@ internal object NFTDomainModule {
fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
): GetNFTAvailableNetworksUseCase = GetNFTAvailableNetworksUseCase(
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
)
@ -66,4 +76,11 @@ internal object NFTDomainModule {
GetNFTNetworkStatusUseCase(
networksRepository = networksRepository,
)
@Provides
@Singleton
fun providesGetNFTExploreUrlUseCase(nftRepository: NFTRepository): GetNFTExploreUrlUseCase =
GetNFTExploreUrlUseCase(
nftRepository = nftRepository,
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.di.domain
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NotificationsDomainModule {
@Provides
@Singleton
fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase =
GetApplicationIdUseCase(
notificationsRepository = notificationsRepository,
)
}

View file

@ -7,7 +7,11 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
@ -35,6 +39,7 @@ internal object TokensDomainModule {
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
@ -43,6 +48,7 @@ internal object TokensDomainModule {
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -55,6 +61,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
@ -63,6 +70,7 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -168,6 +176,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
@ -176,6 +185,7 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -188,6 +198,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
@ -196,6 +207,7 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -364,10 +376,14 @@ internal object TokensDomainModule {
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -386,20 +402,28 @@ internal object TokensDomainModule {
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleQuoteSupplier: SingleQuoteSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleQuoteSupplier = singleQuoteSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -410,21 +434,35 @@ internal object TokensDomainModule {
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleQuoteSupplier: SingleQuoteSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleQuoteSupplier = singleQuoteSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@Provides
@Singleton
fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository)
}
}

View file

@ -30,6 +30,21 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideGetEthSpecificFeeUseCase(walletManagersFacade: WalletManagersFacade): GetEthSpecificFeeUseCase {
return GetEthSpecificFeeUseCase(walletManagersFacade = walletManagersFacade)
}
@Provides
@Singleton
fun provideTransferGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetTransferFeeUseCase {
return GetTransferFeeUseCase(
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(),
)
}
@Provides
@Singleton
fun provideSendTransactionUseCase(
@ -147,4 +162,30 @@ internal object TransactionDomainModule {
fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase {
return GetAllowanceUseCase(transactionRepository)
}
@Provides
@Singleton
fun provideCreateTransferTransactionUseCase(
transactionRepository: TransactionRepository,
): CreateTransferTransactionUseCase {
return CreateTransferTransactionUseCase(transactionRepository)
}
@Provides
@Singleton
fun providePrepareForSendUseCase(
transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository,
): PrepareForSendUseCase {
return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideSignUseCase(
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
): SignUseCase {
return SignUseCase(cardSdkConfigRepository, walletManagersFacade)
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,4 +21,10 @@ internal object WalletConnectDomainModule {
): CheckIsWalletConnectAvailableUseCase {
return CheckIsWalletConnectAvailableUseCase(walletConnectRepository = walletConnectRepository)
}
@Provides
@Singleton
fun providesWcSessionsUseCase(sessionsManager: WcSessionsManager): WcSessionsUseCase {
return WcSessionsUseCase(sessionsManager)
}
}

View file

@ -14,6 +14,8 @@ import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSy
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -172,8 +174,16 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase {
return GetCardImageUseCase(verifier = onlineCardVerifier)
fun provideGetCardImageUseCase(
onlineCardVerifier: OnlineCardVerifier,
cardArtworksProvider: CardArtworksProvider,
cardSdkFeatureToggles: CardSdkFeatureToggles,
): GetCardImageUseCase {
return GetCardImageUseCase(
verifier = onlineCardVerifier,
cardArtworksProvider = cardArtworksProvider,
cardSdkFeatureToggles = cardSdkFeatureToggles,
)
}
@Provides

View file

@ -6,6 +6,7 @@ import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
@ -37,7 +38,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(
if (artworkId.isNullOrEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}

View file

@ -4,14 +4,12 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.*
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
@ -46,7 +44,7 @@ import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.measureTimedValue
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val mode: VisaCardActivationTaskMode,
@Assisted private val activationInput: VisaActivationInput,
@ -97,7 +95,23 @@ class VisaCardActivationTask @AssistedInject constructor(
context.signAuthorizationChallenge(mode.authorizationChallenge)
}
is VisaCardActivationTaskMode.SignOnly -> {
context.signData(mode.dataToSignByCardWallet)
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
context.signData(
mode.dataToSignByCardWallet,
derivedPublicKey,
)
}
}
}
@ -138,9 +152,35 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.processSignedAuthorizationChallenge(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
when (val createWalletResult = createWallet()) {
is CompletionResult.Failure -> return CompletionResult.Failure(createWalletResult.error)
is CompletionResult.Success -> {}
}
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
return coroutineScope {
val dataToSignDeferred = async { getDataToSign(signedChallenge) }
val otpTaskDeferred = async { createWallet() }
val dataToSignDeferred =
async { getDataToSign(signedChallenge = signedChallenge, cardWalletAddress = walletAddress) }
val otpTaskDeferred = async { createOTP() }
val dataToSign = dataToSignDeferred.await()
.getOrElse {
@ -150,12 +190,16 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await()
signData(dataToSign)
signData(
dataToSign = dataToSign,
derivedPublicKey = derivedPublicKey,
)
}
}
private suspend fun SessionContext.getDataToSign(
signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either {
catch(
block = {
@ -168,7 +212,12 @@ class VisaCardActivationTask @AssistedInject constructor(
raise(VisaActivationError.WrongRemoteState.tangemError)
}
visaActivationRepository.getCardWalletAcceptanceData(remoteState.request)
visaActivationRepository.getCardWalletAcceptanceData(
VisaCardWalletDataToSignRequest(
activationOrderInfo = remoteState.activationOrderInfo,
cardWalletAddress = cardWalletAddress,
),
)
},
catch = {
raise(VisaAuthorizationAPIError.tangemError)
@ -182,7 +231,7 @@ class VisaCardActivationTask @AssistedInject constructor(
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
return if (card.wallets.any { it.curve == EllipticCurve.Secp256k1 }) {
createOTP()
CompletionResult.Success(Unit)
} else {
val createWalletTask = CreateWalletTask(EllipticCurve.Secp256k1)
@ -199,7 +248,7 @@ class VisaCardActivationTask @AssistedInject constructor(
when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
createOTP()
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
@ -245,26 +294,15 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.signData(
dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey,
): CompletionResult<VisaCardActivationResponse> {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
val task = SignHashCommand(
hash = dataToSign.hashToSign.hexToBytes(),
walletPublicKey = wallet.publicKey,
@ -286,8 +324,8 @@ class VisaCardActivationTask @AssistedInject constructor(
Timber.i("SignHashCommand success")
handleSignedData(
dataToSign = dataToSign,
walletAddress = walletAddress,
response = result.data,
derivedPublicKey = derivedPublicKey,
)
}
is CompletionResult.Failure -> {
@ -313,7 +351,7 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.handleSignedData(
dataToSign: VisaDataToSignByCardWallet,
walletAddress: String,
derivedPublicKey: ExtendedPublicKey,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val otp = otpStorage.getOTP(cardId) ?: run {
@ -321,11 +359,16 @@ class VisaCardActivationTask @AssistedInject constructor(
otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError)
}
val rsvSignature = EthereumUtils.prepareSignedMessageData(
signedHash = response.signature,
hashToSign = dataToSign.hashToSign.hexToBytes(),
publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(),
)
val signedActivationData = dataToSign.sign(
cardWalletAddress = walletAddress,
rootOTP = otp.rootOTP.toHexString(),
otpCounter = otp.counter,
signature = response.signature.toHexString(),
signature = rsvSignature,
)
return setupAccessCode().map {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
@ -10,7 +11,7 @@ import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -129,6 +130,7 @@ class VisaCustomerWalletApproveTask(
targetWalletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
session = session,
extendedPublicKey = extendedPublicKey,
callback = callback,
)
}
@ -149,6 +151,7 @@ class VisaCustomerWalletApproveTask(
signApproveData(
targetWalletPublicKey = publicKey,
derivationPath = null,
extendedPublicKey = null,
session = session,
callback = callback,
)
@ -157,11 +160,14 @@ class VisaCustomerWalletApproveTask(
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
extendedPublicKey: ExtendedPublicKey?,
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
val signTask = SignHashCommand(
hash = visaDataForApprove.dataToSign.hashToSign.hexToBytes(),
hash = hashToSign,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
@ -169,11 +175,18 @@ class VisaCustomerWalletApproveTask(
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val rsvSignature = EthereumUtils.prepareSignedMessageData(
signedHash = result.data.signature,
hashToSign = hashToSign,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
)
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.dataToSign.sign(
signature = result.data.signature.toHexString(),
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
)

View file

@ -9,4 +9,10 @@ internal class DefaultTokensFeatureToggles(
override val isNetworksLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED")
override val isQuotesLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED")
override val isStakingLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_LOADING_REFACTORING_ENABLED")
}

View file

@ -21,8 +21,6 @@ internal data class UserWalletPublicInformation(
val name: String,
@Json(name = "walletId")
val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>,
@Json(name = "scanResponse")

View file

@ -20,9 +20,12 @@ internal class DefaultUserWalletsPublicInformationRepository(
moshi: Moshi,
private val secureStorage: SecureStorage,
) : UserWalletsPublicInformationRepository {
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> by lazy {
moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {

View file

@ -24,12 +24,15 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
private val secureStorage: SecureStorage,
) : UserWalletsSensitiveInformationRepository {
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> = moshi.adapter(
UserWalletSensitiveInformation::class.java,
)
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> = moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java),
)
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> by lazy {
moshi.adapter(UserWalletSensitiveInformation::class.java)
}
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> by lazy {
moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java),
)
}
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
if (encryptionKey == null) {

View file

@ -15,7 +15,6 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
get() = UserWalletPublicInformation(
name = name,
walletId = walletId,
artworkUrl = artworkUrl,
cardsInWallet = cardsInWallet,
isMultiCurrency = isMultiCurrency,
scanResponse = scanResponse.copy(
@ -31,7 +30,6 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
return UserWallet(
name = name,
walletId = walletId,
artworkUrl = artworkUrl,
cardsInWallet = cardsInWallet,
scanResponse = scanResponse,
isMultiCurrency = isMultiCurrency,

View file

@ -128,6 +128,8 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = runCatching {
// TODO [REDACTED_TASK_KEY]
error("sign and get specific error to switch to card_id flow")
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
}.getOrElse {
Timber.i(
@ -180,6 +182,7 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
@Suppress("LongMethod")
private suspend fun SessionContext.handleCardAuthorization(
cardWalletAddress: String,
): CompletionResult<VisaCardActivationStatus> {
@ -232,11 +235,17 @@ internal class VisaCardScanHandler @Inject constructor(
tokens = authorizationTokensResponse,
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
val activationRemoteState = runCatching {
visaActivationRepository.getActivationRemoteState()
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
}
val error = when (activationRemoteState) {
VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation
VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState
VisaActivationRemoteState.Failed -> VisaActivationError.FailedRemoteState
else -> null
}

View file

@ -4,8 +4,8 @@ import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.*
import com.tangem.blockchainsdk.utils.fromNetworkId
@ -16,6 +16,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
@ -101,7 +102,7 @@ class WalletConnectSdkHelper {
sourceAddress = transaction.from,
destinationAddress = destinationAddress,
extras = EthereumTransactionExtras(
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
callData = CompiledSmartContractCallData(transaction.data.removePrefix(HEX_PREFIX).hexToBytes()),
gasLimit = gasLimit.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
),
@ -191,7 +192,7 @@ class WalletConnectSdkHelper {
val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit(
amount = Amount(value, walletManager.wallet.blockchain),
destination = transaction.to ?: "",
data = transaction.data,
callData = CompiledSmartContractCallData(transaction.data.hexToBytes()),
)
return when (gasLimitResult) {
is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2"))
@ -357,36 +358,9 @@ class WalletConnectSdkHelper {
)
}
private fun createMessageData(message: WcSignMessage): ByteArray {
val messageData = try {
message.data.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
message.data.asciiToHex()?.hexToBytes() ?: byteArrayOf()
}
private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data)
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
return (prefixData + messageData).toKeccak()
}
private fun String.hexToAscii(): String? {
return try {
removePrefix(HEX_PREFIX).hexToBytes()
.map {
val char = it.toInt().toChar()
if (char.isAscii()) char else return null
}
.joinToString("")
} catch (exception: Exception) {
return null
}
}
private fun String.asciiToHex(): String? {
return map {
if (!it.isAscii()) return null
Integer.toHexString(it.code)
}.joinToString("")
}
private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this)
suspend fun signPersonalMessage(
hashToSign: ByteArray,
@ -535,7 +509,6 @@ class WalletConnectSdkHelper {
"{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
private companion object {
const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
// TODO remove after [REDACTED_JIRA]

View file

@ -31,7 +31,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.extensions.localizedDescriptionRes
@ -250,7 +250,7 @@ private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray,
if (artworkId.isNullOrEmpty()) {
defaultArtwork ?: Uri.EMPTY
} else {
Uri.parse(OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
Uri.parse(CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
}
}
is Result.Failure -> defaultArtwork ?: Uri.EMPTY

View file

@ -1,68 +1,14 @@
package com.tangem.tap.network.auth
import com.tangem.common.CardIdRangeDec
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.lib.auth.ExpressAuthProvider
import kotlinx.coroutines.runBlocking
import java.util.UUID
import java.util.concurrent.atomic.AtomicReference
internal class DefaultExpressAuthProvider(
private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
) : ExpressAuthProvider {
internal class DefaultExpressAuthProvider : ExpressAuthProvider {
private var uuid = AtomicReference(UUID.randomUUID())
override fun getUserId(): String {
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: error("No user id provided")
}
override fun getSessionId(): String {
return uuid.get().toString()
}
override fun getRefCode(): String {
val selectedUserWallet = userWalletsStore.selectedUserWalletOrNull ?: error("Can not get selected user wallet")
return when {
isRing(selectedUserWallet) -> "ring"
isChangeNow(selectedUserWallet) -> "ChangeNow"
isPartner(selectedUserWallet) -> "partner"
else -> ""
}
}
private fun isRing(selectedUserWallet: UserWallet): Boolean {
val addedWalletsWithRings = runBlocking {
appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY,
default = emptySet(),
)
}
return addedWalletsWithRings.contains(selectedUserWallet.walletId.stringValue)
}
private fun isChangeNow(selectedUserWallet: UserWallet): Boolean {
val changeNowRange = CardIdRangeDec(
start = "AF99001800554008",
end = "AF99001800559994",
)
val card = selectedUserWallet.scanResponse.card
return card.batchId == BATCH_ID_CHANGENOW || changeNowRange?.contains(card.cardId) == true
}
private fun isPartner(selectedUserWallet: UserWallet): Boolean {
return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_PARTNER
}
private companion object {
const val BATCH_ID_CHANGENOW = "BB000013"
const val BATCH_ID_PARTNER = "AF990015"
}
}

View file

@ -2,8 +2,6 @@ package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
@ -30,14 +28,8 @@ internal class AuthModule {
@Provides
@Singleton
fun provideExpressAuthProvider(
userWalletsStore: UserWalletsStore,
appPreferencesStore: AppPreferencesStore,
): ExpressAuthProvider {
return DefaultExpressAuthProvider(
userWalletsStore = userWalletsStore,
appPreferencesStore = appPreferencesStore,
)
fun provideExpressAuthProvider(): ExpressAuthProvider {
return DefaultExpressAuthProvider()
}
@Provides

View file

@ -1,366 +0,0 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.*
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
@Suppress("LargeClass")
class TransactionManagerImpl(
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsListManager: UserWalletsListManager,
) : TransactionManager {
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found")
return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) {
TxExploreState.Unsupported -> ""
is TxExploreState.Url -> txUrlState.url
}
}
override suspend fun updateWalletManager(networkId: String, derivationPath: String?) {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
getActualWalletManager(blockchain, derivationPath).update()
}
@Throws(IllegalStateException::class)
override suspend fun getFee(
networkId: String,
amountToSend: Amount,
currencyToSend: Currency,
destinationAddress: String,
increaseBy: Int?,
data: String?,
derivationPath: String?,
): ProxyFees {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
if (walletManager is EthereumOptimisticRollupWalletManager) {
return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = amountToSend,
destinationAddress = destinationAddress,
data = data,
)
}
return getFeeForEthereumBlockchain(
walletManager = walletManager,
blockchain = blockchain,
amountToSend = amountToSend,
destinationAddress = destinationAddress,
data = data,
increaseBy = increaseBy,
)
} else {
return getFeeForBlockchain(
walletManager = walletManager,
amountToSend = amountToSend,
destinationAddress = destinationAddress,
)
}
}
override fun getBlockchainInfo(networkId: String): ProxyNetworkInfo {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return ProxyNetworkInfo(
name = blockchain.fullName,
blockchainId = blockchain.id,
)
}
private suspend fun getFeeForBlockchain(
walletManager: WalletManager,
amountToSend: Amount,
destinationAddress: String,
): ProxyFees {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = amountToSend,
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
return when (fee) {
is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now
when (fee.data) {
is TransactionFee.Single -> {
val singleFee = when (val normalFee = (fee.data as TransactionFee.Single).normal) {
is Fee.CardanoToken -> {
ProxyFee.CardanoToken(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
minAdaValue = normalFee.minAdaValue,
)
}
is Fee.Filecoin -> {
ProxyFee.Filecoin(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
gasPremium = normalFee.gasPremium,
)
}
is Fee.Sui -> {
ProxyFee.Sui(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
gasPrice = normalFee.gasPrice,
gasBudget = normalFee.gasBudget,
)
}
else -> {
ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
)
}
}
ProxyFees.SingleFee(singleFee = singleFee)
}
is TransactionFee.Choosable -> {
val choosableFee = fee.data as TransactionFee.Choosable
ProxyFees.MultipleFees(
minFee = ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
),
normalFee = ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.normal.amount),
),
priorityFee = ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
),
)
}
}
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
@Suppress("LongParameterList")
private suspend fun getFeeForEthereumBlockchain(
walletManager: EthereumWalletManager,
blockchain: Blockchain,
amountToSend: Amount,
destinationAddress: String,
data: String?,
increaseBy: Int?,
): ProxyFees {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
amount = amountToSend,
destinationAddress = destinationAddress,
data = data,
).increaseBigIntegerByPercents(increaseBy)
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
}
}
}
private suspend fun getFeeForOptimismBlockchain(
walletManager: EthereumOptimisticRollupWalletManager,
amount: Amount,
destinationAddress: String,
data: String?,
): ProxyFees {
val fee = if (data.isNullOrEmpty()) {
walletManager.getFee(amount, destinationAddress)
} else {
walletManager.getFee(amount, destinationAddress, data)
}
return when (fee) {
is Result.Success -> {
val choosableFee = fee.data
val minProxyFee = ProxyFee.Common(
gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit,
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
)
val normalProxyFee = ProxyFee.Common(
gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit,
fee = convertToProxyAmount(amount = choosableFee.normal.amount),
)
val priorityProxyFee = ProxyFee.Common(
gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit,
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
)
ProxyFees.MultipleFees(
minFee = minProxyFee,
normalFee = normalProxyFee,
priorityFee = priorityProxyFee,
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
private suspend fun getGasLimit(
evmWalletManager: EthereumWalletManager,
amount: Amount,
destinationAddress: String,
data: String?,
): BigInteger {
val result = if (data.isNullOrEmpty()) {
evmWalletManager.getGasLimit(
amount = amount,
destination = destinationAddress,
)
} else {
evmWalletManager.getGasLimit(
amount = amount,
destination = destinationAddress,
data = data,
)
}
when (result) {
is Result.Success -> {
return result.data
}
is Result.Failure -> {
error(result.error.message ?: result.error.customMessage)
}
}
}
override suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
val gasPriceResult = (walletManager as? EthereumWalletManager)?.getGasPrice()
?: error("not supported for $blockchain")
val gasPrice = when (gasPriceResult) {
is Result.Failure -> error("fail to receive gasPrice")
is Result.Success -> gasPriceResult.data
}
return createMultipleProxyFees(gasPrice, gas, blockchain)
}
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
return requireNotNull(walletManager) { "no wallet manager found" }
}
/**
* Create proxy fees
*
* @param gasPrice min fee gasPrice
* @param gasLimit
* @param blockchain
*/
private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
val patchedGasLimit = gasLimit.toBigDecimal().increaseForMantleIfNeeded(blockchain).toBigInteger()
val gasPriceNormal = gasPrice
.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE)
val feeMin = patchedGasLimit.multiply(gasPrice).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).increaseForMantleIfNeeded(blockchain)
val feeNormal = patchedGasLimit.multiply(gasPriceNormal).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).increaseForMantleIfNeeded(blockchain)
val feePriority = patchedGasLimit.multiply(gasPricePriority).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).increaseForMantleIfNeeded(blockchain)
val minFee = ProxyFee.Common(
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeMin,
decimals = blockchain.decimals(),
),
)
val normalFee = ProxyFee.Common(
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeNormal,
decimals = blockchain.decimals(),
),
)
val priorityFee = ProxyFee.Common(
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feePriority,
decimals = blockchain.decimals(),
),
)
return ProxyFees.MultipleFees(
minFee = minFee,
normalFee = normalFee,
priorityFee = priorityFee,
)
}
private fun convertToProxyAmount(amount: Amount): ProxyAmount {
return ProxyAmount(
currencySymbol = amount.currencySymbol,
value = amount.value ?: BigDecimal.ZERO,
decimals = amount.decimals,
)
}
/**
* Increase big integer by percents
*
* @param percents in format 150 -> 50%
* @return increased value
*/
private fun BigInteger.increaseBigIntegerByPercents(percents: Int?): BigInteger {
return if (percents != null && percents != 0) {
this.multiply(percents.toBigInteger()).divide(BigInteger("100"))
} else {
this
}
}
// TODO Workaround for Mantle. Remove after [REDACTED_JIRA]
private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal {
return if (blockchain == Blockchain.Mantle) {
this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
} else {
this
}
}
companion object {
private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50%
private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50%
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
}
}

View file

@ -2,10 +2,8 @@ package com.tangem.tap.proxy.di
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.TransactionManagerImpl
import com.tangem.tap.proxy.UserWalletManagerImpl
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -37,16 +35,4 @@ internal object ProxyModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideTransactionManager(
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
): TransactionManager {
return TransactionManagerImpl(
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
)
}
}

View file

@ -32,6 +32,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@ -76,5 +77,6 @@ data class DaggerGraphState(
val settingsManager: SettingsManager? = null,
val uiMessageSender: UiMessageSender? = null,
val onlineCardVerifier: OnlineCardVerifier? = null,
val cardArworksProvider: CardArtworksProvider? = null,
val userWalletBuilderFactory: UserWalletBuilder.Factory? = null,
) : StateType

View file

@ -12,13 +12,12 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.component.NFTDetailsComponent
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.component.*
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
@ -83,9 +82,8 @@ internal class ChildFactory @Inject constructor(
private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory,
private val sendFeatureToggles: SendFeatureToggles,
private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory,
private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory,
private val nftReceiveComponentFactory: NFTReceiveComponent.Factory,
private val nftDetailsComponentFactory: NFTDetailsComponent.Factory,
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val testerRouter: TesterRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
@ -182,7 +180,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.OnrampSuccess -> {
createComponentChild(
context = context,
params = OnrampSuccessComponent.Params(route.externalTxId),
params = OnrampSuccessComponent.Params(route.txId),
componentFactory = onrampSuccessComponentFactory,
)
}
@ -405,24 +403,26 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletComponentFactory,
)
}
is AppRoute.NFTCollections ->
is AppRoute.NFT ->
createComponentChild(
context = context,
params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId),
componentFactory = nftCollectionsComponentFactory,
params = NFTComponent.Params(
userWalletId = route.userWalletId,
walletName = route.walletName,
),
componentFactory = nftComponentFactory,
)
is AppRoute.NFTReceive ->
is AppRoute.NFTSend -> {
createComponentChild(
context = context,
params = NFTReceiveComponent.Params(userWalletId = route.userWalletId),
componentFactory = nftReceiveComponentFactory,
)
is AppRoute.NFTDetails ->
createComponentChild(
context = context,
params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset),
componentFactory = nftDetailsComponentFactory,
params = NFTSendComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.nftCollectionName,
),
componentFactory = nftSendComponentFactory,
)
}
is AppRoute.OnboardingNote,
is AppRoute.SaveWallet,
is AppRoute.OnboardingOther,
@ -703,7 +703,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.OnrampSuccess -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampSuccessComponent.Params(route.externalTxId),
params = OnrampSuccessComponent.Params(route.txId),
componentFactory = onrampSuccessComponentFactory,
)
}
@ -757,24 +757,26 @@ internal class ChildFactory @Inject constructor(
componentFactory = storiesComponentFactory,
)
}
is AppRoute.NFTCollections ->
is AppRoute.NFT ->
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId),
componentFactory = nftCollectionsComponentFactory,
params = NFTComponent.Params(
userWalletId = route.userWalletId,
walletName = route.walletName,
),
componentFactory = nftComponentFactory,
)
is AppRoute.NFTReceive ->
is AppRoute.NFTSend -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTReceiveComponent.Params(userWalletId = route.userWalletId),
componentFactory = nftReceiveComponentFactory,
)
is AppRoute.NFTDetails ->
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset),
componentFactory = nftDetailsComponentFactory,
params = NFTSendComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.nftCollectionName,
),
componentFactory = nftSendComponentFactory,
)
}
}
// endregion
}

View file

@ -39,7 +39,6 @@ internal class DefaultDerivationsRepositoryTest {
private val defaultUserWallet = UserWallet(
name = "",
walletId = defaultUserWalletId,
artworkUrl = "",
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),

View file

@ -194,7 +194,6 @@ internal class BiometricUserWalletsListManagerTest(private val model: Model) {
return UserWallet(
name = "Wallet $id",
walletId = UserWalletId(stringValue = id),
artworkUrl = "",
cardsInWallet = emptySet(),
isMultiCurrency = true,
hasBackupError = false,

View file

@ -59,9 +59,41 @@ val assembleInternalQA by tasks.registering {
}
}
val assembleExternalQA by tasks.registering {
group = "build"
description = "Builds external APK to 'build/outputs' directory"
val appOutputApkDir = "$projectDir/app/build/outputs/apk/external"
val rootOutputApkDir = "$buildDir/outputs"
val injected = objects.newInstance<Injected>()
dependsOn(":app:assembleExternal")
doFirst {
injected.fs.delete {
delete(appOutputApkDir)
delete("$rootOutputApkDir/app-external.apk")
}
}
doLast {
injected.fs.copy {
from("$appOutputApkDir/app-external.apk")
into(rootOutputApkDir)
}
}
}
val assembleQA by tasks.registering {
group = "build"
description = "Builds internal and external APKs to 'build/outputs' directory"
dependsOn(assembleInternalQA)
dependsOn(assembleExternalQA)
}
val generateComposeMetrics by tasks.registering {
group = "other"
description = "Build internal APK and generates compose metrics to 'build/compose-metrics' directory"
description = "Build external APK and generates compose metrics to 'build/compose-metrics' directory"
subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
@ -86,5 +118,5 @@ val generateComposeMetrics by tasks.registering {
}
}
finalizedBy(assembleInternalQA)
finalizedBy(assembleExternalQA)
}

View file

@ -233,8 +233,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class OnrampSuccess(
val externalTxId: String,
) : AppRoute(path = "/onramp/success/$externalTxId"), RouteBundleParams {
val txId: String,
) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@ -281,18 +281,15 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/stories$storyId")
@Serializable
data class NFTCollections(
data class NFT(
val userWalletId: UserWalletId,
) : AppRoute(path = "/nft_collections/${userWalletId.stringValue}")
val walletName: String,
) : AppRoute(path = "/nft/${userWalletId.stringValue}")
@Serializable
data class NFTReceive(
val userWalletId: UserWalletId,
) : AppRoute(path = "/nft_receive/${userWalletId.stringValue}")
@Serializable
data class NFTDetails(
data class NFTSend(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}")
val nftCollectionName: String,
) : AppRoute(path = "/send/nft/${userWalletId.stringValue}/$nftCollectionName/${nftAsset.id}")
}

View file

@ -9,10 +9,15 @@ android {
}
dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.data.common)
implementation(projects.data.staking)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
@ -20,6 +25,7 @@ dependencies {
implementation(projects.libs.blockchainSdk)
implementation(deps.androidx.datastore)
implementation(deps.jodatime)
implementation(deps.test.coroutine)
implementation(tangemDeps.blockchain)

View file

@ -0,0 +1,19 @@
package com.tangem.common.test.data.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
object MockQuoteResponseFactory {
fun createSinglePrice(value: BigDecimal): QuotesResponse.Quote {
return QuotesResponse.Quote(
price = value,
priceChange24h = value,
priceChange1w = value,
priceChange30d = value,
)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.common.test.data.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.quote.converter.QuoteConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Quote
fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): Quote {
return QuoteConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first())
}
fun Pair<String, QuotesResponse.Quote>.toDomain(source: StatusSource = StatusSource.ACTUAL): Quote {
return QuoteConverter(source = source).convert(value = mapOf(this).entries.first())
}

View file

@ -0,0 +1,59 @@
package com.tangem.common.test.data.staking
import com.tangem.datasource.api.stakekit.models.request.Address
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.model.StakingID
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
object MockYieldBalanceWrapperDTOFactory {
val defaultStakingId = StakingID(
integrationId = "ton-ton-chorus-one-pools-staking",
address = "0x1",
)
fun createWithBalance(stakingId: StakingID = defaultStakingId): YieldBalanceWrapperDTO {
return YieldBalanceWrapperDTO(
addresses = Address(address = stakingId.address),
balances = listOf(
BalanceDTO(
groupId = "groupId",
type = BalanceDTO.BalanceTypeDTO.UNKNOWN,
amount = BigDecimal.ONE,
date = null,
pricePerShare = BigDecimal.ZERO,
pendingActions = listOf(),
pendingActionConstraints = null,
tokenDTO = TokenDTO(
name = "The-Open-Network",
network = NetworkTypeDTO.TON,
symbol = "TON",
decimals = 8,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
validatorAddress = null,
validatorAddresses = null,
providerId = null,
),
),
integrationId = stakingId.integrationId,
)
}
fun createWithEmptyBalance(stakingId: StakingID = defaultStakingId): YieldBalanceWrapperDTO {
return YieldBalanceWrapperDTO(
addresses = Address(address = stakingId.address),
balances = emptyList(),
integrationId = stakingId.integrationId,
)
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.common.test.data.staking
import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.domain.staking.model.StakingID
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
object MockYieldDTOFactory {
val defaultStakingID = StakingID(
integrationId = "ton-ton-chorus-one-pools-staking",
address = "0x1",
)
fun create(stakingID: StakingID = defaultStakingID): YieldDTO {
return YieldDTO(
id = stakingID.integrationId,
token = TokenDTO(
name = "Miguel Estes",
network = NetworkTypeDTO.POLYGON,
symbol = "splendide",
decimals = 1323,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokens = listOf(),
args = YieldDTO.ArgsDTO(
enter = YieldDTO.ArgsDTO.Enter(
addresses = YieldDTO.ArgsDTO.Enter.Addresses(
address = AddressArgumentDTO(required = false),
),
args = mapOf(),
),
exit = null,
),
status = YieldDTO.StatusDTO(enter = true, exit = true),
apy = BigDecimal.ONE,
rewardRate = 1.0,
rewardType = YieldDTO.RewardTypeDTO.UNKNOWN,
metadata = YieldDTO.MetadataDTO(
name = "name",
logoUri = "logoUri",
description = "description",
documentation = null,
gasFeeTokenDTO = TokenDTO(
name = "Johnnie Mullen",
network = NetworkTypeDTO.POLYGON,
symbol = "fuisset",
decimals = 2957,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokenDTO = TokenDTO(
name = "Lazaro Wood",
network = NetworkTypeDTO.POLYGON,
symbol = "vocent",
decimals = 1602,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokensDTO = listOf(),
type = "type",
rewardSchedule = YieldDTO.MetadataDTO.RewardScheduleDTO.DAY,
cooldownPeriod = null,
warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1),
rewardClaiming = YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO,
defaultValidator = null,
minimumStake = null,
supportsMultipleValidators = null,
revshare = YieldDTO.MetadataDTO.EnabledDTO(enabled = true),
fee = YieldDTO.MetadataDTO.EnabledDTO(enabled = true),
),
validators = listOf(),
isAvailable = true,
)
}
}

View file

@ -23,7 +23,6 @@ object MockUserWalletFactory {
return UserWallet(
walletId = userWalletId,
name = "Wallet 1",
artworkUrl = "",
cardsInWallet = emptySet(),
scanResponse = scanResponse,
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),

View file

@ -1,19 +1,18 @@
package com.tangem.common.test.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> CoroutineScope.getEmittedValues(testScheduler: TestCoroutineScheduler, actual: Flow<T>): List<T> {
fun <T> TestScope.getEmittedValues(flow: Flow<T>): List<T> {
val values = mutableListOf<T>()
launch(UnconfinedTestDispatcher(testScheduler)) {
actual.toList(values)
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
flow.toList(values)
}
return values

View file

@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -61,7 +61,7 @@ fun UserWalletItem(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(imageUrl = state.imageUrl)
CardImage(state.imageState)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
@ -147,39 +147,49 @@ private fun NameAndInfo(
}
@Composable
private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
private fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
when (imageState) {
is UserWalletItemUM.ImageState.Loading -> {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
}
is UserWalletItemUM.ImageState.Image -> {
SubcomposeAsyncImage(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageState.artwork.verifiedArtwork?.toByteArray() ?: imageState.artwork.defaultUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
},
contentDescription = null,
)
}
}
}
@Composable
@ -215,7 +225,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Locked,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -224,7 +233,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Hidden,
imageUrl = "",
isEnabled = true,
onClick = {},
endIcon = UserWalletItemUM.EndIcon.Arrow,
@ -234,7 +242,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Failed,
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
@ -244,7 +251,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
@ -257,7 +263,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
value = "1.2345 BTC",
isFlickering = false,
),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
@ -270,7 +275,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
value = "1.2345 BTC",
isFlickering = true,
),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},

View file

@ -2,6 +2,7 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
@ -9,6 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
@ -31,6 +33,7 @@ class UserWalletItemUMConverter(
private val balance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
private val artwork: ArtworkModel? = null,
) : Converter<UserWallet, UserWalletItemUM> {
override fun convert(value: UserWallet): UserWalletItemUM {
@ -40,10 +43,12 @@ class UserWalletItemUMConverter(
name = stringReference(name),
information = getInfo(userWallet = this),
balance = getBalanceInfo(userWallet = this),
imageUrl = artworkUrl,
isEnabled = !isLocked,
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = artwork?.let {
UserWalletItemUM.ImageState.Image(ArtworkUM(it.verifiedArtwork, it.defaultUrl))
} ?: UserWalletItemUM.ImageState.Loading,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import javax.annotation.concurrent.Immutable
@ -10,7 +11,7 @@ data class UserWalletItemUM(
val name: TextReference,
val information: TextReference,
val balance: Balance,
val imageUrl: String,
val imageState: ImageState = ImageState.Loading,
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
@ -36,4 +37,14 @@ data class UserWalletItemUM(
val isFlickering: Boolean,
) : Balance()
}
@Immutable
sealed class ImageState {
data object Loading : ImageState()
data class Image(
val artwork: ArtworkUM,
) : ImageState()
}
}

View file

@ -22,9 +22,35 @@ sealed class MainScreenAnalyticsEvent(
)
// region Action Buttons feature
data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
data class ButtonBuy(
val status: AnalyticsParam.Status,
val screenType: String? = null,
) : MainScreenAnalyticsEvent(
event = "Button - Buy",
params = mapOf(AnalyticsParam.STATUS to status.value),
params = buildMap {
put(AnalyticsParam.STATUS, status.value)
screenType?.let { put(AnalyticsParam.TYPE, it) }
},
)
data object ButtonReceive : MainScreenAnalyticsEvent(
event = "Button - Receive",
)
data object LimitsClicked : MainScreenAnalyticsEvent(
event = "Limits Clicked",
)
data object NoticeBalancesInfo : MainScreenAnalyticsEvent(
event = "Notice - Balances Info",
)
data object NoticeLimitsInfo : MainScreenAnalyticsEvent(
event = "Notice - Limits Info",
)
data object ButtonExplore : MainScreenAnalyticsEvent(
event = "Button - Explore",
)
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
@ -83,4 +109,9 @@ sealed class MainScreenAnalyticsEvent(
params = mapOf(ERROR_CODE to errorCode),
)
// endregion
companion object {
const val VISA_TYPE = "Visa"
const val WALLET_TYPE = "Wallet"
}
}

View file

@ -43,6 +43,10 @@
"name": "NOTE_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "NEW_ARTWORK_LOADING",
"version": "5.24.0"
},
{
"name": "NEW_ATTESTATION_ENABLED",
"version": "undefined"
@ -62,5 +66,13 @@
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0"
},
{
"name": "STAKING_LOADING_REFACTORING_ENABLED",
"version": "undefined"
}
]

View file

@ -32,7 +32,7 @@ internal class DevFeatureTogglesManager(
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
) ?: emptyMap<String, Boolean>()
val localFeatureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())

View file

@ -2,6 +2,7 @@ package com.tangem.core.configtoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.Moshi
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.storage.ConfigToggle
@ -12,6 +13,7 @@ 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.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,7 +26,11 @@ import kotlin.collections.set
internal class DevTogglesManagerTest {
private val localTogglesStorage = mockk<TogglesStorage>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val appPreferenceStore = AppPreferencesStore(
moshi = Moshi.Builder().build(),
dispatchers = TestingCoroutineDispatcherProvider(),
preferencesDataStore = mockk(relaxed = true),
)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localTogglesStorage = localTogglesStorage,

View file

@ -8,6 +8,7 @@ import com.squareup.moshi.Types
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,7 +25,11 @@ internal class LocalTogglesStorageTest {
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val assetLoader = AssetLoader(
assetReader = assetReader,
moshi = moshi,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val storage = LocalTogglesStorage(assetLoader)

View file

@ -72,6 +72,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.api.common.blockaid
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import retrofit2.http.Body
import retrofit2.http.POST
interface BlockAidApi {
@POST("site/scan")
suspend fun scanDomain(@Body request: DomainScanRequest): DomainScanResponse
@POST("evm/json-rpc/scan")
suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse
@POST("solana/message/scan")
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse
}

View file

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

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.common.blockaid.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
@JsonClass(generateAdapter = true)
data class EvmTransactionScanRequest(
@Json(name = "chain") val chain: String,
@Json(name = "account_address") val accountAddress: String,
@Json(name = "method") val method: String,
@Json(name = "data") val data: RpcData,
@Json(name = "options") val options: List<String> = listOf("simulation", "validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,
)
@JsonClass(generateAdapter = true)
data class RpcData(
@Json(name = "jsonrpc") val jsonrpc: String = "2.0",
@Json(name = "method") val method: String,
@Json(name = "params") val params: String,
)

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.common.blockaid.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
@JsonClass(generateAdapter = true)
data class SolanaTransactionScanRequest(
@Json(name = "encoding") val encoding: String = "base64",
@Json(name = "chain") val chain: String,
@Json(name = "method") val method: String,
@Json(name = "options") val options: List<String> = listOf("simulation, validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,
@Json(name = "account_address") val accountAddress: String,
@Json(name = "transactions") val transactions: List<String>,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AccountSummaryResponse(
@Json(name = "assets_diffs") val assetsDiffs: List<AssetDiff>,
@Json(name = "exposures") val exposures: List<Exposure>,
)

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AssetDiff(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: Asset,
@Json(name = "in") val inTransfer: List<Transfer>? = null,
@Json(name = "out") val outTransfer: List<Transfer>? = null,
)
@JsonClass(generateAdapter = true)
data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String,
)
@JsonClass(generateAdapter = true)
data class Transfer(
@Json(name = "value") val value: String,
@Json(name = "raw_value") val rawValue: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DomainScanResponse(
@Json(name = "status") val status: String,
@Json(name = "is_malicious") val isMalicious: Boolean?,
)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Exposure(
@Json(name = "asset") val asset: Asset,
@Json(name = "spenders") val spenders: Map<String, SpenderDetails>,
)
@JsonClass(generateAdapter = true)
data class SpenderDetails(
@Json(name = "exposure") val exposure: List<ExposureDetail>,
@Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null,
)
@JsonClass(generateAdapter = true)
data class ExposureDetail(
@Json(name = "value") val value: String,
@Json(name = "raw_value") val rawValue: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimulationResponse(
@Json(name = "status") val status: String,
@Json(name = "account_summary") val accountSummary: AccountSummaryResponse,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TransactionMetadata(
@Json(name = "domain") val domain: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TransactionScanResponse(
@Json(name = "validation") val validation: ValidationResponse,
@Json(name = "simulation") val simulation: SimulationResponse,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ValidationResponse(
@Json(name = "status") val status: String,
@Json(name = "result_type") val resultType: String,
)

View file

@ -27,6 +27,7 @@ sealed class ApiConfig {
TangemVisaAuth,
TangemVisa,
TangemCardSdk,
BlockAid,
}
private fun initializeId(): ID {
@ -37,6 +38,7 @@ sealed class ApiConfig {
is TangemVisaAuth -> ID.TangemVisaAuth
is TangemVisa -> ID.TangemVisa
is TangemCardSdk -> ID.TangemCardSdk
is BlockAid -> ID.BlockAid
}
}
@ -44,6 +46,7 @@ sealed class ApiConfig {
internal const val DEBUG_BUILD_TYPE = "debug"
internal const val INTERNAL_BUILD_TYPE = "internal"
internal const val MOCKED_BUILD_TYPE = "mocked"
internal const val EXTERNAL_BUILD_TYPE = "external"
internal const val RELEASE_BUILD_TYPE = "release"
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.utils.ProviderSuspend
internal class BlockAid(
private val environmentConfigStorage: EnvironmentConfigStorage,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val environmentConfigs = listOf(
createProdEnvironment(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.blockaid.io/v0/",
headers = buildMap {
environmentConfigStorage.getConfigSync().blockAidApiKey?.let { apiKey ->
put("X-API-KEY", ProviderSuspend { apiKey })
}
put("accept", ProviderSuspend { "application/json" })
put("content-type", ProviderSuspend { "application/json" })
},
)
}

View file

@ -48,10 +48,8 @@ internal class Express(
private fun createHeaders(isProd: Boolean) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId))
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode))
}
private fun getApiKey(isProd: Boolean): String {
@ -73,6 +71,7 @@ internal class Express(
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")

View file

@ -15,7 +15,7 @@ internal class TangemVisa(
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://bff.tangem.com/",
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)

View file

@ -15,7 +15,7 @@ internal class TangemVisaAuth(
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "https://api-s.tangem.org/",
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)

View file

@ -5,10 +5,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.*
/**
* Interface of Tangem Express API (new swap mechanism)
@ -17,16 +14,29 @@ import retrofit2.http.Query
interface TangemExpressApi {
@POST("assets")
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
suspend fun getAssets(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: AssetsRequestBody,
): ApiResponse<List<Asset>>
@POST("pairs")
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
suspend fun getPairs(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: PairsRequestBody,
): ApiResponse<List<SwapPair>>
@GET("providers")
suspend fun getProviders(): ApiResponse<List<ExchangeProvider>>
suspend fun getProviders(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<ExchangeProvider>>
@GET("exchange-quote")
suspend fun getExchangeQuote(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@ -40,6 +50,8 @@ interface TangemExpressApi {
@GET("exchange-data")
suspend fun getExchangeData(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromContractAddress") fromContractAddress: String,
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@ -57,8 +69,16 @@ interface TangemExpressApi {
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
suspend fun getExchangeStatus(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("txId") txId: String,
): ApiResponse<ExchangeStatusResponse>
@POST("exchange-sent")
suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse<ExchangeSentResponseBody>
suspend fun exchangeSent(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: ExchangeSentRequestBody,
): ApiResponse<ExchangeSentResponseBody>
}

View file

@ -3,37 +3,52 @@ package com.tangem.datasource.api.onramp
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.*
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
interface OnrampApi {
@GET("currencies")
suspend fun getCurrencies(): ApiResponse<List<OnrampCurrencyDTO>>
suspend fun getCurrencies(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<OnrampCurrencyDTO>>
@GET("countries")
suspend fun getCountries(): ApiResponse<List<OnrampCountryDTO>>
suspend fun getCountries(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<OnrampCountryDTO>>
@GET("country-by-ip")
suspend fun getCountryByIp(): ApiResponse<OnrampCountryDTO>
suspend fun getCountryByIp(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<OnrampCountryDTO>
@GET("payment-methods")
suspend fun getPaymentMethods(): ApiResponse<List<PaymentMethodDTO>>
suspend fun getPaymentMethods(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
): ApiResponse<List<PaymentMethodDTO>>
@POST("onramp-pairs")
suspend fun getPairs(@Body body: OnrampPairsRequest): ApiResponse<List<OnrampPairDTO>>
suspend fun getPairs(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Body body: OnrampPairsRequest,
): ApiResponse<List<OnrampPairDTO>>
@GET("onramp-quote")
suspend fun getQuote(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromCurrencyCode") fromCurrencyCode: String,
@Query("fromPrecision") fromPrecision: Int,
@Query("toContractAddress") toContractAddress: String,
@ -47,6 +62,8 @@ interface OnrampApi {
@GET("onramp-data")
suspend fun getData(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("fromCurrencyCode") fromCurrencyCode: String,
@Query("fromPrecision") fromPrecision: Int,
@Query("toContractAddress") toContractAddress: String,
@ -64,5 +81,9 @@ interface OnrampApi {
): ApiResponse<OnrampDataResponse>
@GET("onramp-status")
suspend fun getStatus(@Query("txId") txId: String): ApiResponse<OnrampStatusResponse>
suspend fun getStatus(
@Header("user-id") userWalletId: String,
@Header("refcode") refCode: String,
@Query("txId") txId: String,
): ApiResponse<OnrampStatusResponse>
}

View file

@ -91,4 +91,10 @@ enum class Status {
@Json(name = "paused")
Paused,
@Json(name = "refund-in-progress")
RefundInProgress,
@Json(name = "refunded")
Refunded,
}

View file

@ -140,6 +140,40 @@ interface TangemTechApi {
@GET("stories/{story_id}")
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
// region push notifications
@GET("notification/push_notifications_eligible_networks")
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>
@POST("user-wallets/applications/")
suspend fun createApplicationId(
@Body
body: NotificationApplicationCreateBody,
): ApiResponse<NotificationApplicationIdResponse>
@PATCH("user-wallets/applications/{application_id}")
suspend fun updatePushTokenForApplicationId(
@Path("application_id") applicationId: String,
@Body body: NotificationApplicationCreateBody,
): ApiResponse<String>
@PATCH("user-wallets/wallets/{wallet_id}/notify")
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
// endregion
// region wallets
@PATCH("user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
suspend fun associateApplicationIdWithWallets(
@Path("application_id") applicationId: String,
@Body body: List<WalletIdBody>,
): ApiResponse<Unit>
@GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
// endregion
companion object {
val marketsQuoteFields = listOf(
"price",

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CryptoNetworkResponse(
@Json(name = "id") val id: Int,
@Json(name = "networkId") val networkId: String,
@Json(name = "name") val name: String,
)

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NotificationApplicationCreateBody(
@Json(name = "pushToken") val pushToken: String? = null,
@Json(name = "platform") val platform: String? = null,
@Json(name = "device") val device: String? = null,
@Json(name = "systemVersion") val systemVersion: String? = null,
@Json(name = "language") val language: String? = null,
@Json(name = "timezone") val timezone: String? = null,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NotificationApplicationIdResponse(
@Json(name = "uid") val appId: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NotificationApplicationUpdateBody(
@Json(name = "pushToken") val pushToken: String,
@Json(name = "systemVersion") val systemVersion: String? = null,
@Json(name = "language") val language: String? = null,
@Json(name = "timezone") val timezone: String? = null,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletBody(
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
@Json(name = "name") val name: String? = null,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletIdBody(
@Json(name = "id") val walletId: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletResponse(
@Json(name = "notifyStatus") val notifyStatus: Boolean,
@Json(name = "id") val id: String,
@Json(name = "name") val name: String? = null,
)

View file

@ -1,9 +1,11 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.*
import retrofit2.http.Body
@ -11,60 +13,40 @@ import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
interface TangemVisaApi {
@GET("product_instance/activation_status")
@POST("v1/activation/status")
suspend fun getRemoteActivationStatus(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
@Body request: ActivationStatusRequest,
): ApiResponse<CardActivationRemoteStateResponse>
@ReadTimeout(duration = 20, TimeUnit.MINUTES)
@GET("product_instance/activation_status")
suspend fun getRemoteActivationStatusLongPoll(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
): ApiResponse<CardActivationRemoteStateResponse>
@GET("product_instance/card_wallet_acceptance")
@POST("v1/activation/acceptance/message")
suspend fun getCardWalletAcceptance(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("activation_order") activationOrderId: String,
@Query("customer_wallet_address") customerWalletAddress: String,
): ApiResponse<CardWalletDataToSignResponse>
@Body request: GetCardWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@GET("product_instance/customer_wallet_acceptance")
@POST("v1/activation/acceptance/message")
suspend fun getCustomerWalletAcceptance(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("activation_order") activationOrderId: String,
@Query("card_wallet_address") cardWalletAddress: String,
): ApiResponse<CustomerWalletDataToSignResponse>
@Body request: GetCustomerWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@POST("product_instance/activation_by_card_wallet")
@POST("v1/activation/data")
suspend fun activateByCardWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCardWalletRequest,
): ApiResponse<Unit>
@POST("product_instance/activation_by_customer_wallet")
@POST("v1/activation/data")
suspend fun activateByCustomerWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCustomerWalletRequest,
): ApiResponse<Unit>
@POST("product_instance/issuer_activation")
@POST("v1/activation/pin")
suspend fun setPinCode(
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,

View file

@ -1,32 +1,29 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.visa.models.request.*
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
import com.tangem.datasource.api.visa.models.response.JWTResponse
import retrofit2.http.Field
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Query
interface TangemVisaAuthApi {
@POST("auth/card_wallet")
suspend fun generateNonceByWalletAddress(
@Query("card_wallet_address") cardWalletAddress: String,
): GenerateNonceResponse
@POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
@POST("auth/card_id")
suspend fun generateNonceByCard(
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
): GenerateNonceResponse
@POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
@POST("auth/get_token")
suspend fun getAccessToken(
@Query("session_id") sessionId: String,
@Query("signature") signature: String,
@Query("salt") salt: String?,
): JWTResponse
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
@POST("auth/refresh_token")
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
@POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse<JWTResponse>
}

View file

@ -5,22 +5,15 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ActivationByCardWalletRequest(
@Json(name = "customer_id") val customerId: String,
@Json(name = "product_instance_id") val productInstanceId: String,
@Json(name = "activation_order_id") val activationOrderId: String,
@Json(name = "data") val data: Data,
@Json(name = "order_id") val orderId: String,
@Json(name = "card_wallet") val cardWallet: CardWallet,
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
@Json(name = "otp") val otp: Otp,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "card_wallet") val cardWallet: CardWallet,
@Json(name = "otp") val otp: Otp,
)
@JsonClass(generateAdapter = true)
data class CardWallet(
@Json(name = "address") val address: String,
@Json(name = "card_wallet_confirmation") val cardWalletConfirmation: CardWalletConfirmation?,
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
)
@JsonClass(generateAdapter = true)

View file

@ -5,19 +5,12 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ActivationByCustomerWalletRequest(
@Json(name = "customer_id") val customerId: String,
@Json(name = "product_instance_id") val productInstanceId: String,
@Json(name = "activation_order_id") val activationOrderId: String,
@Json(name = "data") val data: Data,
@Json(name = "order_id") val orderId: String,
@Json(name = "customer_wallet") val customerWallet: CustomerWallet,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "customer_wallet") val customerWallet: CustomerWallet,
)
@JsonClass(generateAdapter = true)
data class CustomerWallet(
@Json(name = "address") val address: String,
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
@Json(name = "address") val customerWalletAddress: String,
)
}

View file

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

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNoneByCardIdRequest(
@Json(name = "auth_type") val authType: String = "card_id",
@Json(name = "card_id") val cardId: String,
@Json(name = "card_public_key") val cardPublicKey: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNoneByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetAccessTokenByCardIdRequest(
@Json(name = "auth_type") val authType: String = "card_id",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
@Json(name = "salt") val salt: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetAccessTokenByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetCardWalletAcceptanceRequest(
@Json(name = "type") val type: String = "card_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetCustomerWalletAcceptanceRequest(
@Json(name = "type") val type: String = "customer_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RefreshTokenByCardIdRequest(
@Json(name = "auth_type") val authType: String = "card_id",
@Json(name = "refresh_token") val refreshToken: String,
)

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RefreshTokenByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "refresh_token") val refreshToken: String,
)

View file

@ -5,14 +5,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinCodeRequest(
@Json(name = "customer_id") val customerId: String,
@Json(name = "activation_order_id") val activationOrderId: String,
@Json(name = "product_instance_id") val productInstanceId: String,
@Json(name = "data") val data: Data,
) {
data class Data(
@Json(name = "session_key") val sessionKey: String,
@Json(name = "iv") val iv: String,
@Json(name = "encrypted_pin") val encryptedPin: String,
)
}
@Json(name = "order_id") val orderId: String,
@Json(name = "session_id") val sessionId: String,
@Json(name = "iv") val iv: String,
@Json(name = "pin") val pin: String,
)

View file

@ -5,12 +5,21 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardActivationRemoteStateResponse(
@Json(name = "activation_status") val status: String,
@Json(name = "activation_order") val activationOrder: ActivationOrder?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "status") val status: String,
@Json(name = "order") val activationOrder: ActivationOrder?,
@Json(name = "stepChangeCode") val stepChangeCode: Int?,
@Json(name = "updatedAt") val updatedAt: String?,
)
@JsonClass(generateAdapter = true)
data class ActivationOrder(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
)
}

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerWalletDataToSignResponse(
@Json(name = "data_for_customer_wallet") val dataForCardWallet: Data,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "hash") val hash: String,
)
}

View file

@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNonceResponse(
@Json(name = "nonce") val nonce: String,
@Json(name = "session_id") val sessionId: String,
)
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "nonce") val nonce: String,
@Json(name = "session_id") val sessionId: String,
)
}

View file

@ -5,12 +5,17 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class JWTResponse(
@Json(name = "access_token") val accessToken: String,
@Json(name = "expires_in") val expiresIn: Int,
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
@Json(name = "refresh_token") val refreshToken: String,
@Json(name = "token_type") val tokenType: String,
@Json(name = "not-before-policy") val notBeforePolicy: Int,
@Json(name = "session_state") val sessionState: String,
@Json(name = "scope") val scope: String,
)
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "access_token") val accessToken: String,
@Json(name = "expires_in") val expiresIn: Int,
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
@Json(name = "refresh_token") val refreshToken: String,
@Json(name = "token_type") val tokenType: String,
@Json(name = "not-before-policy") val notBeforePolicy: Int,
@Json(name = "session_state") val sessionState: String,
@Json(name = "scope") val scope: String,
)
}

View file

@ -4,11 +4,11 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardWalletDataToSignResponse(
@Json(name = "dataForCardWallet") val dataForCardWallet: Data,
data class VisaDataToSignResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Data(
data class Result(
@Json(name = "hash") val hash: String,
)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.appcurrency
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import kotlinx.coroutines.flow.Flow
/**
* Store of app currency data model [CurrenciesResponse.Currency]
*
[REDACTED_AUTHOR]
*/
interface AppCurrencyResponseStore {
/** Get flow of [CurrenciesResponse.Currency] */
fun get(): Flow<CurrenciesResponse.Currency?>
/** Get [CurrenciesResponse.Currency] synchronously or null */
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
}

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