Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-12 15:50:29 +03:00
commit cef36091da
379 changed files with 8948 additions and 8976 deletions

View file

@ -20,6 +20,9 @@ configurations.all {
substitute(module("com.facebook.react:hermes-engine"))
.using(module("com.facebook.react:hermes-android:0.72.4"))
substitute(module("org.bouncycastle:bcprov-jdk15on"))
.using(module("org.bouncycastle:bcprov-jdk18on:1.73"))
}
force(

@ -1 +1 @@
Subproject commit 4a1be08512fe2b45504f2bdaa0e35f5c11e11319
Subproject commit a13406537ec4b05f930131b151ea6ebb1c0b5ca3

View file

@ -434,6 +434,16 @@
}
]
},
{
"id" : "algorand",
"symbol" : "ALGO",
"name" : "Algorand",
"networks" : [
{
"networkId" : "algorand/test"
}
]
},
{
"id" : "arbitrum-one",
"symbol" : "ETH",
@ -569,6 +579,16 @@
}
]
},
{
"id": "shibarium",
"name": "Shibarium",
"symbol": "BONE",
"networks": [
{
"networkId": "shibarium/test"
}
]
},
{
"id": "aptos",
"name": "Aptos",

View file

@ -10,6 +10,7 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -28,7 +29,6 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -37,10 +37,8 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
@ -118,9 +116,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var scanCardProcessor: ScanCardProcessor
@Inject
lateinit var blockchainExceptionHandler: BlockchainExceptionHandler
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@ -155,10 +150,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
lateinit var oneTimeEventFilter: OneTimeEventFilter
@Inject
lateinit var derivationsRepository: DerivationsRepository
@Inject
lateinit var testerFeatureToggles: TesterFeatureToggles
lateinit var blockchainDataStorage: BlockchainDataStorage
// endregion Injected
override fun onCreate() {
@ -234,8 +226,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
balanceHidingRepository = balanceHidingRepository,
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
derivationsRepository = derivationsRepository,
testerFeatureToggles = testerFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
),
),
)

View file

@ -33,7 +33,6 @@ import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -91,7 +90,6 @@ data class AppState(
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware.tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,

View file

@ -8,8 +8,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -62,14 +60,8 @@ internal object CardDomainModule {
@Provides
@ViewModelScoped
fun provideDerivePublicKeysUseCase(
tangemSdkManager: TangemSdkManager,
derivationsRepository: DerivationsRepository,
): DerivePublicKeysUseCase {
return DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = derivationsRepository,
)
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
}
@Provides

View file

@ -99,6 +99,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
dispatchers: CoroutineDispatcherProvider,
@ -109,6 +110,7 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,

View file

@ -1,18 +1,34 @@
package com.tangem.tap.di.domain
import com.tangem.domain.visa.GetVisaCurrencyUseCase
import com.tangem.domain.visa.GetVisaTxDetailsUseCase
import com.tangem.domain.visa.GetVisaTxHistoryUseCase
import com.tangem.domain.visa.repository.VisaRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object VisaDomainModule {
@Provides
@ViewModelScoped
fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase {
return GetVisaCurrencyUseCase(visaRepository)
}
@Provides
@ViewModelScoped
fun provideGetVisaTxHistoryUseCase(visaRepository: VisaRepository): GetVisaTxHistoryUseCase {
return GetVisaTxHistoryUseCase(visaRepository)
}
@Provides
@ViewModelScoped
fun provideGetVisaTxDetailsUseCase(visaRepository: VisaRepository): GetVisaTxDetailsUseCase {
return GetVisaTxDetailsUseCase(visaRepository)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
@ -25,6 +26,7 @@ internal object WalletManagersFacadeModule {
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
@ -33,6 +35,7 @@ internal object WalletManagersFacadeModule {
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
configManager = configManager,
blockchainDataStorage = blockchainDataStorage,
assetReader = assetReader,
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),

View file

@ -5,19 +5,20 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -40,8 +41,12 @@ class TapWalletManager(
field = value
}
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
val walletManagerFactory: WalletManagerFactory by lazy {
WalletManagerFactory(
config = blockchainSdkConfig,
blockchainDataStorage = store.state.daggerGraphState.get(DaggerGraphState::blockchainDataStorage),
)
}
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,

View file

@ -1,41 +0,0 @@
package com.tangem.tap.domain.card
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.TangemSdkManager
internal class DefaultDerivePublicKeysUseCase(
private val tangemSdkManager: TangemSdkManager,
private val derivationsRepository: DerivationsRepository,
) : DerivePublicKeysUseCase {
override suspend fun invoke(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Either<Unit, DerivationTaskResponse> {
tangemSdkManager.derivePublicKeys(cardId = cardId, derivations = derivations)
.doOnSuccess { return it.right() }
.doOnFailure { return Unit.left() }
return Unit.left()
}
override suspend fun invoke(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
return Either.catch {
derivationsRepository.derivePublicKeys(userWalletId, currencies)
}
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.customtoken.impl.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
@ -30,7 +29,6 @@ internal object CustomTokenInteractorModule {
reduxStateHolder: AppStateHolder,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
testerFeatureToggles: TesterFeatureToggles,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
@ -40,7 +38,6 @@ internal object CustomTokenInteractorModule {
),
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
testerFeatureToggles = testerFeatureToggles,
)
}
}

View file

@ -1,40 +1,19 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import timber.log.Timber
/**
@ -48,7 +27,6 @@ class DefaultCustomTokenInteractor(
private val featureRepository: CustomTokenRepository,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val testerFeatureToggles: TesterFeatureToggles,
) : CustomTokenInteractor {
// TODO: Move to DI
@ -70,136 +48,12 @@ class DefaultCustomTokenInteractor(
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
val currency = Currency.fromCustomCurrency(customCurrency)
if (testerFeatureToggles.isDerivePublicKeysRefactoringEnabled) {
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
} else {
submitAdd(userWallet, currency)
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
}
}
private fun isNeedToDerive(userWallet: UserWallet, currency: Currency): Boolean {
val scanResponse = userWallet.scanResponse
return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false
}
private suspend fun deriveMissingBlockchains(
userWallet: UserWallet,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
onFailure: suspend (TangemError) -> Unit,
) {
val scanResponse = userWallet.scanResponse
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
onFailure.invoke(result.error)
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: Currency,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private suspend fun submitAdd(userWallet: UserWallet, currency: Currency) {
val scanResponse = userWallet.scanResponse
val userWalletId = userWallet.walletId
val currencyList = listOfNotNull(element = currency.toCryptoCurrency(scanResponse))
userWalletsListManager.update(userWalletId) {
it.copy(scanResponse = scanResponse)
}
addCryptoCurrenciesUseCase(userWalletId, currencyList)
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {

View file

@ -3,21 +3,21 @@ package com.tangem.tap.features.customtoken.impl.presentation.validators
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressService
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.tokens.error.AddCustomTokenError
/**
* Validator of contract address
*
[REDACTED_AUTHOR]
*/
object ContactAddressValidator {
object ContractAddressValidator {
/** Validate a [address] using [blockchain] */
fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
return when {
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FieldIsEmpty)
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FIELD_IS_EMPTY)
validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.InvalidContractAddress)
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.INVALID_CONTRACT_ADDRESS)
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.customtoken.impl.presentation.validators
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.tokens.error.AddCustomTokenError
/**
* Result of validation contract address

View file

@ -16,13 +16,13 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.HDWalletError
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.*
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.error.AddCustomTokenError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
@ -33,7 +33,7 @@ import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTok
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField.SelectorItem
import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidator
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -461,11 +461,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
val isContractAddressFieldEmpty = ContactAddressValidator.validate(
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
address = uiState.form.contractAddressInputField.value,
blockchain = networkSelectorValue,
).let {
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FieldIsEmpty
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
}
val isSupportedToken = if (!isNetworkSelected()) {
@ -513,7 +513,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
val state = when {
isAllTokenFieldsFilled() && isNetworkSelected() -> {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
val error = ContactAddressValidator.validate(
val error = ContractAddressValidator.validate(
address = uiState.form.contractAddressInputField.value,
blockchain = networkSelectorValue,
)
@ -621,7 +621,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
when {
isNetworkSelected() && type == AddCustomTokenError.InvalidContractAddress -> {
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
uiState = uiState.copySealed(
form = uiState.form.copy(
@ -644,7 +644,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
)
}
!isNetworkSelected() || type == AddCustomTokenError.FieldIsEmpty -> {
!isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> {
uiState = uiState.copySealed(
form = uiState.form.copy(
contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false),
@ -727,7 +727,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
)
val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
val validatorResult = ContactAddressValidator.validate(
val validatorResult = ContractAddressValidator.validate(
address = enteredValue,
blockchain = selectedNetwork,
)

View file

@ -170,7 +170,7 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
.padding(horizontal = TangemTheme.dimens.spacing16)
.align(Alignment.BottomCenter)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AnimatedVisibility(
visible = config.currentStory == Stories.Currencies,

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -13,15 +12,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@Suppress("MagicNumber")
@Composable
internal fun HomeButtons(
btnScanStateInProgress: Boolean,
@ -38,7 +33,7 @@ internal fun HomeButtons(
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)
SpacerW8()
SpacerW12()
OrderCardButton(
modifier = Modifier.weight(weight = 1f),
onClick = onShopButtonClick,
@ -48,44 +43,26 @@ internal fun HomeButtons(
@Composable
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.home_button_scan),
useDarkerColors = false,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
colors = LightBgScanCardButtonColors,
showProgress = showProgress,
enabled = true,
onClick = onClick,
showProgress = showProgress,
)
}
@Composable
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.home_button_order),
icon = TangemButtonIconPosition.None,
colors = LightBgOrderCardButtonColors,
showProgress = false,
enabled = true,
useDarkerColors = true,
onClick = onClick,
)
}
private val LightBgScanCardButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledBackgroundColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.Dark6,
)
private val LightBgOrderCardButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Dark6,
contentColor = TangemColorPalette.White,
disabledBackgroundColor = TangemColorPalette.Dark6,
disabledContentColor = TangemColorPalette.White,
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
@ -95,10 +72,10 @@ private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::c
modifier = Modifier.background(Color.Black),
) {
HomeButtons(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
btnScanStateInProgress = state.btnScanStateInProgress,
onScanButtonClick = {},
onShopButtonClick = {},
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
)
}
}

View file

@ -1,31 +1,42 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.material.ButtonColors
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@Composable
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.common_search_tokens),
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
onClick = onClick,
colors = SearchCurrenciesButtonColors,
showProgress = false,
enabled = true,
useDarkerColors = true,
onClick = onClick,
)
}
private val SearchCurrenciesButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Dark5,
contentColor = TangemColorPalette.White,
disabledBackgroundColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.White,
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SearchCurrenciesButtonPreview() {
TangemTheme {
Box(
modifier = Modifier
.background(color = Color.Black)
.padding(all = TangemTheme.dimens.spacing16),
) {
SearchCurrenciesButton(modifier = Modifier.fillMaxWidth(), onClick = {})
}
}
}
// endregion Preview

View file

@ -0,0 +1,51 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun StoriesButton(
text: String,
useDarkerColors: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
showProgress: Boolean = false,
) {
TangemButton(
modifier = modifier,
text = text,
icon = icon,
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
showProgress = showProgress,
enabled = true,
shape = TangemTheme.shapes.roundedCornersXMedium,
iconPadding = when (icon) {
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
is TangemButtonIconPosition.End,
is TangemButtonIconPosition.None,
-> TangemTheme.dimens.spacing8
},
onClick = onClick,
)
}
private val LighterButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledBackgroundColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.Dark6,
)
private val DarkerButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Dark4,
contentColor = TangemColorPalette.White,
disabledBackgroundColor = TangemColorPalette.Dark4,
disabledContentColor = TangemColorPalette.White,
)

View file

@ -13,10 +13,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.delay
@ -30,7 +30,7 @@ fun StoriesProgressBar(
stepDuration: Int = 8_000,
onStepFinish: () -> Unit = {},
) {
val progress = remember(currentStep) { Animatable(0f) }
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
val context = LocalContext.current
val animatorSpeed = Settings.Global.getFloat(
@ -75,20 +75,21 @@ fun StoriesProgressBar(
.height(TangemTheme.dimens.size2)
.weight(1f)
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
.background(Color.White.copy(alpha = 0.4f)),
.background(TangemColorPalette.White.copy(alpha = .2f)),
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
.background(Color.White)
.fillMaxHeight().let {
.background(TangemColorPalette.White)
.fillMaxHeight()
.let {
when (index) {
currentStep -> it.fillMaxWidth(progress.value)
in 0..currentStep -> it.fillMaxWidth(1f)
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
else -> it
}
},
) {}
)
}
if (index != steps) {
SpacerW4()
@ -100,5 +101,12 @@ fun StoriesProgressBar(
@Preview
@Composable
private fun StoriesProgressBarPreview() {
StoriesProgressBar(steps = 3, currentStep = 2, paused = false) { }
Box(
modifier = Modifier
.wrapContentSize()
.background(TangemColorPalette.Black)
.padding(vertical = TangemTheme.dimens.spacing16),
) {
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
}
}

View file

@ -83,7 +83,9 @@ internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modi
Icon(
painter = painterResource(id = model.iconResId.value),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.clip(CircleShape),
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
)

View file

@ -80,6 +80,13 @@ object TokenListPreviewData {
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
isMainNetwork = false,
),
// check icon clipping
NetworkItemState.ReadContent(
name = "SHIBARIUM",
protocolName = "BEP20",
iconResId = mutableStateOf(R.drawable.ic_shibarium_22),
isMainNetwork = false,
),
)
}
}

View file

@ -3,14 +3,20 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import timber.log.Timber
import kotlin.properties.Delegates
@ -24,6 +30,8 @@ import kotlin.properties.Delegates
internal class TokensListMigration(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
) {
private var currentNewCoins: List<CryptoCurrency.Coin> by Delegates.notNull()
@ -76,31 +84,72 @@ internal class TokensListMigration(
}
}
fun onSaveButtonClick(
suspend fun onSaveButtonClick(
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
store.dispatch(
action = TokensAction.SaveChanges(
currentTokens = currentNewTokens,
currentCoins = currentNewCoins,
changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
userWallet = currentUserWallet,
),
val changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
}
val changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
}
val blockchainsToAdd = changedCoins.filterNot(currentNewCoins::contains)
val blockchainsToRemove = currentNewCoins.filterNot(changedCoins::contains)
val tokensToAdd = changedTokens.filterNot(currentNewTokens::contains)
val tokensToRemove = currentNewTokens.filterNot { token -> changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
userWalletId = currentUserWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return
}
val currencyList = blockchainsToAdd + tokensToAdd
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -13,12 +13,14 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.supportedTokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -67,6 +69,8 @@ internal class TokensListViewModel @Inject constructor(
private val router: TokensListRouter,
private val dispatchers: AppCoroutineDispatcherProvider,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
analyticsEventHandler: AnalyticsEventHandler,
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
) : ViewModel(), DefaultLifecycleObserver {
@ -88,6 +92,8 @@ internal class TokensListViewModel @Inject constructor(
private val tokensListMigration = TokensListMigration(
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getCurrenciesUseCase = getCurrenciesUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
)
init {
@ -303,10 +309,13 @@ internal class TokensListViewModel @Inject constructor(
fun onSaveButtonClick() {
analyticsSender.sendWhenSaveButtonClicked()
tokensListMigration.onSaveButtonClick(
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
viewModelScope.launch(dispatchers.main) {
tokensListMigration.onSaveButtonClick(
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
}
}
private fun onSearchValueChange(newValue: String) {

View file

@ -1,250 +0,0 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.core.navigation.NavigationAction
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
object TokensMiddleware {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
next(action)
}
}
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
scope.launch {
val scanResponse = action.userWallet.scanResponse
val currentTokens = action.currentTokens
val currentBlockchains = action.currentCoins
val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
userWalletId = action.userWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = blockchainsToAdd + tokensToAdd
val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::testerFeatureToggles)
if (featureToggles.isDerivePublicKeysRefactoringEnabled) {
val derivePublicKeys = DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = store.state.daggerGraphState.get(DaggerGraphState::derivationsRepository),
)
derivePublicKeys(userWalletId = action.userWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(
userWalletId = action.userWallet.walletId,
currencies = currencyList,
)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitAdd(
userWallet = action.userWallet,
updatedScanResponse = it,
currencyList = currencyList,
)
}
} else {
submitAdd(action.userWallet, scanResponse, currencyList)
}
}
}
}
@Deprecated(message = "Use DerivePublicKeysUseCase instead")
private fun deriveMissingCoins(
scanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
onSuccess: (ScanResponse) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value))
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val blockchain = Blockchain.fromId(currency.network.id.value)
val supportedCurves = blockchain.getSupportedCurves()
val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.network.derivationPath.value?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(
userWallet: UserWallet,
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
scope.launch {
userWalletsListManager.update(
userWalletId = userWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
).doOnSuccess {
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList)
}
}
store.dispatchOnMain(NavigationAction.PopBackTo())
}
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -46,6 +46,11 @@ object TradeCryptoMiddleware {
}
}
private val isSendRedesignedEnabled: Boolean
get() = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::sendFeatureToggles,
).isRedesignedSendEnabled
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
@ -57,8 +62,20 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.Swap -> openSwap(
currency = action.cryptoCurrency,
)
is TradeCryptoAction.SendToken -> handleSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
is TradeCryptoAction.SendToken -> {
if (isSendRedesignedEnabled) {
handleNewSendToken(action = action)
} else {
handleSendToken(action = action)
}
}
is TradeCryptoAction.SendCoin -> {
if (isSendRedesignedEnabled) {
handleNewSendCoin(action = action)
} else {
handleSendCoin(action = action)
}
}
}
}
@ -280,4 +297,35 @@ object TradeCryptoMiddleware {
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
txInfo = action.transactionInfo,
currency = action.tokenCurrency,
)
}
private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
txInfo = action.transactionInfo,
currency = action.coinStatus.currency,
)
}
private fun handleNewSend(
userWalletId: String,
txInfo: TradeCryptoAction.TransactionInfo?,
currency: CryptoCurrency,
) {
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to userWalletId,
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}

View file

@ -1,298 +0,0 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.lib.crypto.models.errors.UserCancelledException
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.tangem.tap.domain.model.Currency as WalletModelCurrency
class DerivationManagerImpl(
private val appStateHolder: AppStateHolder,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
) : DerivationManager {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun deriveAndAddTokens(currency: Currency) = suspendCoroutine { continuation ->
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "selectedUserWallet shouldn't be null" }
val scanResponse = selectedUserWallet.scanResponse
val blockchain = requireNotNull(
Blockchain.fromNetworkId(currency.networkId),
) { "unsupported blockchain" }
val derivationStyleProvider = scanResponse.derivationStyleProvider
val derivationPath = requireNotNull(
blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath,
) { "derivationPath shouldn't be null" }
val hasDerivation = scanResponse.hasDerivation(
blockchain,
derivationPath,
)
if (hasDerivation) {
scope.launch {
addToken(
userWalletId = selectedUserWallet.walletId,
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
continuation.resumeWith(Result.success(derivationPath))
}
} else {
val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider)
val appCurrency = com.tangem.tap.domain.model.Currency.fromBlockchainNetwork(
blockchainNetwork,
getAppToken(currency),
)
deriveMissingBlockchains(
scanResponse = scanResponse,
currencyList = listOf(appCurrency),
onSuccess = { updatedScanResponse ->
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
addToken(
userWalletId = selectedUserWallet.walletId,
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
continuation.resumeWith(Result.success(derivationPath))
}
},
onFailure = {
continuation.resumeWith(Result.failure(it))
},
)
}
}
private suspend fun addToken(
userWalletId: UserWalletId,
blockchain: Blockchain,
currency: Currency,
derivationPath: String,
derivationStyleProvider: DerivationStyleProvider,
) {
val cryptoCurrency = convertCurrency(
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
addCryptoCurrenciesUseCase(userWalletId, cryptoCurrency)
}
private fun convertCurrency(
blockchain: Blockchain,
currency: Currency,
derivationPath: String,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency {
val cryptoCurrencyFactory = CryptoCurrencyFactory()
return when (currency) {
is Currency.NativeToken -> {
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
}
is NonNativeToken -> {
val sdkToken = Token(
name = currency.name,
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimalCount,
id = currency.id,
)
cryptoCurrencyFactory.createToken(
sdkToken = sdkToken,
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
}
} as CryptoCurrency
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<WalletModelCurrency>,
onSuccess: (ScanResponse) -> Unit,
onFailure: (Exception) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
cardId = null, // always ignore cardId in derive task
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
if (selectedUserWallet != null) {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
}
appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
appStateHolder.mainStore?.dispatchDebugErrorNotification(
TapError.CustomError(
"Error derivation",
),
)
onFailure.invoke(handleTangemError(result.error))
}
else -> {
error("result result is null")
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: com.tangem.tap.domain.model.Currency,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is WalletModelCurrency.Blockchain &&
currency.blockchain == Blockchain.Cardano
) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private fun getAppToken(currency: Currency): Token? {
return if (currency is NonNativeToken) {
Token(
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimalCount,
)
} else {
null
}
}
/**
* Simple error handler
* for now specifically handle only UserCancelled
*
* @param error [TangemError]
*/
private fun handleTangemError(error: TangemError): Exception {
if (error is TangemSdkError.UserCancelled) {
return UserCancelledException()
}
return IllegalStateException(error.customMessage)
}
}

View file

@ -4,9 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.*
@ -56,18 +54,4 @@ internal object ProxyModule {
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideDerivationManager(
appStateHolder: AppStateHolder,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
): DerivationManager {
return DerivationManagerImpl(
appStateHolder = appStateHolder,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.proxy.redux
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -7,7 +8,6 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -17,7 +17,6 @@ import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggle
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -52,11 +51,8 @@ data class DaggerGraphState(
val sendFeatureToggles: SendFeatureToggles? = null,
val sendRouter: SendRouter? = null,
val qrScanningRouter: QrScanningRouter? = null,
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
val currenciesRepository: CurrenciesRepository? = null,
val derivationsRepository: DerivationsRepository? = null,
val testerFeatureToggles: TesterFeatureToggles? = null,
val blockchainDataStorage: BlockchainDataStorage? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -125,6 +125,7 @@ sealed class AnalyticsParam {
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val TYPE = "Type"
const val FEE_TYPE = "Fee Type"
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"

View file

@ -33,7 +33,6 @@ dependencies {
implementation(deps.timber)
/** Network */
implementation(deps.krateSharedPref)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)

View file

@ -7,7 +7,7 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class BigDecimalAdapter {
internal class BigDecimalAdapter {
@FromJson
fun fromJson(value: String) = BigDecimal(value)

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.*
import org.joda.time.DateTime
internal class DateTimeAdapter : JsonAdapter<DateTime>() {
@FromJson
override fun fromJson(reader: JsonReader): DateTime? {
val dateString = reader.nextString() ?: return null
return DateTime.parse(dateString)
}
@ToJson
override fun toJson(writer: JsonWriter, value: DateTime?) {
if (value != null) {
writer.value(value.toString())
} else {
writer.nullValue()
}
}
}

View file

@ -1,14 +1,10 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import com.squareup.moshi.*
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat
class LocalDateAdapter : JsonAdapter<LocalDate>() {
internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")

View file

@ -6,7 +6,7 @@ import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
internal class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {

View file

@ -145,6 +145,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
jsonRpc = accessTokens.bitcoin?.jsonRPC,
blockBookRest = accessTokens.bitcoin?.blockBookRest,
),
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
)
}
}

View file

@ -66,6 +66,7 @@ data class GetBlockAccessTokens(
@Json(name = "litecoin") val litecoin: GetBlockToken?,
@Json(name = "dash") val dash: GetBlockToken?,
@Json(name = "bitcoin") val bitcoin: GetBlockToken?,
@Json(name = "algorand") val algorand: GetBlockToken?,
)
@JsonClass(generateAdapter = true)

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.AppPreferenceStorage
import com.tangem.datasource.local.AppPreferenceStorageImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppPreferenceStorageModule {
@Binds
@Singleton
fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
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 BlockchainDataStorageModule {
@Provides
@Singleton
fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage {
return DefaultBlockchainDataStorage(appPreferencesStore)
}
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.DateTimeAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import dagger.Module
import dagger.Provides
@ -22,6 +23,7 @@ class MoshiModule {
return Moshi.Builder()
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}

View file

@ -1,13 +0,0 @@
package com.tangem.datasource.local
/**
* Application local storage
*
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AppPreferencesStore", level = DeprecationLevel.WARNING)
interface AppPreferenceStorage {
/** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */
var featureToggles: String
}

View file

@ -1,24 +0,0 @@
package com.tangem.datasource.local
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import hu.autsoft.krate.SimpleKrate
import hu.autsoft.krate.default.withDefault
import hu.autsoft.krate.stringPref
import javax.inject.Inject
import javax.inject.Singleton
/**
* Implementation of application local storage
*
* @param context application context
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class AppPreferenceStorageImpl @Inject constructor(
@ApplicationContext context: Context,
) : SimpleKrate(context = context), AppPreferenceStorage {
override var featureToggles: String by stringPref().withDefault("")
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.local.blockchain
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
/**
* [BlockchainDataStorage] implementation
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultBlockchainDataStorage(
private val appPreferencesStore: AppPreferencesStore,
) : BlockchainDataStorage {
override suspend fun getOrNull(key: String): String? {
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
}
override suspend fun store(key: String, value: String) {
appPreferencesStore.edit {
it[stringPreferencesKey(key)] = value
}
}
}

View file

@ -52,6 +52,8 @@ object PreferencesKeys {
val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
}
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -7,14 +7,22 @@ plugins {
}
dependencies {
implementation(project(":core:datasource"))
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Local storages */
implementation(deps.androidx.datastore)
/** Other libraries */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
/** Core modules */
implementation(projects.core.datasource)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)

View file

@ -14,13 +14,5 @@
{
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "undefined"
},
{
"name": "WALLETS_SCROLLING_PREVIEW_ENABLED",
"version": "5.5.0"
},
{
"name": "DERIVE_PUBLIC_KEYS_REFACTORING_ENABLED",
"version": "5.5.0"
}
]

View file

@ -11,7 +11,7 @@ import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.local.AppPreferenceStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -29,7 +29,7 @@ internal object FeatureTogglesManagerModule {
fun provideFeatureTogglesManager(
@ApplicationContext context: Context,
assetReader: AssetReader,
appPreferenceStorage: AppPreferenceStorage,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
@ -41,8 +41,7 @@ internal object FeatureTogglesManagerModule {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
appPreferenceStorage = appPreferenceStorage,
jsonAdapter = moshi.adapter(),
appPreferencesStore = appPreferencesStore,
versionProvider = versionProvider,
)
} else {

View file

@ -1,25 +1,25 @@
package com.tangem.core.featuretoggle.manager
import androidx.annotation.VisibleForTesting
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.datasource.local.AppPreferenceStorage
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.storeObject
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property appPreferenceStorage application local storage
* @property jsonAdapter adapter for parsing json
* @property appPreferencesStore application local store
* @property versionProvider application version provider
*/
internal class DevFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val appPreferenceStorage: AppPreferenceStorage,
private val jsonAdapter: JsonAdapter<Map<String, Boolean>>,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
@ -28,11 +28,9 @@ internal class DevFeatureTogglesManager(
override suspend fun init() {
localFeatureTogglesStorage.init()
val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) {
jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty()
} else {
emptyMap()
}
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
featureTogglesMap = localFeatureTogglesStorage.featureToggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
@ -46,10 +44,10 @@ internal class DevFeatureTogglesManager(
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override fun changeToggle(name: String, isEnabled: Boolean) {
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
appPreferencesStore.storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, featureTogglesMap)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)

View file

@ -11,5 +11,5 @@ interface MutableFeatureTogglesManager : FeatureTogglesManager {
fun getFeatureToggles(): Map<String, Boolean>
/** Change availability [isEnabled] of toggle with name [name] */
fun changeToggle(name: String, isEnabled: Boolean)
suspend fun changeToggle(name: String, isEnabled: Boolean)
}

View file

@ -1,5 +1,8 @@
package com.tangem.core.featuretoggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about feature toggle
*
@ -10,4 +13,8 @@ package com.tangem.core.featuretoggle.storage
*
[REDACTED_AUTHOR]
*/
internal data class FeatureToggle(val name: String, val version: String)
@JsonClass(generateAdapter = true)
internal data class FeatureToggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -2,48 +2,42 @@ package com.tangem.core.featuretoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.datasource.local.AppPreferenceStorage
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import io.mockk.verifyAll
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
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 io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.collections.set
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
@SuppressLint("CheckResult")
internal class DevFeatureTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val appPreferenceStorage = mockk<AppPreferenceStorage>(relaxed = true)
private val jsonAdapter = mockk<JsonAdapter<Map<String, Boolean>>>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
appPreferenceStorage = appPreferenceStorage,
jsonAdapter = jsonAdapter,
appPreferencesStore = appPreferenceStore,
versionProvider = versionProvider,
)
@Test
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
val currentVersion = "1.0.0"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns savedFeatureTogglesMap
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -51,7 +45,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(savedFeatureToggles)
versionProvider.get()
}
@ -66,12 +59,12 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
val currentVersion = "1.0.0"
val sharedPrefFeatureToggles = "[]"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns sharedPrefFeatureToggles
coEvery { jsonAdapter.fromJson(sharedPrefFeatureToggles) } returns savedFeatureTogglesMap
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns emptyMap()
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -79,7 +72,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(sharedPrefFeatureToggles)
versionProvider.get()
}
@ -94,10 +86,12 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
val currentVersion = "1.0.0"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns ""
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns null
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -107,7 +101,6 @@ internal class DevFeatureTogglesManagerTest {
localFeatureTogglesStorage.init()
versionProvider.get()
}
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
val expected = localFeatureToggles
.associateToggles(currentVersion)
@ -119,8 +112,7 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns null
@ -128,7 +120,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(savedFeatureToggles)
versionProvider.get()
}
@ -180,7 +171,7 @@ internal class DevFeatureTogglesManagerTest {
}
@Test
fun `change toggle that contains in map`() {
fun `change toggle that contains in map`() = runTest {
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
val resultMap = mutableMapOf(
changeableToggleName to false,
@ -188,18 +179,16 @@ internal class DevFeatureTogglesManagerTest {
)
manager.setFeatureToggles(resultMap)
coEvery { jsonAdapter.toJson(resultMap) } returns ""
manager.changeToggle(changeableToggleName, true)
resultMap[changeableToggleName] = true
verifyOrder { jsonAdapter.toJson(resultMap) }
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
}
@Test
fun `change toggle that doesn't contains in map`() {
fun `change toggle that doesn't contains in map`() = runTest {
val resultMap = mutableMapOf(
"INACTIVE_TEST_FEATURE_ENABLED" to false,
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
@ -209,8 +198,6 @@ internal class DevFeatureTogglesManagerTest {
manager.changeToggle("FEATURE_TOGGLE", true)
verifyAll(inverse = true) { jsonAdapter.toJson(any()) }
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
}

View file

@ -449,6 +449,7 @@
<string name="scan_card_settings_button">Сканировать</string>
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %s</string>

View file

@ -285,7 +285,6 @@
<string name="manage_tokens_network_selector_non_native_info">Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.</string>
<string name="manage_tokens_network_selector_non_native_subtitle">Not original or primary blockchain the token is hosted</string>
<string name="manage_tokens_network_selector_non_native_title">Non-native networks</string>
<string name="manage_tokens_network_selector_other_subtitle">Blockchain the cryptocurrency was initially created</string>
<string name="manage_tokens_network_selector_other_title">Networks</string>
<string name="manage_tokens_network_selector_title">Choose networks</string>
<string name="manage_tokens_network_selector_wallet">Wallet</string>
@ -448,6 +447,7 @@
<string name="scan_card_settings_button">Scan Card</string>
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %s</string>

View file

@ -1,11 +1,13 @@
package com.tangem.core.ui.components.buttons
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -51,12 +53,22 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie
@Composable
private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)
val backgroundColor by animateColorAsState(
targetValue = if (isPrimary) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary,
label = "Update background color",
)
val textColor by animateColorAsState(
targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
label = "Update text color",
)
Box(
modifier = modifier
.defaultMinSize(minWidth = TangemTheme.dimens.size46, minHeight = TangemTheme.dimens.size24)
.clip(shape)
.background(
color = if (isPrimary) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary,
color = backgroundColor,
shape = shape,
)
.clickable(enabled = true, onClick = config.onClick)
@ -66,8 +78,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing10,
),
text = config.text.resolveReference(),
color = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
color = textColor,
maxLines = 1,
style = TangemTheme.typography.button,
)
@ -97,7 +112,7 @@ private fun ButtonsSample() {
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
val config = SmallButtonConfig(
text = TextReference.Str(value = "Add"),
text = TextReference.Str(value = "Adddddddd"),
onClick = {},
)
PrimarySmallButton(config = config)

View file

@ -35,6 +35,7 @@ fun TangemButton(
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
shape: Shape = size.toShape(),
iconPadding: Dp = size.toIconPadding(),
) {
val multipleClickPreventer = remember { MultipleClickPreventer.get() }
@ -53,7 +54,7 @@ fun TangemButton(
ButtonContentContainer(
buttonIcon = icon,
iconPadding = size.toIconPadding(),
iconPadding = iconPadding,
showProgress = showProgress,
progressIndicator = {
CircularProgressIndicator(

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components.fields
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -54,6 +55,7 @@ fun AmountTextField(
keyboardOptions: KeyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
val decimalFormat = rememberDecimalFormat()
@ -75,6 +77,7 @@ fun AmountTextField(
textStyle = textStyle,
color = color,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = true,
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
decorationBox = { innerTextField ->

View file

@ -1,7 +1,9 @@
package com.tangem.core.ui.components.fields
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
@ -33,6 +35,7 @@ fun SimpleTextField(
singleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
color: Color = TangemTheme.colors.text.primary1,
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
readOnly: Boolean = false,
@ -42,26 +45,31 @@ fun SimpleTextField(
mutableStateOf(
TextFieldValue(
text = value,
selection = when {
value.isEmpty() -> TextRange.Zero
else -> TextRange(value.length, value.length)
},
selection = getValueRange(value),
),
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.secondary,
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
val textFieldValue = textFieldValueState.copy(text = value)
SideEffect {
if (textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
) {
textFieldValueState = textFieldValue
val isSelectionChanged by remember {
derivedStateOf {
textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition ||
textFieldValue.text != textFieldValueState.text
}
}
LaunchedEffect(key1 = isSelectionChanged) {
if (isSelectionChanged) {
textFieldValueState = textFieldValue.copy(
selection = getValueRange(value),
)
}
}
@ -86,20 +94,46 @@ fun SimpleTextField(
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = decorationBox ?: { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
}
textValue()
}
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
}
private fun getValueRange(value: String) = when {
value.isEmpty() -> TextRange.Zero
else -> TextRange(value.length, value.length)
}
@Composable
private fun SimpleTextPlaceholder(
placeholder: TextReference?,
value: String,
textStyle: TextStyle,
textValue: @Composable () -> Unit,
) {
Box {
if (value.isBlank() && placeholder != null) {
AnimatedContent(
targetState = placeholder,
label = "Placeholder Change Animation",
) {
Text(
text = it.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
}
}
textValue()
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
@ -51,6 +52,7 @@ fun InputRowEnterAmount(
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
@ -79,6 +81,7 @@ fun InputRowEnterAmount(
color = textColor,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
)

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -49,6 +50,7 @@ fun InputRowEnterInfoAmount(
textColor: Color = TangemTheme.colors.text.primary1,
infoColor: Color = TangemTheme.colors.text.tertiary,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
showDivider: Boolean = false,
) {
DividerContainer(
@ -74,6 +76,7 @@ fun InputRowEnterInfoAmount(
color = textColor,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState.Content
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import java.util.UUID
@ -46,6 +48,7 @@ import java.util.UUID
[REDACTED_AUTHOR]
*/
@Composable
@Suppress("LongMethod")
fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
Surface(
modifier = modifier
@ -76,10 +79,15 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
Title(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing6,
)
.constrainAs(titleItem) {
start.linkTo(iconItem.end)
end.linkTo(amountItem.start)
top.linkTo(iconItem.top)
width = Dimension.fillToConstraints
},
)
@ -173,11 +181,16 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) {
private fun Title(state: TransactionState, modifier: Modifier = Modifier) {
when (state) {
is TransactionState.Content -> {
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
) {
Text(
text = state.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (state.status is Status.Unconfirmed) {
@ -266,7 +279,7 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) {
when (state) {
is TransactionState.Content -> {
Text(
text = state.timestamp,
text = state.time,
modifier = modifier,
textAlign = TextAlign.End,
color = TangemTheme.colors.text.tertiary,
@ -321,89 +334,109 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider<
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "-0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = resourceReference(R.string.common_transfer),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Unconfirmed,
direction = Direction.INCOMING,
iconRes = R.drawable.ic_arrow_down_24,
title = resourceReference(R.string.common_transfer),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Unconfirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_doc_24,
title = resourceReference(R.string.common_approval),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Failed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_doc_24,
title = resourceReference(R.string.common_approval),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_doc_24,
title = resourceReference(R.string.common_approval),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Unconfirmed,
direction = Direction.INCOMING,
iconRes = R.drawable.ic_arrow_down_24,
title = resourceReference(R.string.common_swap),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.INCOMING,
iconRes = R.drawable.ic_doc_24,
title = TextReference.Str("Submit"),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = TextReference.Str("Submit"),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "0.625 USDT",
time = "€0.50",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = TextReference.Str("Unlimint Banking Cards Sandbox London"),
subtitle = stringReference(value = "8:41 • authorized"),
timestamp = 0L,
onClick = {},
),
TransactionState.Loading(txHash = UUID.randomUUID().toString()),

View file

@ -25,13 +25,14 @@ sealed interface TransactionState {
data class Content(
override val txHash: String,
val amount: String,
val timestamp: String,
val time: String,
val status: Status,
val direction: Direction,
val onClick: () -> Unit,
@DrawableRes val iconRes: Int,
val title: TextReference,
val subtitle: TextReference,
val timestamp: Long,
) : TransactionState {
sealed class Status {

View file

@ -49,6 +49,8 @@ fun getActiveIconRes(blockchainId: String): Int {
"xdc", "xdc/test" -> R.drawable.img_xdc_22
"vechain", "vechain/test" -> R.drawable.img_vechain_22
"aptos", "aptos/test" -> R.drawable.img_aptos_22
"shibarium", "shibarium/test" -> R.drawable.img_shibarium_22
"algorand", "algorand/test" -> R.drawable.img_algorand_22
else -> R.drawable.ic_alert_24
}
}
@ -99,6 +101,8 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22
"vechain", "vechain/test" -> R.drawable.img_vechain_22
"aptos", "aptos/test" -> R.drawable.img_aptos_22
"shibarium", "shibarium/test" -> R.drawable.img_shibarium_22
"algorand", "algorand/test" -> R.drawable.img_algorand_22
else -> R.drawable.ic_alert_24
}
}
@ -146,6 +150,8 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"xdce-crowd-sale" -> R.drawable.img_xdc_22
"vechain" -> R.drawable.img_vechain_22
"aptos" -> R.drawable.img_aptos_22
"shibarium" -> R.drawable.img_shibarium_22
"algorand" -> R.drawable.img_algorand_22
else -> R.drawable.ic_alert_24
}
}
@ -196,6 +202,8 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"xdc", "xdc/test" -> R.drawable.ic_xdc_22
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
"shibarium", "shibarium/test" -> R.drawable.ic_shibarium_22
"algorand", "algorand/test" -> R.drawable.ic_algorand_22
else -> R.drawable.ic_alert_24
}
}
@ -246,6 +254,8 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
"shibarium", "shibarium/test" -> R.drawable.ic_shibarium_22
"algorand", "algorand/test" -> R.drawable.ic_algorand_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -55,6 +55,10 @@ object DateTimeFormatters {
.withLocale(Locale.getDefault())
}
val dateTimeFormatter: DateTimeFormatter by lazy {
DateTimeFormat.forPattern("dd.MM.yyyy HH:mm")
}
fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String {
return formatter.print(time)
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M6.908,16L8.504,13.245L10.101,10.5L11.688,7.745L11.95,7.309L12.067,7.745L12.554,9.559L12.009,10.5L10.412,13.245L8.826,16H10.734L12.33,13.245L13.158,11.819L13.547,13.245L14.287,16H16L15.26,13.245L14.52,10.5L14.326,9.792L15.513,7.745H13.781L13.722,7.541L13.119,5.291L13.041,5H11.376L11.337,5.058L9.78,7.745L8.183,10.5L6.596,13.245L5,16H6.908Z" />
</vector>

View file

@ -0,0 +1,57 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:strokeWidth="1"
android:pathData="M14.261,15.678C14.261,15.678 12.348,17.54 8.915,17.796C5.848,17.796 4.032,15.581 4.032,15.581C3.151,14.831 2.588,14.067 2.406,13.373C2.347,13.151 2.338,12.983 2.357,12.63L2.355,12.584C2.428,11.406 2.735,10.298 3.302,9.149C3.312,9.132 3.321,9.113 3.329,9.096C3.221,8.777 2.934,7.893 2.701,6.888C2.345,5.369 2.273,3.575 2.959,3.041C2.959,3.041 3.959,2.954 5.52,4.431C5.941,4.833 6.323,5.271 6.666,5.739C6.688,5.731 6.71,5.724 6.734,5.714C7.343,5.503 7.878,5.398 8.577,5.352C8.808,5.337 9.517,5.337 9.707,5.352C10.337,5.393 10.963,5.515 11.566,5.712L11.671,5.746C12.002,5.276 12.375,4.836 12.781,4.429C14.281,2.959 15.401,3.041 15.401,3.041C16.048,3.557 15.836,5.276 15.503,6.746C15.257,7.83 14.872,8.003 14.872,8.003L15,8L16.348,8.023L18.348,5.746H19.5M14.261,15.678H15.503L16.848,17.04H19.848M14.261,15.678C14.754,15.285 15.791,14 15.2,12"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M13.848,10.04L17.5,10L19.348,8.04H21.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M15,14.04H18.348L19.348,15.04H21.348"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M13.348,12.04H18.348H19.848"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M10.988,14.91C10.988,14.917 10.958,15.034 10.922,15.173C10.885,15.311 10.856,15.423 10.856,15.428C10.83,15.431 10.805,15.433 10.778,15.431H10.7L10.593,15.684C10.535,15.823 10.481,15.952 10.474,15.971L10.459,16.005L10.408,15.922L10.357,15.84V15.165L10.338,15.17C10.296,15.18 10.002,15.219 9.873,15.231C9.34,15.285 8.804,15.273 8.273,15.197C8.188,15.185 8.115,15.175 8.113,15.177C8.11,15.18 8.113,15.336 8.118,15.528L8.125,15.874L8.084,15.937C8.062,15.971 8.04,16 8.04,16.003C8.032,16.01 8.008,15.971 7.952,15.859C7.891,15.742 7.847,15.618 7.816,15.489L7.799,15.418L7.723,15.423L7.648,15.431L7.628,15.341C7.619,15.292 7.606,15.217 7.602,15.175L7.594,15.097L7.531,15.041C7.494,15.01 7.458,14.978 7.451,14.973C7.438,14.963 7.434,14.946 7.434,14.932V14.903L7.738,14.905L8.042,14.907L8.052,14.939L8.062,14.971L8.166,14.976C8.225,14.978 8.41,14.983 8.575,14.988L8.88,14.995L8.957,14.873L9.038,14.752H9.14L9.138,14.498L9.135,14.245L9.001,14.187C8.57,13.997 8.317,13.78 8.222,13.517C8.203,13.464 8.203,13.444 8.198,13.201C8.195,12.96 8.195,12.938 8.213,12.884C8.247,12.763 8.342,12.668 8.463,12.636C8.505,12.624 8.614,12.624 9.189,12.624L9.865,12.626L9.941,12.663C10.031,12.707 10.072,12.738 10.123,12.807C10.182,12.884 10.199,12.945 10.199,13.094C10.199,13.306 10.184,13.488 10.163,13.561C10.131,13.661 10.082,13.756 10.021,13.841C9.9,13.997 9.678,14.153 9.476,14.223L9.418,14.245L9.42,14.501L9.422,14.756L9.476,14.761L9.53,14.766L9.603,14.876L9.673,14.985H9.999C10.179,14.985 10.338,14.988 10.352,14.99C10.377,14.995 10.382,14.993 10.406,14.951L10.435,14.907H10.715C10.924,14.898 10.988,14.903 10.988,14.91Z"
android:fillColor="#000000"/>
<path
android:pathData="M9.702,15.608C9.656,15.635 9.624,15.647 9.617,15.642C9.61,15.64 9.576,15.611 9.539,15.584L9.473,15.531L9.405,15.604C9.257,15.764 9.252,15.771 9.208,15.774C9.14,15.781 9.125,15.769 9.043,15.645C8.999,15.582 8.965,15.528 8.965,15.528C8.965,15.528 8.933,15.523 8.897,15.521L8.828,15.514L8.797,15.579L8.765,15.645L8.709,15.628C8.665,15.613 8.621,15.596 8.58,15.577L8.507,15.54V15.401L9.155,15.404L9.802,15.406L9.805,15.472C9.807,15.55 9.809,15.548 9.702,15.608Z"
android:fillColor="#000000"/>
<path
android:pathData="M15.014,4.569C15.002,4.566 14.99,4.564 14.978,4.561L15.014,4.569Z"
android:fillColor="#000000"/>
<path
android:pathData="M3.001,4.561C2.988,4.564 2.974,4.566 2.962,4.569L3.001,4.561Z"
android:fillColor="#000000"/>
<path
android:pathData="M15.769,12.39C15.764,12.281 15.754,12.127 15.747,12.052C15.642,10.947 15.36,10.043 14.824,9.087C14.802,9.05 14.783,9.014 14.771,8.985C14.767,8.979 14.765,8.974 14.763,8.97L14.761,8.965C14.837,8.739 15.146,7.792 15.392,6.708C15.725,5.238 15.937,3.519 15.289,3.003C15.289,3.003 14.17,2.921 12.67,4.391C12.263,4.797 11.891,5.238 11.56,5.708L11.455,5.674C10.851,5.477 10.226,5.355 9.595,5.314C9.405,5.299 8.697,5.299 8.466,5.314C7.767,5.36 7.232,5.464 6.623,5.676C6.599,5.686 6.577,5.693 6.555,5.701C6.212,5.233 5.829,4.795 5.408,4.393C3.848,2.916 2.687,3.001 2.687,3.001C2,3.534 2.234,5.331 2.589,6.85C2.823,7.855 3.11,8.739 3.217,9.058C3.21,9.075 3.2,9.094 3.191,9.111C2.623,10.26 2.317,11.368 2.244,12.546C2.879,12.453 4.629,12.349 5.941,13.761C5.941,13.761 6.808,11.472 9.145,11.472C11.482,11.472 11.981,13.761 11.981,13.761C13.529,12.483 15.197,12.524 15.781,12.585C15.779,12.536 15.774,12.463 15.769,12.39L15.769,12.39V12.39ZM3.677,8.14C3.677,8.14 2.767,5.864 3.001,4.564C3.04,4.345 3.113,4.152 3.227,4.006C3.227,4.006 4.259,4.109 5.924,5.959C5.924,5.959 5.608,6.112 5.184,6.438C5.184,6.438 5.182,6.441 5.179,6.441C4.719,6.794 4.13,7.354 3.677,8.14ZM7.541,11.521C7.526,11.55 7.453,11.582 7.37,11.594C7.288,11.606 7.034,11.599 6.891,11.582C6.375,11.514 5.827,11.356 5.428,11.161C5.204,11.051 5.043,10.942 4.924,10.823L4.856,10.752L4.848,10.659C4.836,10.499 4.839,10.246 4.858,10.156C4.87,10.08 4.897,10.007 4.933,9.941C4.953,9.917 4.953,9.917 5.041,9.917C5.148,9.917 5.238,9.934 5.384,9.98C5.683,10.073 6.129,10.297 6.672,10.623C7.12,10.891 7.288,11.022 7.417,11.202C7.504,11.314 7.563,11.472 7.541,11.521ZM13.276,10.715C13.269,10.754 13.154,10.871 13.04,10.956C12.704,11.202 12.103,11.431 11.492,11.541C11.144,11.604 10.795,11.614 10.691,11.563C10.623,11.528 10.613,11.502 10.637,11.414C10.688,11.231 10.854,11.032 11.112,10.844C11.243,10.75 11.776,10.426 12.025,10.289C12.434,10.065 12.765,9.932 12.989,9.9C13.059,9.89 13.145,9.888 13.171,9.9C13.215,9.917 13.264,10.046 13.283,10.202C13.293,10.285 13.288,10.65 13.276,10.715ZM12.872,6.438L12.867,6.436C12.458,6.11 12.151,5.956 12.151,5.956C13.76,4.106 14.756,4.004 14.756,4.004C14.866,4.152 14.936,4.342 14.975,4.561C15.202,5.861 14.323,8.14 14.323,8.14C13.955,7.485 13.461,6.906 12.872,6.438ZM11.862,9.403C11.862,9.403 11.083,9.452 11.18,8.843C11.277,8.235 11.886,8.162 12.056,8.186C12.227,8.21 12.908,8.454 12.787,8.965C12.686,9.39 12.551,9.394 12.452,9.398C12.432,9.399 12.414,9.399 12.397,9.403C12.3,9.428 11.862,9.403 11.862,9.403ZM5.435,8.843C5.337,9.452 6.116,9.403 6.116,9.403C6.116,9.403 6.555,9.428 6.652,9.403C6.668,9.399 6.687,9.399 6.707,9.398C6.806,9.394 6.94,9.39 7.041,8.965C7.163,8.454 6.482,8.21 6.311,8.186C6.141,8.162 5.532,8.235 5.435,8.843Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M19.75,5.79m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#000000"/>
<path
android:pathData="M21.098,8.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#000000"/>
<path
android:pathData="M20.098,12.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#000000"/>
<path
android:pathData="M21.098,15.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#000000"/>
<path
android:pathData="M20.098,17.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#000000"/>
<path
android:pathData="M6.908,16L8.504,13.245L10.101,10.5L11.688,7.745L11.95,7.309L12.067,7.745L12.554,9.559L12.009,10.5L10.412,13.245L8.826,16H10.734L12.33,13.245L13.158,11.819L13.547,13.245L14.287,16H16L15.26,13.245L14.52,10.5L14.326,9.792L15.513,7.745H13.781L13.722,7.541L13.119,5.291L13.041,5H11.376L11.337,5.058L9.78,7.745L8.183,10.5L6.596,13.245L5,16H6.908Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -0,0 +1,90 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#202743"/>
<path
android:strokeWidth="1"
android:pathData="M19.848,17.04H16.848L15.503,15.678H14.261C14.261,15.678 12.348,17.54 8.915,17.796C5.848,17.796 4.032,15.581 4.032,15.581C3.151,14.831 2.588,14.067 2.406,13.373C2.347,13.151 2.338,12.983 2.357,12.63L2.355,12.584C2.428,11.406 2.735,10.298 3.302,9.149C3.312,9.132 3.321,9.113 3.329,9.096C3.221,8.777 2.934,7.893 2.701,6.888C2.345,5.369 2.273,3.575 2.959,3.041C2.959,3.041 3.959,2.954 5.52,4.431C5.941,4.833 6.323,5.271 6.666,5.739C6.688,5.731 6.71,5.724 6.734,5.714C7.343,5.503 7.878,5.398 8.577,5.352C8.808,5.337 9.517,5.337 9.707,5.352C10.337,5.393 10.963,5.515 11.566,5.712L11.671,5.746C12.002,5.276 12.375,4.836 12.781,4.429C14.281,2.959 15.401,3.041 15.401,3.041C16.048,3.557 15.836,5.276 15.503,6.746C15.257,7.83 14.872,8.003 14.872,8.003L14.882,8.023H16.348L18.348,5.746H19.5"
android:fillColor="#00000000"
android:strokeColor="#74BA40"/>
<path
android:strokeWidth="1"
android:pathData="M13.848,10.04L17.5,10L19.348,8.04H21.5"
android:fillColor="#00000000"
android:strokeColor="#74BA40"/>
<path
android:strokeWidth="1"
android:pathData="M13.848,14.04H18.348L19.348,15.04H21.348"
android:fillColor="#00000000"
android:strokeColor="#74BA40"/>
<path
android:strokeWidth="1"
android:pathData="M13.348,12.04H18.348H19.848"
android:fillColor="#00000000"
android:strokeColor="#74BA40"/>
<path
android:pathData="M10.988,14.91C10.988,14.917 10.958,15.034 10.922,15.173C10.885,15.311 10.856,15.423 10.856,15.428C10.83,15.431 10.805,15.433 10.778,15.431H10.7L10.593,15.684C10.535,15.823 10.481,15.952 10.474,15.971L10.459,16.005L10.408,15.922L10.357,15.84V15.165L10.338,15.17C10.296,15.18 10.002,15.219 9.873,15.231C9.34,15.285 8.804,15.273 8.273,15.197C8.188,15.185 8.115,15.175 8.113,15.177C8.11,15.18 8.113,15.336 8.118,15.528L8.125,15.874L8.084,15.937C8.062,15.971 8.04,16 8.04,16.003C8.032,16.01 8.008,15.971 7.952,15.859C7.891,15.742 7.847,15.618 7.816,15.489L7.799,15.418L7.723,15.423L7.648,15.431L7.628,15.341C7.619,15.292 7.606,15.217 7.602,15.175L7.594,15.097L7.531,15.041C7.494,15.01 7.458,14.978 7.451,14.973C7.438,14.963 7.434,14.946 7.434,14.932V14.903L7.738,14.905L8.042,14.907L8.052,14.939L8.062,14.971L8.166,14.976C8.225,14.978 8.41,14.983 8.575,14.988L8.88,14.995L8.957,14.873L9.038,14.752H9.14L9.138,14.498L9.135,14.245L9.001,14.187C8.57,13.997 8.317,13.78 8.222,13.517C8.203,13.464 8.203,13.444 8.198,13.201C8.195,12.96 8.195,12.938 8.213,12.884C8.247,12.763 8.342,12.668 8.463,12.636C8.505,12.624 8.614,12.624 9.189,12.624L9.865,12.626L9.941,12.663C10.031,12.707 10.072,12.738 10.123,12.807C10.182,12.884 10.199,12.945 10.199,13.094C10.199,13.306 10.184,13.488 10.163,13.561C10.131,13.661 10.082,13.756 10.021,13.841C9.9,13.997 9.678,14.153 9.476,14.223L9.418,14.245L9.42,14.501L9.422,14.756L9.476,14.761L9.53,14.766L9.603,14.876L9.673,14.985H9.999C10.179,14.985 10.338,14.988 10.352,14.99C10.377,14.995 10.382,14.993 10.406,14.951L10.435,14.907H10.715C10.924,14.898 10.988,14.903 10.988,14.91Z"
android:fillColor="#202743"/>
<path
android:pathData="M9.702,15.608C9.656,15.635 9.624,15.647 9.617,15.642C9.61,15.64 9.576,15.611 9.539,15.584L9.473,15.531L9.405,15.604C9.257,15.764 9.252,15.771 9.208,15.774C9.14,15.781 9.125,15.769 9.043,15.645C8.999,15.582 8.965,15.528 8.965,15.528C8.965,15.528 8.933,15.523 8.897,15.521L8.828,15.514L8.797,15.579L8.765,15.645L8.709,15.628C8.665,15.613 8.621,15.596 8.58,15.577L8.507,15.54V15.401L9.155,15.404L9.802,15.406L9.805,15.472C9.807,15.55 9.809,15.548 9.702,15.608Z"
android:fillColor="#202743"/>
<path
android:pathData="M15.788,12.639C15.786,12.629 15.786,12.612 15.783,12.585C15.199,12.522 13.531,12.483 11.983,13.761C11.983,13.761 11.484,11.472 9.147,11.472C6.81,11.472 5.944,13.761 5.944,13.761C4.634,12.351 2.881,12.453 2.248,12.546C2.248,12.561 2.246,12.578 2.246,12.592C2.226,12.945 2.236,13.113 2.295,13.335C2.477,14.029 3.039,14.793 3.921,15.543C5.364,16.77 7.321,17.656 8.804,17.758C10.35,17.863 12.631,16.959 14.15,15.64C14.388,15.428 14.612,15.204 14.822,14.966C14.973,14.79 15.197,14.489 15.18,14.489C15.175,14.489 15.18,14.484 15.189,14.479C15.197,14.474 15.204,14.467 15.199,14.462C15.197,14.46 15.201,14.452 15.209,14.45C15.216,14.447 15.221,14.442 15.219,14.438C15.216,14.433 15.219,14.428 15.228,14.425C15.236,14.423 15.238,14.416 15.236,14.411C15.233,14.406 15.236,14.401 15.24,14.401C15.245,14.401 15.25,14.394 15.25,14.389C15.25,14.384 15.255,14.377 15.26,14.377C15.265,14.377 15.27,14.372 15.27,14.367C15.277,14.347 15.289,14.328 15.301,14.311C15.338,14.255 15.484,13.995 15.503,13.951C15.642,13.637 15.73,13.357 15.771,13.077C15.786,12.943 15.798,12.697 15.788,12.639ZM9.702,15.608C9.656,15.635 9.624,15.647 9.617,15.643C9.61,15.64 9.576,15.611 9.539,15.584L9.473,15.531L9.405,15.604C9.257,15.764 9.252,15.772 9.208,15.774C9.14,15.781 9.125,15.769 9.043,15.645C8.999,15.582 8.965,15.528 8.965,15.528C8.965,15.528 8.933,15.523 8.896,15.521L8.828,15.514L8.797,15.579L8.765,15.645L8.709,15.628C8.665,15.613 8.621,15.596 8.58,15.577L8.507,15.54V15.402L9.155,15.404L9.802,15.406L9.804,15.472C9.807,15.55 9.809,15.548 9.702,15.608ZM10.919,15.17C10.883,15.309 10.854,15.421 10.854,15.426C10.827,15.428 10.803,15.431 10.776,15.428H10.698L10.593,15.684C10.535,15.823 10.481,15.952 10.474,15.971L10.459,16.005L10.408,15.922L10.357,15.84V15.165L10.338,15.17C10.296,15.18 10.002,15.219 9.873,15.231C9.34,15.285 8.804,15.273 8.273,15.197C8.188,15.185 8.115,15.175 8.113,15.178C8.11,15.18 8.113,15.336 8.117,15.528L8.125,15.874L8.083,15.937C8.061,15.971 8.04,16 8.04,16.003C8.032,16.01 8.008,15.971 7.952,15.859C7.891,15.742 7.847,15.618 7.816,15.489L7.799,15.419L7.723,15.424L7.648,15.431L7.628,15.341C7.618,15.292 7.606,15.217 7.601,15.175L7.594,15.097L7.531,15.041C7.494,15.01 7.458,14.978 7.45,14.973C7.438,14.963 7.433,14.946 7.433,14.932V14.903L7.738,14.905L8.042,14.907L8.052,14.939L8.061,14.971L8.166,14.976C8.225,14.978 8.41,14.983 8.575,14.988L8.879,14.995L8.957,14.873L9.038,14.752H9.14L9.137,14.498L9.135,14.245L9.001,14.187C8.57,13.997 8.317,13.78 8.222,13.517C8.203,13.464 8.203,13.444 8.198,13.201C8.195,12.96 8.195,12.938 8.212,12.884C8.247,12.763 8.341,12.668 8.463,12.636C8.505,12.624 8.614,12.624 9.189,12.624L9.865,12.626L9.941,12.663C10.031,12.707 10.072,12.738 10.123,12.807C10.182,12.884 10.199,12.945 10.199,13.094C10.199,13.306 10.184,13.488 10.162,13.561C10.131,13.661 10.082,13.756 10.021,13.841C9.899,13.997 9.678,14.153 9.476,14.223L9.417,14.245L9.42,14.501L9.422,14.757L9.476,14.761L9.529,14.766L9.602,14.876L9.673,14.985H9.999C10.179,14.985 10.338,14.988 10.352,14.99C10.377,14.995 10.381,14.993 10.406,14.951L10.435,14.907H10.715C10.932,14.907 10.995,14.91 10.995,14.917C10.988,14.915 10.956,15.034 10.919,15.17Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M15.769,12.39C15.764,12.281 15.754,12.127 15.747,12.052C15.642,10.947 15.36,10.043 14.824,9.087C14.802,9.05 14.783,9.014 14.771,8.985C14.766,8.977 14.763,8.97 14.761,8.965C14.837,8.739 15.146,7.792 15.392,6.708C15.725,5.238 15.937,3.519 15.289,3.003C15.289,3.003 14.17,2.921 12.67,4.391C12.263,4.797 11.891,5.238 11.56,5.708L11.455,5.674C10.851,5.477 10.226,5.355 9.595,5.314C9.405,5.299 8.697,5.299 8.466,5.314C7.767,5.36 7.232,5.464 6.623,5.676C6.599,5.686 6.577,5.693 6.555,5.701C6.212,5.233 5.829,4.795 5.408,4.393C3.848,2.916 2.687,3.001 2.687,3.001C2,3.534 2.234,5.331 2.589,6.85C2.823,7.855 3.11,8.739 3.217,9.058C3.21,9.075 3.2,9.094 3.191,9.111C2.623,10.26 2.317,11.368 2.244,12.546C2.879,12.453 4.629,12.349 5.941,13.761C5.941,13.761 6.808,11.472 9.145,11.472C11.482,11.472 11.981,13.761 11.981,13.761C13.529,12.483 15.197,12.524 15.781,12.585C15.779,12.536 15.774,12.463 15.769,12.39ZM3.677,8.14C3.677,8.14 2.767,5.864 3.001,4.564C3.04,4.345 3.113,4.152 3.227,4.006C3.227,4.006 4.259,4.109 5.924,5.959C5.924,5.959 5.608,6.112 5.184,6.438C5.184,6.438 5.182,6.441 5.179,6.441C4.719,6.794 4.13,7.354 3.677,8.14ZM7.541,11.521C7.526,11.55 7.453,11.582 7.37,11.594C7.288,11.606 7.034,11.599 6.891,11.582C6.375,11.514 5.827,11.356 5.428,11.161C5.204,11.051 5.043,10.942 4.924,10.823L4.856,10.752L4.848,10.659C4.836,10.499 4.839,10.246 4.858,10.156C4.87,10.08 4.897,10.007 4.933,9.941C4.953,9.917 4.953,9.917 5.041,9.917C5.148,9.917 5.238,9.934 5.384,9.98C5.683,10.073 6.129,10.297 6.672,10.623C7.12,10.891 7.288,11.022 7.417,11.202C7.504,11.314 7.563,11.472 7.541,11.521ZM13.276,10.715C13.269,10.754 13.154,10.871 13.04,10.956C12.704,11.202 12.103,11.431 11.492,11.541C11.144,11.604 10.795,11.614 10.691,11.563C10.623,11.528 10.613,11.502 10.637,11.414C10.688,11.231 10.854,11.032 11.112,10.844C11.243,10.75 11.776,10.426 12.025,10.289C12.434,10.065 12.765,9.932 12.989,9.9C13.059,9.89 13.145,9.888 13.171,9.9C13.215,9.917 13.264,10.046 13.283,10.202C13.293,10.285 13.288,10.65 13.276,10.715ZM12.872,6.438L12.867,6.436C12.458,6.11 12.151,5.956 12.151,5.956C13.76,4.106 14.756,4.004 14.756,4.004C14.866,4.152 14.936,4.342 14.975,4.561C15.202,5.861 14.323,8.14 14.323,8.14C13.955,7.485 13.461,6.906 12.872,6.438Z"
android:fillColor="#FFA409"/>
<path
android:pathData="M13.276,10.715C13.269,10.754 13.154,10.871 13.04,10.956C12.704,11.202 12.103,11.431 11.492,11.541C11.143,11.604 10.795,11.614 10.691,11.563C10.623,11.528 10.613,11.502 10.637,11.414C10.688,11.231 10.854,11.032 11.112,10.844C11.243,10.75 11.776,10.426 12.025,10.289C12.434,10.065 12.765,9.932 12.989,9.9C13.059,9.89 13.144,9.888 13.171,9.9C13.215,9.917 13.264,10.046 13.283,10.202C13.293,10.285 13.288,10.65 13.276,10.715Z"
android:fillColor="#202743"/>
<path
android:pathData="M7.541,11.521C7.526,11.55 7.453,11.582 7.37,11.594C7.287,11.606 7.034,11.599 6.891,11.582C6.375,11.514 5.827,11.356 5.428,11.161C5.204,11.051 5.043,10.942 4.924,10.823L4.856,10.752L4.848,10.659C4.836,10.499 4.839,10.245 4.858,10.156C4.87,10.08 4.897,10.007 4.933,9.941C4.953,9.917 4.953,9.917 5.041,9.917C5.148,9.917 5.238,9.934 5.384,9.98C5.683,10.073 6.129,10.297 6.672,10.623C7.12,10.891 7.287,11.022 7.417,11.202C7.504,11.314 7.563,11.472 7.541,11.521Z"
android:fillColor="#202743"/>
<path
android:pathData="M14.977,4.561C14.788,4.537 13.889,4.532 12.872,6.438L12.867,6.436C12.458,6.11 12.151,5.956 12.151,5.956C13.76,4.106 14.756,4.004 14.756,4.004C14.868,4.152 14.939,4.342 14.977,4.561Z"
android:fillColor="#FF9300"/>
<path
android:pathData="M14.325,8.14C13.955,7.483 13.463,6.906 12.872,6.438C13.889,4.532 14.785,4.537 14.977,4.561C15.204,5.864 14.325,8.14 14.325,8.14Z"
android:fillColor="#FF8300"/>
<path
android:pathData="M15.014,4.569C15.002,4.566 14.99,4.564 14.978,4.561L15.014,4.569Z"
android:fillColor="#FF8300"/>
<path
android:pathData="M5.924,5.956C5.924,5.956 5.608,6.11 5.184,6.436C5.184,6.436 5.182,6.438 5.179,6.438C4.125,4.53 3.198,4.537 2.998,4.561C3.037,4.342 3.11,4.15 3.224,4.004C3.227,4.004 4.259,4.106 5.924,5.956Z"
android:fillColor="#FF9300"/>
<path
android:pathData="M5.182,6.438C4.719,6.794 4.13,7.351 3.677,8.14C3.677,8.14 2.767,5.864 3.001,4.561C3.198,4.537 4.128,4.532 5.182,6.438Z"
android:fillColor="#FF8300"/>
<path
android:pathData="M3.001,4.561C2.988,4.564 2.974,4.566 2.962,4.569L3.001,4.561Z"
android:fillColor="#FF8300"/>
<path
android:pathData="M11.862,9.403C11.862,9.403 11.083,9.452 11.18,8.843C11.277,8.235 11.886,8.162 12.056,8.186C12.227,8.21 12.908,8.454 12.787,8.965C12.665,9.476 12.495,9.379 12.397,9.403C12.3,9.428 11.862,9.403 11.862,9.403Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M6.116,9.403C6.116,9.403 5.337,9.452 5.435,8.843C5.532,8.235 6.141,8.162 6.311,8.186C6.482,8.21 7.163,8.454 7.041,8.965C6.92,9.476 6.749,9.379 6.652,9.403C6.555,9.428 6.116,9.403 6.116,9.403Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M19.75,5.79m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#74BA40"/>
<path
android:pathData="M21.098,8.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#74BA40"/>
<path
android:pathData="M20.098,12.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#74BA40"/>
<path
android:pathData="M21.098,15.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#74BA40"/>
<path
android:pathData="M20.098,17.04m-0.75,0a0.75,0.75 0,1 1,1.5 0a0.75,0.75 0,1 1,-1.5 0"
android:fillColor="#74BA40"/>
</group>
</vector>

View file

@ -0,0 +1,14 @@
package com.tangem.data.tokens.converters
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.common.UtxoAmountLimit as BlockchainUtxoAmountLimit
internal class UtxoConverter : Converter<BlockchainUtxoAmountLimit, UtxoAmountLimit> {
override fun convert(value: BlockchainUtxoAmountLimit): UtxoAmountLimit {
return UtxoAmountLimit(
maxLimit = value.maxLimit,
maxAmount = value.maxAmount,
)
}
}

View file

@ -113,4 +113,10 @@ internal object TokensDataModule {
): NetworksCompatibilityRepository {
return DefaultNetworksCompatibilityRepository(userWalletsStore = userWalletsStore, dispatchers = dispatchers)
}
@Provides
@Singleton
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {
return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade)
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.ReserveAmountProvider
import com.tangem.blockchain.common.UtxoAmountLimitProvider
import com.tangem.data.tokens.converters.UtxoConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
internal class DefaultCurrencyChecksRepository(
private val walletManagersFacade: WalletManagersFacade,
) : CurrencyChecksRepository {
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null
}
override suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return manager?.dustValue
}
override suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null
}
override suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true
}
override suspend fun checkUtxoAmountLimit(
userWalletId: UserWalletId,
network: Network,
amount: BigDecimal,
fee: BigDecimal,
): UtxoAmountLimit? {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
val utxoAmount = if (manager is UtxoAmountLimitProvider) {
manager.checkUtxoAmountLimit(amount, fee)
} else {
null
}
return utxoAmount?.let(UtxoConverter()::convert)
}
}

View file

@ -11,9 +11,35 @@ android {
dependencies {
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Domain */
implementation(projects.domain.visa)
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
/** Project - Utils */
implementation(projects.core.utils)
implementation(projects.domain.legacy)
/** Project - Libs */
implementation(projects.libs.visa)
/** Libs - Other */
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.arrow.fx)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.androidx.paging.runtime)
implementation(deps.moshi.kotlin)
/** Libs - Tangem */
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
/** DI */
implementation(deps.hilt.core)

View file

@ -0,0 +1,190 @@
package com.tangem.data.visa
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import arrow.fx.coroutines.parZip
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.toHexString
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.utils.VisaConfig
import com.tangem.data.visa.utils.VisaCurrencyFactory
import com.tangem.data.visa.utils.VisaTxDetailsFactory
import com.tangem.data.visa.utils.VisaTxHistoryPagingSource
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.lib.visa.VisaContractInfoProvider
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import java.math.BigDecimal
internal class DefaultVisaRepository(
private val visaContractInfoProvider: VisaContractInfoProvider,
private val tangemTechApi: TangemTechApi,
private val visaApi: VisaApi,
private val cacheRegistry: CacheRegistry,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : VisaRepository {
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
VisaCurrencyFactory()
}
private val txDetailsFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
VisaTxDetailsFactory()
}
private val fetchedCurrencies = MutableStateFlow(
value = hashMapOf<String, VisaCurrency>(),
)
private val fetchedHistoryItems = MutableStateFlow(
value = emptyMap<String, List<VisaTxHistoryResponse.Transaction>>(),
)
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
val address = makeAddress(userWalletId)
// val address = "0x143fe062a538176aa0bf162f13d390208f90898f" // for testing
fetchVisaCurrencyIfExpired(address, isRefresh)
return requireNotNull(fetchedCurrencies.value[address]) {
"Unable to find VISA currency for $address"
}
}
private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) {
cacheRegistry.invokeOnExpire(
key = getBalancesAndLimitsKey(address),
skipCache = isRefresh,
block = { fetchVisaCurrency(address) },
)
}
private suspend fun fetchVisaCurrency(address: String) {
parZip(
dispatchers.io,
{ visaContractInfoProvider.getBalancesAndLimits(address) },
{ getFiatRate() },
{ balancesAndLimits, fiatRate ->
fetchedCurrencies.update { value ->
value.apply {
put(address, currencyFactory.create(balancesAndLimits, fiatRate))
}
}
},
)
}
override suspend fun getTxHistory(
userWalletId: UserWalletId,
pageSize: Int,
isRefresh: Boolean,
): Flow<PagingData<VisaTxHistoryItem>> {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet).toHexString()
// val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing
val pager = Pager(
config = PagingConfig(
pageSize = pageSize,
initialLoadSize = pageSize,
),
pagingSourceFactory = {
VisaTxHistoryPagingSource(
params = VisaTxHistoryPagingSource.Params(
cardPublicKey = cardPubKey,
pageSize = pageSize,
isRefresh = isRefresh,
),
cacheRegistry = cacheRegistry,
visaApi = visaApi,
fetchedItems = fetchedHistoryItems,
dispatchers = dispatchers,
)
},
)
return pager.flow
}
override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails {
return withContext(dispatchers.io) {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet).toHexString()
// val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing
val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull {
it.transactionId.toString() == txId
}
requireNotNull(transaction) { "Transaction not found: $txId" }
txDetailsFactory.create(
transaction = transaction,
walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain(),
)
}
}
private suspend fun makeAddress(userWalletId: UserWalletId): String {
val userWallet = findVisaUserWallet(userWalletId)
val walletAddresses = makeWalletAddresses(userWallet)
val walletAddress = walletAddresses.firstOrNull { it.type == AddressType.Default }
return requireNotNull(walletAddress?.value) {
"Unable to find wallet address"
}
}
private suspend fun getFiatRate(): BigDecimal? {
val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase()
val quotes = tangemTechApi.getQuotes(
currencyId = fiatCurrencyId,
coinIds = VisaConfig.TOKEN_ID,
).getOrThrow()
return quotes.quotes[VisaConfig.TOKEN_ID]?.price
}
private fun makeWalletAddresses(userWallet: UserWallet): Set<Address> {
val walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
return walletBlockchain.makeAddresses(getCardPubKey(userWallet))
}
private fun getCardPubKey(userWallet: UserWallet): ByteArray {
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
it.curve == EllipticCurve.Secp256k1
}
requireNotNull(cardWallet) { "Secp256k1 card wallet not found" }
return cardWallet.publicKey
}
private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"No user wallet found: $userWalletId"
}
if (!userWallet.scanResponse.cardTypesResolver.isVisaWallet()) {
error("VISA wallet required: $userWalletId")
}
return userWallet
}
private fun getBalancesAndLimitsKey(address: String): String {
return "visa_balances_and_limits_$address"
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.data.visa
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWalletId
internal class DummyVisaRepository : VisaRepository {
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
TODO(reason = "Implement in [REDACTED_JIRA]")
}
}

View file

@ -1,7 +1,16 @@
package com.tangem.data.visa.di
import com.tangem.data.visa.DummyVisaRepository
import com.squareup.moshi.Moshi
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.BuildConfig
import com.tangem.data.visa.DefaultVisaRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.lib.visa.VisaContractInfoProvider
import com.tangem.lib.visa.api.VisaApiBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -14,7 +23,30 @@ internal object VisaDataModule {
@Provides
@Singleton
fun provideVisaRepository(): VisaRepository {
return DummyVisaRepository()
fun provideVisaRepository(
@NetworkMoshi moshi: Moshi,
tangemTechApi: TangemTechApi,
cacheRegistry: CacheRegistry,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): VisaRepository {
val contractInfoProvider = VisaContractInfoProvider.Builder(
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
dispatchers = dispatchers,
).build()
val visaApi = VisaApiBuilder(
useDevApi = true,
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
moshi = moshi,
).build()
return DefaultVisaRepository(
contractInfoProvider,
tangemTechApi,
visaApi,
cacheRegistry,
userWalletsStore,
dispatchers,
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.data.visa.utils
import android.os.Build
import timber.log.Timber
import java.util.Currency
internal fun findCurrencyByNumericCode(code: Int): Currency {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code }
?: Currency.getInstance(VisaConfig.fiatCurrency.code)
} else {
Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}")
Currency.getInstance(VisaConfig.fiatCurrency.code)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.data.visa.utils
import com.tangem.domain.appcurrency.model.AppCurrency
internal object VisaConfig {
const val NETWORK_NAME = "Polygon PoS"
const val TOKEN_SYMBOL = "USDT"
const val TOKEN_ID = "tether"
const val TOKEN_DECIMALS = 8
val fiatCurrency = AppCurrency(
code = "EUR",
name = "Euro",
symbol = "",
)
}

View file

@ -0,0 +1,79 @@
package com.tangem.data.visa.utils
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.lib.visa.model.VisaBalancesAndLimits
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Instant
import java.math.BigDecimal
import java.math.BigInteger
internal class VisaCurrencyFactory {
fun create(balancesAndLimits: VisaBalancesAndLimits, fiatRate: BigDecimal?): VisaCurrency {
val now = Instant.now()
val currentLimit = if (balancesAndLimits.limitsChangeDate > now) {
balancesAndLimits.oldLimits
} else {
balancesAndLimits.newLimits
}
return VisaCurrency(
symbol = VisaConfig.TOKEN_SYMBOL,
networkName = VisaConfig.NETWORK_NAME,
decimals = VisaConfig.TOKEN_DECIMALS,
fiatRate = fiatRate,
fiatCurrency = VisaConfig.fiatCurrency,
balances = with(balancesAndLimits) {
VisaCurrency.Balances(
total = balances.total,
verified = balances.verified,
available = balances.available.forPayment,
blocked = balances.blocked,
debt = balances.debt,
pendingRefund = balances.pendingRefund,
)
},
limits = VisaCurrency.Limits(
remainingOtp = getRemainingOtp(currentLimit, now),
remainingNoOtp = getRemainingNoOtp(currentLimit, now),
singleTransaction = currentLimit.singleTransactionLimit,
expirationDate = getLimitsExpirationDate(currentLimit, now),
),
)
}
private fun getRemainingOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent
}
return currentLimit.spendLimit.limit
}
private fun getRemainingNoOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent
}
return currentLimit.noOtpLimit.limit
}
private fun getLimitsExpirationDate(currentLimits: VisaBalancesAndLimits.Limits, now: Instant): DateTime {
val expirationDate = if (currentLimits.expirationDate >= now) {
currentLimits.expirationDate.toDateTime()
} else {
val spendPeriodDays = currentLimits.spendPeriodSeconds
.div(BigInteger.valueOf(SECONDS_IN_DAY))
.toInt()
now.toDateTime().plusDays(spendPeriodDays)
}
return expirationDate.withZone(DateTimeZone.getDefault())
}
private companion object {
const val SECONDS_IN_DAY = 86_400L
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.data.visa.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.lib.visa.model.VisaTxHistoryResponse
internal class VisaTxDetailsFactory {
fun create(transaction: VisaTxHistoryResponse.Transaction, walletBlockchain: Blockchain): VisaTxDetails {
return VisaTxDetails(
id = transaction.transactionId.toString(),
type = transaction.transactionType,
status = transaction.transactionStatus,
blockchainAmount = transaction.blockchainAmount,
blockchainFee = transaction.blockchainFee,
transactionAmount = transaction.transactionAmount,
transactionCurrencyCode = transaction.transactionCurrencyCode,
merchantName = transaction.merchantName,
merchantCity = transaction.merchantCity,
merchantCountryCode = transaction.merchantCountryCode,
merchantCategoryCode = transaction.merchantCategoryCode,
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
requests = transaction.requests.map { createRequest(it, walletBlockchain) },
)
}
private fun createRequest(
request: VisaTxHistoryResponse.Transaction.Request,
walletBlockchain: Blockchain,
): VisaTxDetails.Request {
return VisaTxDetails.Request(
billingAmount = request.billingAmount,
billingCurrencyCode = request.billingCurrencyCode,
blockchainAmount = request.blockchainAmount,
blockchainFee = request.blockchainFee,
errorCode = request.errorCode,
requestDate = request.requestDt,
requestStatus = request.requestStatus,
requestType = request.requestType,
transactionAmount = request.transactionAmount,
txCurrencyCode = request.transactionCurrencyCode,
id = request.transactionRequestId.toString(),
txHash = request.txHash,
txStatus = request.txStatus,
fiatCurrency = findCurrencyByNumericCode(request.transactionCurrencyCode),
exploreUrl = request.txHash?.let(walletBlockchain::getExploreTxUrl),
)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.visa.utils
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.lib.visa.model.VisaTxHistoryResponse
internal class VisaTxHistoryItemFactory {
fun create(transaction: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
return VisaTxHistoryItem(
id = transaction.transactionId.toString(),
date = transaction.transactionDt,
amount = transaction.blockchainAmount,
fiatAmount = transaction.transactionAmount,
merchantName = transaction.merchantName,
status = transaction.transactionStatus,
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
)
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.data.visa.utils
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class VisaTxHistoryPagingSource(
params: Params,
private val cacheRegistry: CacheRegistry,
private val visaApi: VisaApi,
private val fetchedItems: MutableStateFlow<Map<String, List<VisaTxHistoryResponse.Transaction>>>,
private val dispatchers: CoroutineDispatcherProvider,
) : PagingSource<Int, VisaTxHistoryItem>() {
private val itemsFactory = VisaTxHistoryItemFactory()
private val cardPublicKey = params.cardPublicKey
private val pageSize = params.pageSize
private val isRefresh = params.isRefresh
private val pagedItems = MutableStateFlow<Map<Int, List<VisaTxHistoryItem>>>(
value = emptyMap(),
)
override fun getRefreshKey(state: PagingState<Int, VisaTxHistoryItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(pageSize) ?: anchorPage?.nextKey?.minus(pageSize)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VisaTxHistoryItem> {
val offsetToLoad = params.key ?: INITIAL_OFFSET
return try {
fetchItemsIfExpired(offsetToLoad, pageSize, isRefresh = isRefresh && params is LoadParams.Refresh)
val items = pagedItems.value[offsetToLoad].orEmpty()
val prevOffset = when {
items.isEmpty() -> null
offsetToLoad > INITIAL_OFFSET -> offsetToLoad - pageSize
else -> null
}
val nextOffset = when {
items.isEmpty() -> INITIAL_OFFSET
items.size % pageSize == 0 -> offsetToLoad + pageSize
else -> null
}
LoadResult.Page(items, prevOffset, nextOffset)
} catch (e: Throwable) {
Timber.e(e, "Unable to load the transaction history for the requested offset: $offsetToLoad")
LoadResult.Error(e)
}
}
private suspend fun fetchItemsIfExpired(offset: Int, pageSize: Int, isRefresh: Boolean) {
cacheRegistry.invokeOnExpire(
key = getCacheKey(offset),
skipCache = isRefresh,
block = { fetchItems(offset, pageSize) },
)
}
private suspend fun fetchItems(offset: Int, pageSize: Int) = withContext(dispatchers.io) {
val response = visaApi.getTxHistory(
cardPublicKey = cardPublicKey,
limit = pageSize,
offset = offset,
).getOrThrow()
fetchedItems.update {
it.toMutableMap().apply {
this[cardPublicKey] = this[cardPublicKey].orEmpty() + response.transactions
}
}
pagedItems.update {
it.toMutableMap().apply {
this[offset] = response.transactions.map(itemsFactory::create)
}
}
}
private fun getCacheKey(offset: Int): String {
return "visa_tx_history_${cardPublicKey}_$offset"
}
class Params(
val cardPublicKey: String,
val pageSize: Int,
val isRefresh: Boolean,
)
private companion object {
const val INITIAL_OFFSET = 0
}
}

View file

@ -1,21 +1,17 @@
package com.tangem.domain.card
import arrow.core.Either
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
// TODO: Convert to class [REDACTED_JIRA]
interface DerivePublicKeysUseCase {
class DerivePublicKeysUseCase(
private val derivationsRepository: DerivationsRepository,
) {
// TODO: delete [REDACTED_JIRA]
@Deprecated(message = "Use invoke(cardId: String?, derivations: Map<ByteArrayKey, List<DerivationPath>>) instead")
suspend operator fun invoke(
cardId: String? = null,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Either<Unit, DerivationTaskResponse>
suspend operator fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): Either<Throwable, Unit>
suspend operator fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): Either<Throwable, Unit> {
return Either.catch {
derivationsRepository.derivePublicKeys(userWalletId, currencies)
}
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.domain
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleErrorCode
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
*/
sealed interface DomainModuleMessage : ModuleMessage
sealed class DomainModuleError(
subCode: Int,
override val message: String,
override val data: Any?,
) : DomainModuleMessage, ModuleError() {
override val code: Int = ModuleErrorCode.DOMAIN + subCode
companion object {
// base code used for all errors in the module
internal const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
// const val CODE_ANY_OTHER = 200..299, 300..399 etc
}
}
sealed class AddCustomTokenError(
subCode: Int = 0,
message: String? = null,
data: Any? = null,
) : DomainModuleError(
subCode = ERROR_CODE_ADD_CUSTOM_TOKEN + subCode,
message = message ?: this::class.java.simpleName,
data = data,
) {
object FieldIsEmpty : AddCustomTokenError()
object InvalidContractAddress : AddCustomTokenError()
}

View file

@ -80,6 +80,11 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"vechain/test" -> Blockchain.VeChainTestnet
"aptos" -> Blockchain.Aptos
"aptos/test" -> Blockchain.AptosTestnet
"" -> Blockchain.Playa3ull // FIXME
"shibarium" -> Blockchain.Shibarium
"shibarium/test" -> Blockchain.ShibariumTestnet
"algorand" -> Blockchain.Algorand
"algorand/test" -> Blockchain.AlgorandTestnet
else -> null
}
}
@ -161,6 +166,11 @@ fun Blockchain.toNetworkId(): String {
Blockchain.VeChainTestnet -> "vechain/test"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Playa3ull -> "" // FIXME
Blockchain.Shibarium -> "shibarium"
Blockchain.ShibariumTestnet -> "shibarium/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
}
}
@ -212,6 +222,11 @@ fun Blockchain.toCoinId(): String {
Blockchain.VeChain, Blockchain.VeChainTestnet -> "vechain"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Playa3ull -> "" // FIXME
Blockchain.Shibarium -> "bone-shibaswap"
Blockchain.ShibariumTestnet -> "bone-shibaswap/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Unknown -> "unknown"
}
}
@ -240,5 +255,5 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Ducatus,
Blockchain.Aptos,
Blockchain.Playa3ull,
)

View file

@ -2,8 +2,6 @@ package com.tangem.domain.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import org.rekotlin.Action
sealed interface TokensAction : Action {
@ -13,14 +11,6 @@ sealed interface TokensAction : Action {
object ManageAccess : SetArgs
object ReadAccess : SetArgs
}
data class SaveChanges(
val currentTokens: List<CryptoCurrency.Token>,
val currentCoins: List<CryptoCurrency.Coin>,
val changedTokens: List<CryptoCurrency.Token>,
val changedCoins: List<CryptoCurrency.Coin>,
val userWallet: UserWallet,
) : TokensAction
}
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)

View file

@ -5,12 +5,12 @@ import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
@ -40,21 +40,22 @@ import timber.log.Timber
import java.math.BigDecimal
import java.util.EnumSet
@Suppress("LargeClass", "TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
// FIXME: Move to its own module and make internal
@Deprecated("Inject the WalletManagerFacade interface using DI instead")
class DefaultWalletManagersFacade(
private val walletManagersStore: WalletManagersStore,
private val userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
mnemonic: Mnemonic,
assetReader: AssetReader,
moshi: Moshi,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
) : WalletManagersFacade {
private val demoConfig by lazy { DemoConfig() }
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
private val walletManagerFactory by lazy { WalletManagerFactory(configManager) }
private val walletManagerFactory by lazy { WalletManagerFactory(configManager, blockchainDataStorage) }
private val sdkTokenConverter by lazy { SdkTokenConverter() }
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }
@ -275,7 +276,11 @@ class DefaultWalletManagersFacade(
resultFactory.getResult(walletManager)
} catch (e: BlockchainSdkError.AccountNotFound) {
resultFactory.getNoAccountResult(walletManager = walletManager, customMessage = e.customMessage)
resultFactory.getNoAccountResult(
walletManager = walletManager,
customMessage = e.customMessage,
amountToCreateAccount = e.amountToCreateAccount,
)
} catch (e: Throwable) {
Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}")
@ -287,7 +292,11 @@ class DefaultWalletManagersFacade(
return try {
resultFactory.getResult(walletManager)
} catch (e: BlockchainSdkError.AccountNotFound) {
resultFactory.getNoAccountResult(walletManager = walletManager, customMessage = e.customMessage)
resultFactory.getNoAccountResult(
walletManager = walletManager,
customMessage = e.customMessage,
amountToCreateAccount = e.amountToCreateAccount,
)
} catch (e: Throwable) {
Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}")
@ -321,6 +330,16 @@ class DefaultWalletManagersFacade(
return walletManager
}
@Deprecated("Will be removed in future")
override suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager? {
val blockchain = Blockchain.fromId(network.id.value)
return getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
}
@Deprecated("Will be removed in future")
override suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List<WalletManager> {
return walletManagersStore.getAllSync(userWalletId)
@ -376,63 +395,6 @@ class DefaultWalletManagersFacade(
}
}
@Deprecated("Will be removed in future")
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null
}
@Deprecated("Will be removed in future")
override suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal? {
val blockchain = Blockchain.fromId(network.id.value)
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
return manager?.dustValue
}
@Deprecated("Will be removed in future")
override suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null
}
@Deprecated("Will be removed in future")
override suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean {
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true
}
@Deprecated("Will be removed in future")
override suspend fun checkUtxoAmountLimit(
userWalletId: UserWalletId,
network: Network,
amount: BigDecimal,
fee: BigDecimal,
): UtxoAmountLimit? {
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is UtxoAmountLimitProvider) manager.checkUtxoAmountLimit(amount, fee) else null
}
@Deprecated("Will be removed in future")
override fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>> {
return walletManagersStore.getAll(userWalletId)
@ -605,13 +567,4 @@ class DefaultWalletManagersFacade(
walletManager.addTokens(tokensToAdd)
}
private suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager? {
val blockchain = Blockchain.fromId(network.id.value)
return getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
}
}

View file

@ -108,6 +108,9 @@ interface WalletManagersFacade {
derivationPath: String?,
): WalletManager?
@Deprecated("Will be removed in future")
suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager?
@Deprecated("Will be removed in future")
suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List<WalletManager>
@ -136,47 +139,6 @@ interface WalletManagersFacade {
*/
suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent?
/**
* Returns value which indicates if the account balance drops below the existential deposit value, it will be
* deactivated and any remaining funds will be destroyed.
*
* [REDACTED_TODO_COMMENT]
*/
@Deprecated("Will be removed in future")
suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal?
@Deprecated("Will be removed in future")
suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal?
/**
* Returns reserve amount which is required to create an account
*
* [REDACTED_TODO_COMMENT]
*/
@Deprecated("Will be removed in future")
suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal?
/**
* Returns true if account with [address] was reserved with minimum amount
*
* [REDACTED_TODO_COMMENT]
*/
@Deprecated("Will be removed in future")
suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean
/**
* Checks if transaction amount is within the UTXO limit
*
* [REDACTED_TODO_COMMENT]
*/
@Deprecated("Will be removed in future")
suspend fun checkUtxoAmountLimit(
userWalletId: UserWalletId,
network: Network,
amount: BigDecimal,
fee: BigDecimal,
): UtxoAmountLimit?
@Deprecated("Will be removed in future")
fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>>

View file

@ -40,13 +40,17 @@ internal class UpdateWalletManagerResultFactory {
)
}
fun getNoAccountResult(walletManager: WalletManager, customMessage: String): UpdateWalletManagerResult {
fun getNoAccountResult(
walletManager: WalletManager,
customMessage: String,
amountToCreateAccount: BigDecimal?,
): UpdateWalletManagerResult {
val wallet = walletManager.wallet
val blockchain = wallet.blockchain
val firstWalletToken = wallet.getTokens().firstOrNull()
val amountToCreateAccount = blockchain.amountToCreateAccount(firstWalletToken)
val amount = amountToCreateAccount ?: blockchain.amountToCreateAccount(firstWalletToken)
return if (amountToCreateAccount == null) {
return if (amount == null) {
Timber.w("Unable to get required amount to create account for: $blockchain")
UpdateWalletManagerResult.Unreachable(
selectedAddress = wallet.address,
@ -56,7 +60,7 @@ internal class UpdateWalletManagerResultFactory {
UpdateWalletManagerResult.NoAccount(
selectedAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
amountToCreateAccount = amountToCreateAccount,
amountToCreateAccount = amount,
errorMessage = customMessage,
)
}

View file

@ -3,7 +3,7 @@ package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.DerivationStyleProvider
@ -11,13 +11,12 @@ import com.tangem.domain.common.extensions.makeWalletManagerForApp
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import timber.log.Timber
import com.tangem.blockchain.common.WalletManagerFactory as BlockchainWalletManagerFactory
internal class WalletManagerFactory(
private val configManager: ConfigManager,
) {
internal class WalletManagerFactory(configManager: ConfigManager, blockchainDataStorage: BlockchainDataStorage) {
private val sdkWalletManagerFactory by lazy {
WalletManagerFactory(configManager.config.blockchainSdkConfig)
BlockchainWalletManagerFactory(configManager.config.blockchainSdkConfig, blockchainDataStorage)
}
fun createWalletManager(

View file

@ -0,0 +1,14 @@
package com.tangem.domain.tokens.model.blockchains
import java.math.BigDecimal
/**
* Model stores utxo limits
*
* @property maxLimit utxo limit
* @property maxAmount max amount
*/
data class UtxoAmountLimit(
val maxLimit: BigDecimal,
val maxAmount: BigDecimal,
)

View file

@ -29,6 +29,7 @@ class GetCurrencyWarningsUseCase(
private val promoRepository: PromoRepository,
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val currencyChecksRepository: CurrencyChecksRepository,
) {
suspend operator fun invoke(
@ -54,7 +55,7 @@ class GetCurrencyWarningsUseCase(
isSingleWalletWithTokens = isSingleWalletWithTokens,
),
flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)),
flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)),
flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)),
getSwapPromoNotificationWarning(
operations = operations,
userWalletId = userWalletId,

View file

@ -3,22 +3,28 @@ package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.tokens.error.AddCustomTokenError
import com.tangem.domain.tokens.repository.TokensListRepository
class ValidateContractAddressUseCase(private val tokensListRepository: TokensListRepository) {
class ValidateContractAddressUseCase(
private val tokensListRepository: TokensListRepository,
) {
operator fun invoke(address: String, networkId: String): Either<AddCustomTokenError, Unit> {
return either {
catch(
block = {
if (address.isEmpty()) raise(AddCustomTokenError.FieldIsEmpty)
if (address.isEmpty()) raise(AddCustomTokenError.FIELD_IS_EMPTY)
if (!tokensListRepository.validateAddress(networkId, address)) {
raise(AddCustomTokenError.InvalidContractAddress)
if (!tokensListRepository.validateAddress(
contractAddress = address,
networkId = networkId,
)
) {
raise(AddCustomTokenError.INVALID_CONTRACT_ADDRESS)
}
},
catch = {
AddCustomTokenError.InvalidContractAddress
AddCustomTokenError.INVALID_CONTRACT_ADDRESS
},
)
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.tokens.error
enum class AddCustomTokenError {
FIELD_IS_EMPTY,
INVALID_CONTRACT_ADDRESS,
}

View file

@ -0,0 +1,32 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
interface CurrencyChecksRepository {
/**
* Returns value which indicates if the account balance drops below the existential deposit value, it will be
* deactivated and any remaining funds will be destroyed.
*/
suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal?
/** Returns dust value */
suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal?
/** Returns reserve amount which is required to create an account */
suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal?
/** Returns true if account with [address] was reserved with minimum amount */
suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean
/** Checks if transaction amount is within the UTXO limit */
suspend fun checkUtxoAmountLimit(
userWalletId: UserWalletId,
network: Network,
amount: BigDecimal,
fee: BigDecimal,
): UtxoAmountLimit?
}

View file

@ -19,4 +19,5 @@ dependencies {
/** Libs - Other */
implementation(deps.jodatime)
implementation(deps.androidx.paging.runtime)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.visa
import arrow.core.Either
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWalletId
class GetVisaTxDetailsUseCase(
private val visaRepository: VisaRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, transactionId: String): Either<Throwable, VisaTxDetails> {
return Either.catch { visaRepository.getTxDetails(userWalletId, transactionId) }
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.domain.visa
import androidx.paging.PagingData
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetVisaTxHistoryUseCase(
private val visaRepository: VisaRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
pageSize: Int = DEFAULT_PAGE_SIZE,
isRefresh: Boolean = false,
): Flow<Either<Throwable, PagingData<VisaTxHistoryItem>>> {
return visaRepository
.getTxHistory(userWalletId, pageSize, isRefresh)
.map<PagingData<VisaTxHistoryItem>, Either<Throwable, PagingData<VisaTxHistoryItem>>> { it.right() }
.catch { emit(it.left()) }
}
companion object {
const val DEFAULT_PAGE_SIZE = 10
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.domain.visa.model
import org.joda.time.DateTime
import java.math.BigDecimal
import java.util.Currency
data class VisaTxDetails(
val id: String,
val type: String,
val status: String,
val blockchainAmount: BigDecimal,
val blockchainFee: BigDecimal,
val transactionAmount: BigDecimal,
val transactionCurrencyCode: Int,
val merchantName: String?,
val merchantCity: String?,
val merchantCountryCode: String?,
val merchantCategoryCode: String?,
val fiatCurrency: Currency,
val requests: List<Request>,
) {
data class Request(
val id: String,
val billingAmount: BigDecimal,
val billingCurrencyCode: Int,
val blockchainAmount: BigDecimal,
val blockchainFee: BigDecimal,
val errorCode: Int,
val requestDate: DateTime,
val requestStatus: String,
val requestType: String,
val transactionAmount: BigDecimal,
val txCurrencyCode: Int,
val txHash: String?,
val txStatus: String?,
val exploreUrl: String?,
val fiatCurrency: Currency,
)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.visa.model
import org.joda.time.DateTime
import java.math.BigDecimal
import java.util.Currency
data class VisaTxHistoryItem(
val id: String,
val date: DateTime,
val amount: BigDecimal,
val fiatAmount: BigDecimal,
val merchantName: String?,
val status: String,
val fiatCurrency: Currency,
)

View file

@ -1,9 +1,21 @@
package com.tangem.domain.visa.repository
import androidx.paging.PagingData
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface VisaRepository {
suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean = false): VisaCurrency
suspend fun getTxHistory(
userWalletId: UserWalletId,
pageSize: Int,
isRefresh: Boolean = false,
): Flow<PagingData<VisaTxHistoryItem>>
suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails
}

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