Updated on 2026-08-14
This commit is contained in:
commit
cef36091da
379 changed files with 8948 additions and 8976 deletions
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ dependencies {
|
|||
implementation(deps.timber)
|
||||
|
||||
/** Network */
|
||||
implementation(deps.krateSharedPref)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.okHttp)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import java.math.BigDecimal
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BigDecimalAdapter {
|
||||
internal class BigDecimalAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String) = BigDecimal(value)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
jsonRpc = accessTokens.bitcoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.bitcoin?.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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("")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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> */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_algorand_22.xml
Normal file
10
core/ui/src/main/res/drawable/ic_algorand_22.xml
Normal 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>
|
||||
57
core/ui/src/main/res/drawable/ic_shibarium_22.xml
Normal file
57
core/ui/src/main/res/drawable/ic_shibarium_22.xml
Normal 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>
|
||||
16
core/ui/src/main/res/drawable/img_algorand_22.xml
Normal file
16
core/ui/src/main/res/drawable/img_algorand_22.xml
Normal 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>
|
||||
90
core/ui/src/main/res/drawable/img_shibarium_22.xml
Normal file
90
core/ui/src/main/res/drawable/img_shibarium_22.xml
Normal 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>
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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]")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "€",
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.tokens.error
|
||||
|
||||
enum class AddCustomTokenError {
|
||||
FIELD_IS_EMPTY,
|
||||
INVALID_CONTRACT_ADDRESS,
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -19,4 +19,5 @@ dependencies {
|
|||
|
||||
/** Libs - Other */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue