Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-06 18:54:41 +03:00
commit 629d42fc34
79 changed files with 1241 additions and 2127 deletions

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.userWalletList.asLockable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -95,8 +96,9 @@ internal class LockUserWalletsTimer(
val startTime = System.currentTimeMillis()
delay(duration)
if (isActive) {
val userWalletsListManager = userWalletsListManagerSafe ?: return@launch
if (userWalletsListManager.hasSavedUserWallets) {
val userWalletsListManager = userWalletsListManagerSafe?.asLockable()
?: return@launch
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
Timber.d(
"""

View file

@ -19,6 +19,7 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.googlepay.GooglePayService
@ -27,6 +28,7 @@ import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
@ -45,11 +47,8 @@ import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
lateinit var userWalletsListManager: UserWalletsListManager
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
private set
var userWalletsListManagerSafe: UserWalletsListManager? = null
private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
@ -60,6 +59,12 @@ private val mainCoroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
val mainScope = CoroutineScope(mainCoroutineContext)
// TODO: Move to DI
val userWalletsListManagerSafe: UserWalletsListManager?
get() = store.state.globalState.userWalletsListManager
val userWalletsListManager: UserWalletsListManager
get() = userWalletsListManagerSafe!!
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
@ -83,14 +88,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
appStateHolder.tangemSdkManager = tangemSdkManager
appStateHolder.tangemSdk = tangemSdk
backupService = BackupService.init(tangemSdk, this)
userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
context = applicationContext,
tangemSdkManager = tangemSdkManager,
)
appStateHolder.userWalletsListManager = userWalletsListManager
userWalletsListManagerSafe = userWalletsListManager
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
initUserWalletsListManager()
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this),
@ -98,6 +99,18 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
)
}
private fun initUserWalletsListManager() {
val manager = if (preferencesStorage.shouldSaveUserWallets) {
UserWalletsListManager.provideBiometricImplementation(
context = applicationContext,
tangemSdkManager = tangemSdkManager,
)
} else {
UserWalletsListManager.provideRuntimeImplementation()
}
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
}
private fun systemActions() {
WindowCompat.setDecorFitsSystemWindows(window, false)
@ -122,7 +135,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intentHandler.handleIntent(intent, userWalletsListManager.hasSavedUserWallets)
intentHandler.handleIntent(intent, userWalletsListManager.hasUserWallets)
}
override fun onStart() {
@ -200,11 +213,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
backStackIsEmpty -> {
navigateToInitialScreen(intent)
}
else -> Unit
}
}
private fun navigateToInitialScreen(intent: Intent?) {
if (userWalletsListManager.hasSavedUserWallets) {
if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
store.dispatchOnMain(WelcomeAction.HandleIntentIfNeeded(intent))
} else {

View file

@ -1,15 +1,20 @@
package com.tangem.tap.common
import android.content.Intent
import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
@ -27,6 +32,7 @@ class IntentHandler {
fun handleIntent(intent: Intent?, hasSavedUserWallets: Boolean) {
handleBackgroundScan(intent, hasSavedUserWallets)
handleWalletConnectLink(intent)
handleBuyCurrencyCallback(intent)
handleSellCurrencyCallback(intent)
}
@ -72,6 +78,17 @@ class IntentHandler {
return true
}
private fun handleBuyCurrencyCallback(intent: Intent?) {
val data = intent?.data ?: return
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
if (data.host == successUri.host && data.authority == successUri.authority) {
val currency = store.state.walletState.selectedCurrency ?: return
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(Token.Bought(currencyType))
}
}
fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =

View file

@ -70,7 +70,15 @@ sealed class AnalyticsParam {
object MyWallets : ScannedFrom("My Wallets")
}
sealed class TxSentFrom(val value: String) {
object Send : TxSentFrom("Send")
object Swap : TxSentFrom("Swap")
object WalletConnect : TxSentFrom("WalletConnect")
object Sell : TxSentFrom("Sell")
}
companion object Key {
const val Source = "Source"
const val Batch = "Batch"
const val ProductType = "Product Type"
const val Firmware = "Firmware"

View file

@ -16,7 +16,7 @@ sealed class Basic(
) : Basic(
event = "Card Was Scanned",
params = mapOf(
"Source" to source.value,
AnalyticsParam.Source to source.value,
),
)
@ -38,6 +38,11 @@ sealed class Basic(
params = mapOf(AnalyticsParam.Currency to currency.value),
)
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom) : Basic(
event = "Transaction sent",
params = mapOf(AnalyticsParam.Source to sentFrom.value),
)
class ScanError(error: Throwable) : Basic(
event = "Scan",
error = error,

View file

@ -46,6 +46,12 @@ sealed class Token(
params = mapOf("Token" to type.value),
)
class Bought(type: CurrencyType) : Token(
category = "Token",
event = "Token bought",
params = mapOf("Token" to type.value),
)
sealed class Receive(
event: String,
params: Map<String, String> = mapOf(),

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.extensions
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -16,10 +16,25 @@ import org.rekotlin.Store
/**
* Dispatch action with creating the new coroutine with the Main dispatcher
*
* @see dispatchWithMain
*/
fun Store<*>.dispatchOnMain(action: Action) {
scope.launch(Dispatchers.Main) {
store.dispatch(action)
dispatch(action)
}
}
/**
* Dispatch action on the Main coroutine context
*
* @param action [Action] to be dispatched
*
* @see dispatchOnMain
* */
suspend fun Store<*>.dispatchWithMain(action: Action) {
withMainContext {
dispatch(action)
}
}
@ -27,9 +42,8 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
@Suppress("UnusedReceiverParameter")
suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
}
fun Store<*>.dispatchToastNotification(resId: Int) {
@ -63,20 +77,18 @@ fun Store<*>.dispatchDialogHide() {
/**
* Dispatch action inside a coroutine with the Main dispatcher
*/
@Deprecated(
message = "Use dispatchWithMain instead",
replaceWith = ReplaceWith(expression = "dispatchWithMain"),
)
suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
/**
* Dispatch action
*/
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse)
fun Store<*>.dispatchOpenUrl(url: String) {
dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
dispatch(NavigationAction.Share(url))
}

View file

@ -25,6 +25,10 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Deprecated(
message = "Use WalletStoresManager.fetch({userWalletId}, refresh = true) (to update all user wallet tokens)" +
"or WalletCurrenciesManager.update(...) (to update only one user wallet blockchain and its tokens) instead",
)
@Suppress("MagicNumber")
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
val scanResponse = store.state.globalState.scanResponse

View file

@ -18,6 +18,7 @@ import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Action
@ -105,4 +106,6 @@ sealed class GlobalAction : Action {
object FetchUserCountry : GlobalAction() {
data class Success(val countryCode: String) : GlobalAction()
}
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
}

View file

@ -84,6 +84,12 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
is GlobalAction.FetchUserCountry.Success -> globalState.copy(
userCountryCode = action.countryCode,
)
is GlobalAction.UpdateUserWalletsListManager -> {
appStateHolder.userWalletsListManager = action.manager
globalState.copy(
userWalletsListManager = action.manager,
)
}
else -> globalState
}
}

View file

@ -1,13 +1,14 @@
package com.tangem.tap.common.redux.global
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.datasource.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
@ -26,6 +27,7 @@ data class GlobalState(
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val userCountryCode: String? = null,
val userWalletsListManager: UserWalletsListManager? = null,
) : StateType
typealias CryptoCurrencyName = String

View file

@ -1,106 +1,42 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.CardDTO
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.datasource.config.ConfigManager
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.features.demo.isDemoCard
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.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.middlewares.handleBasicAnalyticsEvent
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.walletStoresManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import timber.log.Timber
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
// TODO("After adding DI") get dependencies by DI
val rates: RatesRepository by lazy {
RatesRepository(
tangemTechApi = store.state.domainNetworks.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
)
}
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler =
ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
suspend fun loadWalletData(walletManager: WalletManager) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val result = if (walletManagersThrottler.isStillThrottled(blockchainNetwork)) {
walletManagersThrottler.geValue(blockchainNetwork)!!
} else {
val safeUpdateResult = walletManager.safeUpdate()
walletManagersThrottler.updateThrottlingTo(blockchainNetwork)
walletManagersThrottler.setValue(blockchainNetwork, safeUpdateResult)
safeUpdateResult
}
when (result) {
is Result.Success -> {
dispatchOnMain(WalletAction.LoadWallet.Success(result.data, blockchainNetwork))
}
is Result.Failure -> {
when (result.error) {
is TapError.WalletManager.NoAccountError -> {
dispatchOnMain(
WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManager.NoAccountError).customMessage,
),
)
}
else -> {
dispatchOnMain(
WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage,
),
)
}
}
}
}
}
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
Analytics.setContext(userWallet.scanResponse)
val scanResponse = userWallet.scanResponse
@ -110,9 +46,11 @@ class TapWalletManager {
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
updateConfigManager(scanResponse)
withMainContext {
// Order is important
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
store.dispatch(WalletAction.UserWalletChanged(userWallet))
store.dispatch(WalletAction.UpdateCanSaveUserWallets(preferencesStorage.shouldSaveUserWallets))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
@ -134,9 +72,12 @@ class TapWalletManager {
.doOnFailure { error ->
val errorAction = when (error) {
is WalletStoresError -> when (error) {
is WalletStoresError.FetchFiatRatesError,
is WalletStoresError.UpdateWalletManagerError,
-> WalletAction.LoadData.Failure(error = null)
is WalletStoresError.FetchFiatRatesError -> WalletAction.LoadData.Failure(error = null)
is WalletStoresError.UpdateWalletManagerTokensError -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.InternalError(
message = error.cause.localizedMessage ?: error.customMessage,
),
)
is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.CreationError,
)
@ -156,32 +97,6 @@ class TapWalletManager {
}
}
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
updateConfigManager(data)
withMainContext {
store.dispatch(WalletAction.ResetState(data.card))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(
WalletAction.MultiWallet.SetIsMultiwalletAllowed(
data.cardTypesResolver.isMultiwalletAllowed(),
),
)
store.dispatch(WalletConnectAction.RestoreSessions(data))
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed &&
data.card.backupStatus == CardDTO.BackupStatus.NoBackup,
),
)
loadData(data)
}
}
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.cardTypesResolver.getBlockchain()
@ -198,126 +113,6 @@ class TapWalletManager {
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
}
}
suspend fun loadData(data: ScanResponse) {
dispatchOnMain(WalletAction.LoadCardInfo(data.card))
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
dispatchOnMain(it)
return
}
if (data.cardTypesResolver.isMultiwalletAllowed()) {
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
loadMultiWalletData(data)
} else {
loadSingleWalletData(data)
}
dispatchOnMain(WalletAction.LoadWallet())
}
private suspend fun loadMultiWalletData(scanResponse: ScanResponse) {
loadUserCurrencies(scanResponse, walletManagerFactory)
}
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<BlockchainNetwork>, scanResponse: ScanResponse) {
blockchainNetworks.map {
if (it.tokens.isNotEmpty()) {
WalletAction.MultiWallet.AddTokens(it.tokens, it)
}
}
val missingDerivations = blockchainNetworks
.filter {
it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath)
}
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
}
private suspend fun loadSingleWalletData(data: ScanResponse) {
val blockchain = data.cardTypesResolver.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(primaryWalletManager)
val actionsList = listOfNotNull<Action>(
WalletAction.MultiWallet.AddBlockchains(
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
walletManagers = listOf(primaryWalletManager),
),
data.cardTypesResolver.getPrimaryToken()?.let {
primaryWalletManager.addToken(it)
primaryWalletManager.wallet.setAmount(Amount(it))
WalletAction.MultiWallet.AddToken(it, blockchainNetwork, false)
},
WalletAction.LoadFiatRate(),
)
dispatchOnMain(*actionsList.toTypedArray())
}
}
private suspend fun loadUserCurrencies(scanResponse: ScanResponse, walletManagerFactory: WalletManagerFactory) {
val userTokens = userTokensRepository.getUserTokens(scanResponse.card)
withMainContext {
val blockchainNetworks = userTokens.toBlockchainNetworks()
val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, userTokens)
store.dispatch(
WalletAction.MultiWallet.AddBlockchains(
blockchains = blockchainNetworks,
walletManagers = walletManagers,
),
)
blockchainNetworks.filter { it.tokens.isNotEmpty() }
.map {
store.dispatch(
WalletAction.MultiWallet.AddTokens(
tokens = it.tokens,
blockchain = it,
),
)
}
checkIfDerivationsAreMissing(blockchainNetworks, scanResponse)
store.dispatch(WalletAction.LoadFiatRate(coinsList = userTokens))
}
}
suspend fun reloadData(data: ScanResponse) {
if (data.cardTypesResolver.isMultiwalletAllowed()) {
loadUserCurrencies(data, walletManagerFactory)
}
withContext(Dispatchers.Main) {
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)
return@withContext
}
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
store.dispatch(WalletAction.LoadWallet())
}
}
private fun getActionIfUnknownBlockchainOrEmptyWallet(data: ScanResponse): WalletAction? {
return when {
// check order is important
data.cardTypesResolver.isTangemTwins() && !data.twinsIsTwinned() -> {
WalletAction.EmptyWallet
}
data.cardTypesResolver.getBlockchain() == Blockchain.Unknown &&
!data.cardTypesResolver.isMultiwalletAllowed() -> {
WalletAction.LoadData.Failure(TapError.UnknownBlockchain)
}
data.isDemoCard() -> {
return null
}
data.card.wallets.isEmpty() -> {
WalletAction.EmptyWallet
}
else -> null
}
}
}
fun Wallet.getFirstToken(): Token? {

View file

@ -20,7 +20,6 @@ import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.disclaimer.DisclaimerType
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
@ -150,7 +149,7 @@ object ScanCardProcessor {
crossinline nextHandler: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
val disclaimer = DisclaimerType.get(scanResponse.card).createDisclaimer(scanResponse.card)
val disclaimer = scanResponse.card.createDisclaimer()
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) {

View file

@ -19,6 +19,7 @@ import com.tangem.tap.network.NetworkConnectivity
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
class UserTokensRepository(
private val storageService: UserTokensStorageService,
@ -37,26 +38,14 @@ class UserTokensRepository(
return@withContext loadTokensOffline(card, userId)
}
runCatching { tangemTechApi.getUserTokens(userId) }
.onSuccess { response ->
return@withContext response.tokens
.mapNotNull(Currency.Companion::fromTokenResponse).also {
storageService.saveUserTokens(userId, it.toUserTokensResponse())
}
.distinct()
}
.onFailure {
return@withContext handleGetUserTokensFailure(card = card, userId = userId, error = it)
}
error("Unreachable code because runCatching must return result")
return@withContext remoteGetUserTokens(card, userId)
}
// TODO("After adding DI") replace with CoroutineDispatcherProvider
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) = withContext(dispatchers.io) {
val userId = getUserWalletId(card) ?: return@withContext
val userTokens = tokens.toUserTokensResponse()
tangemTechApi.saveUserTokens(userId, userTokens)
remoteSaveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
}
@ -97,7 +86,7 @@ class UserTokensRepository(
return when {
error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) ->
storageService.getUserTokens(card).also {
tangemTechApi.saveUserTokens(userId = userId, userTokens = it.toUserTokensResponse())
remoteSaveUserTokens(userId = userId, userTokens = it.toUserTokensResponse())
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
@ -106,6 +95,31 @@ class UserTokensRepository(
}
}
private suspend fun remoteGetUserTokens(card: CardDTO, userId: String): List<Currency> {
runCatching {
tangemTechApi.getUserTokens(userId)
}.onSuccess { response ->
return response.tokens
.mapNotNull(Currency.Companion::fromTokenResponse).also {
storageService.saveUserTokens(userId, it.toUserTokensResponse())
}
.distinct()
}.onFailure {
return handleGetUserTokensFailure(card = card, userId = userId, error = it)
}
error("Unreachable code because runCatching must return result")
}
private suspend fun remoteSaveUserTokens(userId: String, userTokens: UserTokensResponse) {
// it can throw okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR
// if the /user-tokens endpoint disabled
runCatching {
tangemTechApi.saveUserTokens(userId, userTokens)
}.onFailure {
Timber.e(it)
}
}
private fun getUserWalletId(card: CardDTO): String? {
return UserWalletIdBuilder.card(card).build()
?.stringValue

View file

@ -22,47 +22,19 @@ interface UserWalletsListManager {
val selectedUserWalletSync: UserWallet?
/**
* Indicates that all [UserWallet]s is unlocked
*
* @see [isLockedSync]
* @see [UserWallet.isLocked]
* Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet]
* */
val isLocked: Flow<Boolean>
/**
* Indicates that all [UserWallet]s is unlocked
*
* Sync version
*
* @see [isLocked]
* @see [UserWallet.isLocked]
* */
val isLockedSync: Boolean
val hasSavedUserWallets: Boolean
/**
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false.
* Require biometric user authorization
*
* @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet]
* */
suspend fun unlockWithBiometry(): CompletionResult<UserWallet?>
/**
* Remove [UserWallet]s from [userWallets] and set [isLocked] as true
* */
fun lock()
val hasUserWallets: Boolean
/**
* Set [UserWallet] with provided [UserWalletId] as selected
*
* @param userWalletId [UserWalletId] of [UserWallet] which must be selected
*
* @return [CompletionResult] of operation with selected [UserWallet]
* @return [CompletionResult.Success] with selected [UserWallet]
* or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found
* */
suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet>
suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* Save provided user wallet and set it as selected
@ -80,13 +52,16 @@ interface UserWalletsListManager {
* Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]
* @param userWalletId update [UserWallet] with that [UserWalletId]
* @param update lambda that receives stored [UserWallet] and returns updated [UserWallet]
* @return [CompletionResult] of operation with updated [UserWallet]
* @return [CompletionResult.Success] with updated [UserWallet]
* or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found
* */
suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult<UserWallet>
/**
* Delete saved [UserWallet]s with provided [UserWalletId]s
*
* Sets [isLocked] as true if [userWallets] is empty or if all [userWallets] are locked
*
* @param userWalletIds [UserWalletId]s of [UserWallet]s which must be deleted
*
* @return [CompletionResult] of operation
@ -102,12 +77,45 @@ interface UserWalletsListManager {
/**
* Get [UserWallet] with provided [UserWalletId]
* May terminate with [NoSuchElementException] if [UserWallet] is not found
*
* @return [CompletionResult] of operation with found [UserWallet]
* @return [CompletionResult.Success] with found [UserWallet]
* or [CompletionResult.Failure] with [NoSuchElementException] if [UserWallet] with [userWalletId] not found
* */
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
interface Lockable : UserWalletsListManager {
/**
* Indicates that all [UserWallet]s is locked
*
* @see [isLockedSync]
* @see [UserWallet.isLocked]
* */
val isLocked: Flow<Boolean>
/**
* Indicates that all [UserWallet]s is locked
*
* Sync version
*
* @see [isLocked]
* @see [UserWallet.isLocked]
* */
val isLockedSync: Boolean
/**
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false.
*
* @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet]
* */
suspend fun unlock(): CompletionResult<UserWallet?>
/**
* Remove [UserWallet]s from [userWallets] and set [isLocked] as true
* */
fun lock()
}
// For provider
companion object
}

View file

@ -0,0 +1,57 @@
package com.tangem.tap.domain.userWalletList
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.model.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
/**
* Indicates that the [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* */
val UserWalletsListManager.isLockable: Boolean
get() = this is UserWalletsListManager.Lockable
/**
* Indicates that the [UserWalletsListManager] is locked
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which
* produces only one false value
* */
val UserWalletsListManager.isLocked: Flow<Boolean>
get() = asLockable()?.isLocked ?: flowOf(false)
/**
* Indicates that the [UserWalletsListManager] is locked
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns false
* */
val UserWalletsListManager.isLockedSync: Boolean
get() = asLockable()?.isLockedSync ?: false
/**
* Call [UserWalletsListManager.Lockable.unlock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable]
* returns [CompletionResult.Success] with [UserWalletsListManager.selectedUserWalletSync]
* */
suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult<UserWallet?> {
return asLockable()?.unlock() ?: CompletionResult.Success(selectedUserWalletSync)
}
/**
* Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
* or do nothing otherwise
* */
fun UserWalletsListManager.lockIfLockable() {
asLockable()?.lock()
}
/**
* Safe cast [UserWalletsListManager] to [UserWalletsListManager.Lockable]
*
* @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] then returns null or
* [UserWalletsListManager.Lockable] otherwise
* */
fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? {
return this as? UserWalletsListManager.Lockable
}

View file

@ -10,6 +10,7 @@ import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
@ -68,4 +69,8 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
)
}
fun UserWalletsListManager.Companion.provideRuntimeImplementation(): UserWalletsListManager {
return RuntimeUserWalletsListManager()
}

View file

@ -23,7 +23,7 @@ internal class BiometricUserWalletsListManager(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
) : UserWalletsListManager {
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
override val userWallets: Flow<List<UserWallet>>
@ -50,10 +50,10 @@ internal class BiometricUserWalletsListManager(
override val isLockedSync: Boolean
get() = state.value.isLocked
override val hasSavedUserWallets: Boolean
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
override suspend fun unlock(): CompletionResult<UserWallet?> {
return unlockWithBiometryInternal()
.map { selectedUserWalletSync }
}
@ -62,20 +62,20 @@ internal class BiometricUserWalletsListManager(
state.update { State() }
}
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
if (state.value.selectedUserWalletId == userWalletId) {
return@catching findSelectedUserWallet()!!
}
selectedUserWalletRepository.set(userWalletId)
state.update { prevState ->
val newState = state.updateAndGet { prevState ->
prevState.copy(
selectedUserWalletId = userWalletId,
)
}
findSelectedUserWallet()!!
newState.userWallets.first { it.walletId == userWalletId }
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
@ -289,7 +289,7 @@ internal class BiometricUserWalletsListManager(
}
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.find {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
@OptIn(ExperimentalCoroutinesApi::class)
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
private val state = MutableStateFlow(State())
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { listOfNotNull(it.userWallet) }
.distinctUntilChanged()
override val selectedUserWallet: Flow<UserWallet>
get() = state
.mapLatest { it.userWallet }
.filterNotNull()
.distinctUntilChanged()
override val selectedUserWalletSync: UserWallet?
get() = state.value.userWallet
override val hasUserWallets: Boolean
get() = state.value.userWallet != null
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }
?: walletNotFound()
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return if (canOverride) {
saveInternal(userWallet)
} else {
val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId
if (isWalletSaved) {
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
} else {
saveInternal(userWallet)
}
}
}
override suspend fun update(
userWalletId: UserWalletId,
update: (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> = catching {
val wallet = state.value.userWallet
?.takeIf { it.walletId == userWalletId }
?: walletNotFound()
state.updateAndGet { prevState ->
prevState.copy(
userWallet = update(wallet),
)
}.userWallet!!
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> = clear()
override suspend fun clear(): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(
userWallet = null,
)
}
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet ?: walletNotFound()
}
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(
userWallet = userWallet,
)
}
}
private fun walletNotFound(): Nothing {
throw NoSuchElementException("User wallet not found")
}
private data class State(
val userWallet: UserWallet? = null,
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.walletCurrencies.implementation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.flatMap
@ -69,7 +68,7 @@ internal class DefaultWalletCurrenciesManager(
.flatMap {
updateWalletStoresAmounts(
userWallet = userWallet,
updatedBlockchains = currenciesToAdd.map { it.blockchain }.distinct(),
updatedCurrencies = currenciesToAddWithMissingBlockchains,
)
}
}
@ -220,11 +219,13 @@ internal class DefaultWalletCurrenciesManager(
private suspend fun updateWalletStoresAmounts(
userWallet: UserWallet,
updatedBlockchains: List<Blockchain>,
updatedCurrencies: List<Currency>,
): CompletionResult<Unit> {
val updatedBlockchains = updatedCurrencies
.filterIsInstance<Currency.Blockchain>()
val updatedWalletStores = walletStoresRepository.get(userWallet.walletId)
.firstOrNull()
?.filter { it.blockchain in updatedBlockchains }
?.filter { it.blockchainWalletData.currency in updatedBlockchains }
?: return CompletionResult.Success(Unit)
return walletAmountsRepository.updateAmountsForWalletStores(

View file

@ -35,10 +35,10 @@ sealed class WalletStoresError(code: Int) : TangemError(code) {
}
@Suppress("MagicNumber")
class UpdateWalletManagerError(
class UpdateWalletManagerTokensError(
blockchain: Blockchain,
override val cause: Throwable,
) : WalletStoresError(600015) {
override var customMessage: String = "Unable to update wallet manager for currency $blockchain: $cause"
override var customMessage: String = "Unable to update wallet manager tokens for currency $blockchain: $cause"
}
}

View file

@ -153,7 +153,7 @@ internal class DefaultWalletStoresManager(
.flatMapOnFailure { error ->
when (error) {
is WalletStoresError.WalletManagerNotCreated,
is WalletStoresError.UpdateWalletManagerError,
is WalletStoresError.UpdateWalletManagerTokensError,
-> storeWalletStore(null)
else -> CompletionResult.Failure(error)
}
@ -178,7 +178,7 @@ internal class DefaultWalletStoresManager(
.flatMapOnFailure { error ->
when (error) {
is WalletStoresError.WalletManagerNotCreated,
is WalletStoresError.UpdateWalletManagerError,
is WalletStoresError.UpdateWalletManagerTokensError,
-> CompletionResult.Success(Unit)
else -> CompletionResult.Failure(error)
}

View file

@ -2,7 +2,9 @@ package com.tangem.tap.domain.walletStores.repository.implementation
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionHistoryProvider
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result.Failure
@ -17,9 +19,9 @@ import com.tangem.common.map
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.replaceByOrAdd
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.walletStores.WalletStoresError
@ -37,6 +39,7 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.filterByCoin
@ -47,10 +50,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import kotlin.time.Duration
@Suppress("LargeClass")
internal class DefaultWalletAmountsRepository(
@ -118,8 +123,12 @@ internal class DefaultWalletAmountsRepository(
walletStores: List<WalletStoreModel>?,
fiatCurrency: FiatCurrency,
): CompletionResult<Unit> {
val walletStoresInternal = walletStores ?: getWalletStores(userWallets)
// FIXME: Use NetworkConnectionManager when it is added to DI
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
return CompletionResult.Failure(WalletStoresError.NoInternetConnection)
}
val walletStoresInternal = walletStores ?: getWalletStores(userWallets)
val currencies = walletStoresInternal
.asSequence()
.flatMap { it.walletsData }
@ -142,8 +151,8 @@ internal class DefaultWalletAmountsRepository(
Timber.e(
error,
"""
Unable to fetch fiat rates
|- Coins ids: $coinsIds
Unable to fetch fiat rates
|- Coins ids: $coinsIds
""".trimIndent(),
)
@ -177,6 +186,14 @@ internal class DefaultWalletAmountsRepository(
scanResponse: ScanResponse,
walletStores: List<WalletStoreModel>,
): CompletionResult<Unit> = coroutineScope {
// FIXME: Use NetworkConnectionManager when it is added to DI
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
walletStores.forEach {
updateWalletStoreWithUnreachable(it)
}
return@coroutineScope CompletionResult.Failure(WalletStoresError.NoInternetConnection)
}
walletStores.map { walletStore ->
async {
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
@ -206,8 +223,8 @@ internal class DefaultWalletAmountsRepository(
updateWalletStoreWithUnreachable(walletStore)
}
else -> {
withInternetConnection { walletManager.safeUpdate() }
.map { updateWalletManagerWithAmounts(userWalletId, walletManager) }
updateWalletManager(scanResponse, walletManager)
.map { updateWalletManagerInStorage(userWalletId, walletManager) }
.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
@ -228,6 +245,24 @@ internal class DefaultWalletAmountsRepository(
}
}
private suspend fun updateWalletManager(
scanResponse: ScanResponse,
walletManager: WalletManager,
demoCardsDelay: Duration = with(Duration) { 500.milliseconds },
): CompletionResult<Unit> = catching {
if (scanResponse.isDemoCard() || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(demoCardsDelay)
TestActions.testAmountInjectionForWalletManagerEnabled = false
} else {
walletManager.update()
val wallet = walletManager.wallet
if (wallet.blockchain == Blockchain.SaltPay && walletManager is TransactionHistoryProvider) {
walletManager.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens())
}
}
}
private suspend fun fetchWalletStoreRentIfNeeded(
walletStore: WalletStoreModel,
walletManager: WalletManager,
@ -278,6 +313,7 @@ internal class DefaultWalletAmountsRepository(
Unable to fetch amounts
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -310,6 +346,7 @@ internal class DefaultWalletAmountsRepository(
Fetched amounts
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -337,6 +374,7 @@ internal class DefaultWalletAmountsRepository(
Missed derivation
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -360,6 +398,7 @@ internal class DefaultWalletAmountsRepository(
Wallet manager is null
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
""".trimIndent(),
)
@ -405,6 +444,7 @@ internal class DefaultWalletAmountsRepository(
Fetched wallet rent
|- User wallet id: ${walletStore.userWalletId}
|- Blockchain: ${walletStore.blockchain}
|- Derivation path: ${walletStore.derivationPath?.rawPath}
|- Rent: $rent
""".trimIndent(),
)
@ -421,19 +461,7 @@ internal class DefaultWalletAmountsRepository(
}
}
private suspend inline fun withInternetConnection(crossinline block: suspend () -> Unit): CompletionResult<Unit> {
return if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
val error = WalletStoresError.NoInternetConnection
Timber.e(error)
CompletionResult.Failure(error)
} else {
withContext(Dispatchers.IO) {
catching { block() }
}
}
}
private suspend fun updateWalletManagerWithAmounts(
private suspend fun updateWalletManagerInStorage(
userWalletId: UserWalletId,
walletManager: WalletManager,
) = withContext(Dispatchers.Default) {

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.walletStores.repository.implementation
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
@ -51,6 +52,7 @@ internal class DefaultWalletManagersRepository(
val foundWalletManager = findWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchainNetwork?.blockchain,
derivationPath = blockchainNetwork?.derivationPath,
)
foundWalletManager?.updateTokens(
@ -85,7 +87,13 @@ internal class DefaultWalletManagersRepository(
return when {
blockchain == Blockchain.Unknown || blockchain == null -> {
val error = WalletStoresError.UnknownBlockchain()
Timber.e(error)
Timber.e(
error,
"""
Unknown blockchain while creating wallet manager
|- User wallet ID: ${userWallet.walletId}
""".trimIndent(),
)
CompletionResult.Failure(error)
}
walletManager != null -> {
@ -97,7 +105,15 @@ internal class DefaultWalletManagersRepository(
}
else -> {
val error = WalletStoresError.WalletManagerNotCreated(blockchain)
Timber.e(error)
Timber.e(
error,
"""
Unable to create wallet manager
|- User wallet ID: ${userWallet.walletId}
|- Blockchain: $blockchain
|- Derivation path: ${blockchainNetwork?.derivationPath}
""".trimIndent(),
)
CompletionResult.Failure(error)
}
}
@ -156,17 +172,21 @@ internal class DefaultWalletManagersRepository(
val tokens = blockchainNetwork?.tokens ?: listOfNotNull(scanResponse.cardTypesResolver.getPrimaryToken())
if (tokens != walletManager.cardTokens) {
// TODO: remove ability to manipulate with walletManager.cardTokens
walletManager.cardTokens.clear()
walletManager.wallet.removeAllTokens()
if (tokens.isNotEmpty()) {
walletManager.cardTokens.addAll(tokens)
// add empty amounts to prepare templates of tokens WalletDataModel
// see: WalletMangerWalletStoreBuilderImpl.build()
tokens.forEach { walletManager.wallet.setAmount(Amount(it)) }
}
}
walletManager
}
.mapFailure {
val error = WalletStoresError.UpdateWalletManagerError(
val error = WalletStoresError.UpdateWalletManagerTokensError(
blockchain = walletManager.wallet.blockchain,
cause = it,
)
@ -178,6 +198,7 @@ internal class DefaultWalletManagersRepository(
private suspend fun findWalletManager(
userWalletId: UserWalletId,
blockchain: Blockchain?,
derivationPath: String?,
): WalletManager? {
return walletManagersStorage.getAll()
.firstOrNull()
@ -186,7 +207,10 @@ internal class DefaultWalletManagersRepository(
if (blockchain == null) {
userWalletManagers.firstOrNull()
} else {
userWalletManagers.firstOrNull { it.wallet.blockchain == blockchain }
userWalletManagers.firstOrNull {
it.wallet.blockchain == blockchain &&
it.wallet.publicKey.derivationPath?.rawPath == derivationPath
}
}
}
}

View file

@ -121,9 +121,10 @@ private inline fun List<WalletStoreModel>.replaceWalletStores(
if (currentWalletStore != updatedWalletStore) {
Timber.d(
"""
Update wallet store in storage
|- User wallet ID: ${updatedWalletStore.userWalletId}
|- Blockchain: ${updatedWalletStore.blockchain}
Update wallet store in storage
|- User wallet ID: ${updatedWalletStore.userWalletId}
|- Blockchain: ${updatedWalletStore.blockchain}
|- Derivation path: ${updatedWalletStore.derivationPath?.rawPath}
""".trimIndent(),
)

View file

@ -25,6 +25,8 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
@ -144,6 +146,8 @@ class WalletConnectSdkHelper {
)
return when (result) {
SimpleResult.Success -> {
val sentFrom = AnalyticsParam.TxSentFrom.WalletConnect
Analytics.send(Basic.TransactionSent(sentFrom))
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
}
is SimpleResult.Failure -> {

View file

@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ScanResponse
@ -12,6 +13,7 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
@ -20,9 +22,15 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.domain.userWalletList.isLockedSync
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -132,7 +140,6 @@ class DetailsMiddleware {
store.onUserWalletSelected(selectedUserWallet)
}
} else {
userWalletsListManager.lock()
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
@ -234,7 +241,7 @@ class DetailsMiddleware {
delay(timeMillis = 100)
}
}
store.dispatchOnMain(
store.dispatchWithMain(
DetailsAction.AppSettings.BiometricsStatusChanged(
needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics,
),
@ -250,13 +257,13 @@ class DetailsMiddleware {
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
// Nothing to change
if (preferencesStorage.shouldSaveUserWallets == enable) {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleSaveWallets(state.scanResponse, enable)
.doOnFailure {
store.dispatchOnMain(
store.dispatchWithMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
prevState = !enable,
setting = AppSetting.SaveWallets,
@ -264,7 +271,7 @@ class DetailsMiddleware {
)
}
.doOnSuccess {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
@ -282,13 +289,13 @@ class DetailsMiddleware {
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
// Nothing to change
if (preferencesStorage.shouldSaveAccessCodes == enable) {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleSaveAccessCodes(state.scanResponse, state.appSettingsState.saveWallets, enable)
.doOnFailure {
store.dispatchOnMain(
store.dispatchWithMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
prevState = !enable,
setting = AppSetting.SaveAccessCode,
@ -296,7 +303,7 @@ class DetailsMiddleware {
)
}
.doOnSuccess {
store.dispatchOnMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
@ -320,11 +327,14 @@ class DetailsMiddleware {
scanResponse: ScanResponse?,
enableAccessCodesSaving: Boolean,
): CompletionResult<Unit> {
val userWallet = scanResponse?.let { UserWalletBuilder(it).build() }
val userWallet = userWalletsListManager.selectedUserWalletSync
?: scanResponse?.let { UserWalletBuilder(it).build() }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("scanResponse is null")),
error = TangemSdkError.ExceptionError(IllegalStateException("scanResponse is null")),
)
updateUserWalletsListManager(enableUserWalletsSaving = true)
return userWalletsListManager.save(userWallet)
.flatMap {
if (enableAccessCodesSaving) {
@ -339,7 +349,7 @@ class DetailsMiddleware {
preferencesStorage.shouldShowSaveUserWalletScreen = false
preferencesStorage.shouldSaveUserWallets = true
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
@ -352,9 +362,11 @@ class DetailsMiddleware {
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
deleteSavedAccessCodes()
updateUserWalletsListManager(enableUserWalletsSaving = false)
preferencesStorage.shouldSaveUserWallets = false
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
}
.doOnFailure { error ->
Timber.e(error, "Unable to delete saved wallets")
@ -386,5 +398,27 @@ class DetailsMiddleware {
Timber.e(error, "Unable to delete saved access codes")
}
}
private suspend fun updateUserWalletsListManager(enableUserWalletsSaving: Boolean) {
val manager = if (enableUserWalletsSaving) {
createBiometricsUserWalletsManager() ?: return
} else {
UserWalletsListManager.provideRuntimeImplementation()
}
store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager))
}
private fun createBiometricsUserWalletsManager(): UserWalletsListManager? {
val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard {
Timber.e(IllegalStateException("No activities in foreground"))
return null
}
return UserWalletsListManager.provideBiometricImplementation(
context = context,
tangemSdkManager = tangemSdkManager,
)
}
}
}

View file

@ -35,6 +35,8 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
}
}
fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this)
private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider {
return object : DisclaimerDataProvider {
override fun getLanguage(): String = Locale.getDefault().language

View file

@ -5,13 +5,13 @@ import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -94,29 +94,32 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
changeButtonState(ButtonState.ENABLED)
},
onSuccess = { scanResponse ->
scope.launch {
if (preferencesStorage.shouldSaveUserWallets) {
val userWallet = UserWalletBuilder(scanResponse).build() ?: return@launch
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.onCardScanned(scanResponse)
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
navigateTo(AppScreen.Wallet)
}
} else {
store.onCardScanned(scanResponse)
navigateTo(AppScreen.Wallet)
}
}
proceedWithScanResponse(scanResponse)
},
)
}
fun proceedWithScanResponse(scanResponse: ScanResponse) {
scope.launch {
val userWallet = UserWalletBuilder(scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
return@launch
}
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
navigateTo(AppScreen.Wallet)
}
}
}
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(timeMillis = 200)

View file

@ -1,21 +1,27 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -62,6 +68,8 @@ object OnboardingHelper {
when {
// When should save user wallets, then save card without navigate to save wallet screen
preferencesStorage.shouldSaveUserWallets -> scope.launch {
proceedWithScanResponse(scanResponse, backupCardsIds)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
@ -75,7 +83,7 @@ object OnboardingHelper {
// then open save wallet screen
tangemSdkManager.canUseBiometry &&
preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch {
store.onCardScanned(scanResponse)
proceedWithScanResponse(scanResponse, backupCardsIds)
delay(timeMillis = 1_200)
@ -90,7 +98,7 @@ object OnboardingHelper {
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> scope.launch {
store.onCardScanned(scanResponse)
proceedWithScanResponse(scanResponse, backupCardsIds)
}
}
@ -100,4 +108,22 @@ object OnboardingHelper {
fun onInterrupted() {
Analytics.removeContext()
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {
Timber.e("User wallet not created")
return
}
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet) }
}
}
}

View file

@ -11,10 +11,11 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
@ -112,9 +113,12 @@ private fun handleOtherCardsAction(action: Action) {
)
}
store.dispatch(
WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks, updatedResponse.card),
)
scope.launch {
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))

View file

@ -26,6 +26,7 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.userWalletList.isLockedSync
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
@ -335,7 +336,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
private fun getPopBackScreen(): AppScreen {
return if (userWalletsListManager.hasSavedUserWallets) {
return if (userWalletsListManager.hasUserWallets) {
if (userWalletsListManager.isLockedSync) {
AppScreen.Welcome
} else {

View file

@ -26,13 +26,14 @@ import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -135,12 +136,12 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
BlockchainNetwork(blockchain, result.data.card)
}
store.dispatch(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks,
scope.launch {
userTokensRepository.saveUserTokens(
card = result.data.card,
),
)
tokens = blockchainNetworks.toCurrencies(),
)
}
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}

View file

@ -1,19 +1,27 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.isLockable
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -21,6 +29,7 @@ import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
internal class SaveWalletMiddleware {
val middleware: Middleware<AppState> = { _, stateProvider ->
@ -86,32 +95,51 @@ internal class SaveWalletMiddleware {
}
scope.launch {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build() ?: return@launch
val userWallet = userWalletsListManager.selectedUserWalletSync
?: UserWalletBuilder(scanResponse)
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build()
?: return@launch
val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets
provideBiometricUserWalletsListManager()
val isFirstSavedWallet = !userWalletsListManager.hasUserWallets
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
.flatMap { userWalletsListManager.save(userWallet, canOverride = true) }
.doOnFailure { error ->
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
store.dispatchWithMain(SaveWalletAction.Save.Error(error))
}
.doOnSuccess {
preferencesStorage.shouldSaveUserWallets = true
// Enable saving access codes only if this is the first time user save the wallet
preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet ||
preferencesStorage.shouldSaveAccessCodes
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(SaveWalletAction.Save.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
}
}
}
private suspend fun provideBiometricUserWalletsListManager() {
if (store.state.globalState.userWalletsListManager?.isLockable == true) return
val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard {
val error = IllegalStateException("No activities in foreground")
Timber.e(error)
store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error)))
return
}
val manager = UserWalletsListManager.provideBiometricImplementation(
context = context,
tangemSdkManager = tangemSdkManager,
)
store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager))
}
private fun dismiss(state: SaveWalletState) {
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring

View file

@ -12,6 +12,7 @@ import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.CardDTO
@ -19,6 +20,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
@ -33,7 +35,6 @@ import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
@ -61,6 +62,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.*
/**
@ -233,8 +235,10 @@ private fun sendTransaction(
dispatch(SendAction.SendSuccess)
if (externalTransactionData != null) {
Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Sell))
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Send))
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
@ -333,17 +337,16 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
}
private suspend fun updateWallet(walletManager: WalletManager) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
val wallet = walletManager.wallet
walletCurrenciesManager.update(
userWallet = selectedUserWallet,
currency = Currency.Blockchain(
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
)
} else {
store.dispatchOnMain(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to update wallet, no user wallet selected")
return
}
val wallet = walletManager.wallet
walletCurrenciesManager.update(
userWallet = selectedUserWallet,
currency = Currency.Blockchain(
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
)
}

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.common.hdWallet.DerivationPath
@ -29,11 +29,8 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -43,6 +40,7 @@ import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
class TokensMiddleware {
@ -190,7 +188,7 @@ class TokensMiddleware {
}
}
fun deriveMissingBlockchains(
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
onSuccess: (ScanResponse) -> Unit,
@ -271,77 +269,33 @@ class TokensMiddleware {
private class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(scanResponse = scanResponse)
},
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = currencyList,
)
}
}
} else {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val derivationStyle = scanResponse.card.derivationStyle
val addActions = currencyList.mapIndexedNotNull { index, currency ->
when (currency) {
is Currency.Blockchain -> {
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
val derivationParams = derivationStyle?.let {
when (derivationPath) {
null -> DerivationParams.Default(derivationStyle)
else -> DerivationParams.Custom(derivationPath)
}
}
val walletManager = factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = currency.blockchain,
derivationParams = derivationParams,
) ?: return@mapIndexedNotNull null
WalletAction.MultiWallet.AddBlockchain(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
walletManager = walletManager,
save = index == currencyList.lastIndex,
)
}
is Currency.Token -> {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork =
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
WalletAction.MultiWallet.AddToken(
token = currency.token,
blockchain = blockchainNetwork,
save = index == currencyList.lastIndex,
)
}
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add currencies, no user wallet selected")
return
}
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(scanResponse = scanResponse)
},
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = currencyList,
)
}
}
addActions.forEach { store.dispatchOnMain(it) }
}
}
private suspend fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
when {
currencies.isEmpty() -> Unit
userWalletsListManager.hasSavedUserWallets -> {
walletCurrenciesManager.removeCurrencies(
userWallet = userWalletsListManager.selectedUserWalletSync!!,
currenciesToRemove = currencies,
)
}
else -> {
store.dispatch(WalletAction.MultiWallet.RemoveWallets(currencies))
}
if (currencies.isEmpty()) return
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove currencies, no user wallet selected")
return
}
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
}
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {

View file

@ -2,12 +2,8 @@ package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
@ -20,13 +16,12 @@ import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
sealed class WalletAction : Action {
data class ResetState(val newCard: CardDTO) : WalletAction()
object PopBackToInitialScreen : WalletAction()
data class SetIfTestnetCard(val isTestnet: Boolean) : WalletAction()
data class UpdateCanSaveUserWallets(val canSaveUserWallets: Boolean) : WalletAction()
object LoadData : WalletAction() {
object Refresh : WalletAction()
@ -34,71 +29,14 @@ sealed class WalletAction : Action {
data class Failure(val error: TapError?) : WalletAction()
}
data class LoadWallet(
val blockchain: BlockchainNetwork? = null,
val walletManager: WalletManager? = null,
) : WalletAction() {
data class Success(val wallet: Wallet, val blockchain: BlockchainNetwork) : WalletAction()
data class NoAccount(
val wallet: Wallet,
val blockchain: BlockchainNetwork,
val amountToCreateAccount: String,
) : WalletAction()
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
}
data class SetArtworkId(val artworkId: String?) : WalletAction()
sealed class UserTokens : WalletAction() {
object Loading : UserTokens()
object Loaded : UserTokens()
}
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddBlockchains(
val blockchains: List<BlockchainNetwork>,
val walletManagers: List<WalletManager>,
) : MultiWallet()
data class AddTokens(
val tokens: List<Token>,
val blockchain: BlockchainNetwork,
) : MultiWallet()
data class AddBlockchain(
val blockchain: BlockchainNetwork,
val walletManager: WalletManager?,
val save: Boolean,
) : MultiWallet()
data class AddToken(
val token: Token,
val blockchain: BlockchainNetwork,
val save: Boolean,
) : MultiWallet()
data class SaveCurrencies(
val blockchainNetworks: List<BlockchainNetwork>,
val card: CardDTO? = null,
) : MultiWallet()
data class TokenLoaded(
val amount: Amount,
val token: Token,
val blockchain: BlockchainNetwork,
) : MultiWallet()
data class SelectWallet(val currency: Currency?) : MultiWallet()
data class SetSingleWalletCurrency(val currency: Currency?) : MultiWallet()
data class TryToRemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallets(val currencies: List<Currency>) : MultiWallet()
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
object BackupWallet : MultiWallet()
object ScheduleCheckForMissingDerivation : MultiWallet()
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : MultiWallet()
@ -125,24 +63,6 @@ sealed class WalletAction : Action {
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
}
data class LoadFiatRate(
val wallet: Wallet? = null,
val coinsList: List<Currency>? = null,
) : WalletAction() {
data class Success(
val fiatRates: Map<Currency, BigDecimal?>,
) : WalletAction()
object Failure : WalletAction()
}
class LoadCardInfo(val card: CardDTO) : WalletAction()
data class LoadArtwork(val card: CardDTO, val artworkId: String?) : WalletAction() {
data class Success(val artwork: Artwork) : WalletAction()
object Failure : WalletAction()
}
data class Scan(val onScanSuccessEvent: AnalyticsEvent?) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction()
@ -179,7 +99,6 @@ sealed class WalletAction : Action {
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object ChangeWallet : WalletAction()
object ShowSaveWalletIfNeeded : WalletAction()
@ -202,14 +121,6 @@ sealed class WalletAction : Action {
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class SetWalletRent(
val wallet: Wallet,
val minRent: String,
val rentExempt: String,
) : WalletAction()
data class RemoveWalletRent(val wallet: Wallet) : WalletAction()
sealed class AppCurrencyAction : WalletAction() {
object ChooseAppCurrency : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()

View file

@ -77,36 +77,34 @@ data class WalletData(
if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress(currency.currencyName))
}
}
private fun assembleBlockchainWarnings(walletWarnings: MutableList<WalletWarning>) {
if (!currency.isBlockchain()) return
if (existentialDepositString != null) {
val warning = WalletWarning.ExistentialDeposit(
currencyName = currency.currencyName,
edStringValueWithSymbol = "$existentialDepositString ${currency.currencySymbol}",
)
walletWarnings.add(warning)
}
if (walletRent != null) {
walletWarnings.add(WalletWarning.Rent(walletRent))
}
}
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>) {
with(currency) {
if (!isToken()) return
private fun assembleBlockchainWarnings(walletWarnings: MutableList<WalletWarning>) = with(currency) {
if (!isBlockchain()) return
if (blockchainAmountIsEmpty() && !tokenAmountIsEmpty()) {
walletWarnings.add(
WalletWarning.BalanceNotEnoughForFee(
currencyName = currencyName,
blockchainFullName = blockchain.fullName,
blockchainSymbol = blockchain.currency,
),
)
}
if (existentialDepositString != null) {
val warning = WalletWarning.ExistentialDeposit(
currencyName = currencyName,
edStringValueWithSymbol = "$existentialDepositString $currencySymbol",
)
walletWarnings.add(warning)
}
}
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>) = with(currency) {
if (!isToken()) return
if (blockchainAmountIsEmpty() && !tokenAmountIsEmpty()) {
walletWarnings.add(
WalletWarning.BalanceNotEnoughForFee(
currencyName = currencyName,
blockchainFullName = blockchain.fullName,
blockchainSymbol = blockchain.currency,
),
)
}
}

View file

@ -2,8 +2,6 @@ package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.entities.Button
@ -11,16 +9,13 @@ import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
@ -45,6 +40,7 @@ data class WalletState(
val derivationsCheckIsScheduled: Boolean = false,
val loadingUserTokens: Boolean = false,
val walletCardsCount: Int? = null,
val canSaveUserWallets: Boolean = false,
) : StateType {
val walletsDataFromStores: List<WalletData>
@ -87,12 +83,6 @@ data class WalletState(
val primaryWalletData: WalletData?
get() = primaryWalletStore?.walletsData?.firstOrNull()
val primaryBlockchain: Blockchain?
get() = primaryWalletManager?.wallet?.blockchain
val primaryToken: Token?
get() = primaryWalletManager?.wallet?.getFirstToken()
val primaryTokenData: WalletData?
get() = primaryWalletStore?.walletsData?.toMutableList()
?.apply { remove(primaryWalletData) }
@ -102,9 +92,6 @@ data class WalletState(
primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWalletData?.currencyData?.status != BalanceStatus.UnknownBlockchain
val hasSavedWallets: Boolean
get() = userWalletsListManager.hasSavedUserWallets
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager
@ -131,14 +118,7 @@ data class WalletState(
}
}
fun getWalletStore(wallet: Wallet?): WalletStore? {
if (wallet == null) return null
val currency =
Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath)
return getWalletStore(currency)
}
fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? {
private fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? {
if (blockchainNetwork == null) return null
return walletsStores.firstOrNull {
it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath &&
@ -151,26 +131,12 @@ data class WalletState(
return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency }
}
fun replaceWalletStoreInWalletsStores(wallet: WalletStore?): List<WalletStore> {
if (wallet == null) return walletsStores
var changed = false
val updatedWallets = walletsStores.map {
if (it.blockchainNetwork == wallet.blockchainNetwork) {
changed = true
wallet
} else {
it
}
}
return if (changed) updatedWallets else walletsStores + wallet
}
fun updateWalletData(walletData: WalletData?): WalletState {
if (walletData == null) return this
return updateWalletsData(listOf(walletData))
}
fun updateWalletsData(walletsData: List<WalletData>): WalletState {
private fun updateWalletsData(walletsData: List<WalletData>): WalletState {
val walletStores = walletsData
.map { BlockchainNetwork(it.currency.blockchain, it.currency.derivationPath, emptyList()) }
.distinct().map { getWalletStore(it) }.mapNotNull { it?.updateWallets(walletsData) }
@ -178,12 +144,6 @@ data class WalletState(
return updateWalletsStores(walletStores)
}
fun updateWalletStore(walletStore: WalletStore?): WalletState {
return copy(walletsStores = replaceWalletStoreInWalletsStores(walletStore))
.updateTotalBalance()
.updateProgressState()
}
private fun updateWalletsStores(walletStores: List<WalletStore>): WalletState {
val walletStoresMutable = walletStores.toMutableList()
val updatedWallets = walletsStores.map { oldWalletStore ->
@ -198,54 +158,9 @@ data class WalletState(
}
}
return copy(walletsStores = updatedWallets + walletStoresMutable)
.updateTotalBalance()
.updateProgressState()
}
fun removeWalletData(walletData: WalletData?): WalletState {
if (walletData == null) return this
return when (val currency = walletData.currency) {
is Currency.Blockchain -> {
val walletStores = walletsStores.filterNot {
it.blockchainNetwork.blockchain == currency.blockchain &&
it.blockchainNetwork.derivationPath == currency.derivationPath
}
copy(walletsStores = walletStores)
.updateTotalBalance()
.updateProgressState()
}
is Currency.Token -> {
val walletStore = getWalletStore(walletData.currency)
val walletDataList = walletStore?.walletsData
?.filterNot { it.currency == walletData.currency }
?: emptyList()
val updatedWalletManager = walletStore?.walletManager?.also { it.removeToken(currency.token) }
val updatedWalletStore = walletStore?.copy(
walletsData = walletDataList,
walletManager = updatedWalletManager,
)
updateWalletStore(updatedWalletStore)
}
}
}
private fun updateTotalBalance(): WalletState {
val walletsData = this.walletsStores
.flatMap(WalletStore::walletsData)
return if (walletsData.isNotEmpty()) {
this.copy(
totalBalance = TotalBalance(
state = walletsData.findProgressState(),
fiatAmount = walletsData.calculateTotalFiatAmount(),
fiatCurrency = store.state.globalState.appCurrency,
),
)
} else {
this.copy(totalBalance = null)
}
}
private fun updateProgressState(): WalletState {
val walletsData = this.walletsStores
.flatMap(WalletStore::walletsData)
@ -267,21 +182,6 @@ data class WalletState(
}
}
fun List<WalletData>.replaceSomeWalletsData(newWallets: List<WalletData>): List<WalletData> {
val remainingWallets: MutableList<WalletData> = newWallets.toMutableList()
val updatedWallets = this.map { wallet ->
val newWallet = newWallets
.firstOrNull { wallet.currency == it.currency }
if (newWallet == null) {
wallet
} else {
remainingWallets.remove(newWallet)
newWallet
}
}
return updatedWallets + remainingWallets
}
enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error }
enum class ErrorType { NoInternetConnection }

View file

@ -1,7 +1,8 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.entities.FiatCurrency
@ -18,6 +19,7 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import timber.log.Timber
class AppCurrencyMiddleware(
private val walletRepository: WalletRepository,
@ -66,14 +68,12 @@ class AppCurrencyMiddleware(
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch {
tapWalletManager.loadData(selectedUserWallet, refresh = true)
}
} else {
tapWalletManager.rates.clear()
store.dispatch(WalletAction.LoadFiatRate())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to select currency, no user wallet selected")
return
}
scope.launch {
tapWalletManager.loadData(selectedUserWallet, refresh = true)
}
}

View file

@ -1,8 +1,5 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
@ -13,23 +10,14 @@ import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.redux.reducers.toWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -38,69 +26,20 @@ import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
import timber.log.Timber
class MultiWalletMiddleware {
@Suppress("LongMethod", "ComplexMethod")
fun handle(
action: WalletAction.MultiWallet,
walletState: WalletState?,
globalState: GlobalState?,
) {
val globalState = globalState ?: return
when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
handleAddingWalletManagers(globalState, action.walletManagers)
}
is WalletAction.MultiWallet.SelectWallet -> {
if (action.currency != null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails))
}
}
is WalletAction.MultiWallet.AddToken -> {
addTokens(listOf(action.token), action.blockchain, walletState, globalState, action.save)
}
is WalletAction.MultiWallet.AddTokens -> {
addTokens(action.tokens, action.blockchain, walletState, globalState, save = false)
}
is WalletAction.MultiWallet.AddBlockchain -> {
action.walletManager?.let {
handleAddingWalletManagers(globalState, listOf(action.walletManager))
}
val currencies: List<Currency> =
(walletState?.currencies ?: emptyList()) + action.blockchain.toCurrencies()
if (action.save && globalState.scanResponse != null) {
scope.launch {
userTokensRepository.saveUserTokens(
card = globalState.scanResponse.card,
tokens = currencies,
)
}
}
store.dispatch(
WalletAction.LoadFiatRate(
coinsList = listOf(
Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath,
),
),
),
)
store.dispatch(
WalletAction.LoadWallet(
blockchain = action.blockchain,
walletManager = action.walletManager,
),
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val card = action.card ?: globalState.scanResponse?.card ?: return
scope.launch { userTokensRepository.saveUserTokens(card, action.blockchainNetworks.toCurrencies()) }
}
is WalletAction.MultiWallet.TryToRemoveWallet -> {
val currency = action.currency
val walletManager = walletState?.getWalletManager(currency).guard {
@ -130,60 +69,36 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch {
walletCurrenciesManager.removeCurrency(
userWallet = selectedUserWallet,
currencyToRemove = action.currency,
)
}
} else {
val currency = action.currency
val card = globalState.scanResponse?.card.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
var currencies = walletState?.currencies ?: emptyList()
currencies = currencies.filterNot { it == currency }
if (currency.isBlockchain()) {
currencies.filter {
it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath
}
}
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
}
}
is WalletAction.MultiWallet.RemoveWallets -> {
val card = globalState.scanResponse?.card.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove wallet, no user wallet selected")
return
}
var currencies = walletState?.currencies ?: emptyList()
currencies = currencies.filterNot { action.currencies.contains(it) }
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
}
is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit
is WalletAction.MultiWallet.BackupWallet -> {
store.state.globalState.scanResponse?.let {
Analytics.addContext(it)
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
scope.launch {
walletCurrenciesManager.removeCurrency(
userWallet = selectedUserWallet,
currencyToRemove = action.currency,
)
}
}
is WalletAction.MultiWallet.BackupWallet -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to backup wallet, no user wallet selected")
return
}
val scanResponse = selectedUserWallet.scanResponse
Analytics.addContext(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is WalletAction.MultiWallet.AddMissingDerivations -> {
scope.launch { handleBasicAnalyticsEvent() }
}
is WalletAction.MultiWallet.ScanToGetDerivations -> {
val selectedWallet = userWalletsListManager.selectedUserWalletSync
if (selectedWallet != null) {
scanAndUpdateCard(selectedWallet, walletState)
} else {
store.dispatch(WalletAction.Scan(onScanSuccessEvent = null))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to scan to get derivations, no user wallet selected")
return
}
scanAndUpdateCard(selectedUserWallet, walletState)
}
else -> {}
}
@ -215,86 +130,4 @@ class MultiWalletMiddleware {
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
private fun addDummyBalances(walletManagers: List<WalletManager>) {
walletManagers.forEach {
if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) {
DemoHelper.injectDemoBalance(it)
}
}
}
private fun handleAddingWalletManagers(
globalState: GlobalState,
walletManagers: List<WalletManager>,
) {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(walletManagers)
if (globalState.scanResponse?.isDemoCard() == true) {
addDummyBalances(walletManagers)
}
}
private fun addTokens(
tokens: List<Token>,
blockchainNetwork: BlockchainNetwork,
walletState: WalletState?,
globalState: GlobalState?,
save: Boolean,
) {
if (tokens.isEmpty()) return
val scanResponse = globalState?.scanResponse ?: return
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletState = walletState ?: return
val walletManager = walletState.getWalletManager(blockchainNetwork)?.also {
if (save) {
val wallets = tokens.mapNotNull { token -> token.toWallet(walletState, blockchainNetwork) }
val currencies = walletState.updateWalletsData(wallets).currencies
scope.launch { userTokensRepository.saveUserTokens(scanResponse.card, currencies) }
}
} ?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatchOnMain(
WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork.updateTokens(tokens),
walletManager = it,
save = save,
),
)
}
store.dispatchOnMain(
WalletAction.LoadFiatRate(
coinsList = tokens.map { token ->
Currency.Token(
token,
blockchainNetwork.blockchain,
blockchainNetwork.derivationPath,
)
},
),
)
if (tokens.isNotEmpty()) walletManager?.addTokens(tokens)
scope.launch {
when (val result = walletManager?.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token ->
wallet.getTokenAmount(token)?.let { amount -> Pair(token, amount) }
}
.forEach { (token, tokenAmount) ->
store.dispatchOnMain(
WalletAction.MultiWallet.TokenLoaded(
amount = tokenAmount,
token = token,
blockchain = blockchainNetwork,
),
)
}
}
else -> Unit
}
}
}
}

View file

@ -161,7 +161,11 @@ class TradeCryptoMiddleware {
private fun openSwap() {
val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()
val bundle = bundleOf(SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency))
val bundle =
bundleOf(
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
}

View file

@ -1,17 +1,11 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker
import com.tangem.tap.common.analytics.converters.BasicEventsSourceData
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -19,32 +13,24 @@ import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.userWalletList.lockIfLockable
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.filterByCoin
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.getSendableAmounts
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
@ -63,8 +49,6 @@ import com.tangem.tap.userWalletsListManagerSafe
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
@ -107,97 +91,26 @@ class WalletMiddleware {
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is WalletAction.DialogAction -> walletDialogMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
walletState.walletManagers.map { walletManager ->
async { globalState.tapWalletManager.loadWalletData(walletManager) }
}.awaitAll()
handleBasicAnalyticsEvent()
} else {
val walletManager = walletState.getWalletManager(action.blockchain)
?: action.walletManager
walletManager?.let { globalState.tapWalletManager.loadWalletData(it) }
}
}
}
is WalletAction.LoadWallet.Success -> {
checkForRentWarning(walletState.getWalletManager(action.blockchain))
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount?.isZero() == false && walletState.getWalletData(action.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
blockchain = action.blockchain,
walletManager = null,
save = true,
),
)
store.dispatch(WalletAction.LoadWallet.Success(action.wallet, action.blockchain))
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.LoadFiatRate -> {
val appCurrencyId = globalState.appCurrency.code
scope.launch {
val coinsList = when {
action.wallet != null -> {
val wallet = action.wallet
wallet.getTokens()
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
}
action.coinsList != null -> action.coinsList
else -> {
if (walletState.isMultiwalletAllowed) {
walletState.walletsDataFromStores.map { it.currency }
} else {
val derivationPath = walletState.primaryWalletData?.currency?.derivationPath
val primaryBlockchain = walletState.primaryBlockchain
val primaryToken = walletState.primaryToken
listOfNotNull(
primaryBlockchain?.let { Currency.Blockchain(it, derivationPath) },
primaryToken?.let { Currency.Token(it, primaryBlockchain!!, derivationPath) },
)
}
}
}
val ratesResult = globalState.tapWalletManager.rates.loadFiatRate(
currencyId = appCurrencyId,
coinsList = coinsList,
)
when (ratesResult) {
is Result.Success -> {
ratesResult.data.loadedRates.let {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it))
}
ratesResult.data.failedRates.forEach { (currency, throwable) ->
Timber.e(
throwable,
"Loading rates failed for [%s]",
currency.currencySymbol,
)
}
}
is Result.Failure -> {
store.dispatchDebugErrorNotification("LoadFiatRate.Failure")
dispatchOnMain(WalletAction.LoadFiatRate.Failure)
}
}
}
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId)
when (result) {
when (val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId)) {
is CompletionResult.Success -> {
val scanResponse = globalState.scanResponse?.copy(card = result.data)
scanResponse?.let { store.onCardScanned(scanResponse) }
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to create wallet, no user wallet selected")
return@launch
}
userWalletsListManager.update(selectedUserWallet.walletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
card = result.data,
),
)
}
}
is CompletionResult.Failure -> {}
is CompletionResult.Failure -> Unit
}
}
}
@ -208,58 +121,29 @@ class WalletMiddleware {
store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent))
}
}
is WalletAction.LoadCardInfo -> {
val attestationFailed = action.card.attestation.status == Attestation.Status.Failed
store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
scope.launch {
val response = OnlineCardVerifier().getCardInfo(action.card.cardId, action.card.cardPublicKey)
when (response) {
is Result.Success -> {
val actionList = listOf(
WalletAction.SetArtworkId(response.data.artwork?.id),
WalletAction.LoadArtwork(action.card, response.data.artwork?.id),
)
withMainContext { actionList.forEach { store.dispatch(it) } }
}
is Result.Failure -> {}
}
store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded)
}
}
is WalletAction.LoadData,
is WalletAction.LoadData.Refresh,
-> {
val selectedWallet = userWalletsListManager.selectedUserWalletSync
val selectedWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to load/refresh wallets data, no user wallet selected")
return
}
scope.launch {
if (selectedWallet != null) {
globalState.tapWalletManager.loadData(
userWallet = selectedWallet,
refresh = action is WalletAction.LoadData.Refresh,
)
} else {
val scanResponse = globalState.scanResponse ?: return@launch
if (walletState.walletsDataFromStores.isNotEmpty()) {
globalState.tapWalletManager.reloadData(scanResponse)
} else {
globalState.tapWalletManager.loadData(scanResponse)
}
}
globalState.tapWalletManager.loadData(
userWallet = selectedWallet,
refresh = action is WalletAction.LoadData.Refresh,
)
}
}
is NetworkStateChanged -> {
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
if (!action.isOnline) return
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) }
} else {
globalState.scanResponse?.let { scanNoteResponse ->
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
}
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard {
Timber.e("Unable to proceed with changed network state, no user wallet selected")
return
}
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet, refresh = true) }
}
is WalletAction.CopyAddress -> {
Analytics.send(Token.Receive.ButtonCopyAddress())
@ -300,7 +184,7 @@ class WalletMiddleware {
showSaveWalletIfNeeded()
}
is WalletAction.ChangeWallet -> {
changeWallet()
changeWallet(walletState)
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
@ -311,6 +195,16 @@ class WalletMiddleware {
tryToShowAppRatingWarning(action.walletStores)
}
is WalletAction.TotalFiatBalanceChanged -> Unit
is WalletAction.PopBackToInitialScreen -> {
userWalletsListManager.lockIfLockable()
val screen = if (walletState.canSaveUserWallets) {
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
}
}
}
@ -375,9 +269,9 @@ class WalletMiddleware {
}
}
private fun changeWallet() {
private fun changeWallet(state: WalletState) {
when {
userWalletsListManager.hasSavedUserWallets -> {
state.canSaveUserWallets -> {
Analytics.send(MainScreen.ButtonMyWallets())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
@ -457,48 +351,6 @@ class WalletMiddleware {
tokenRate = tokenRate,
)
}
private fun checkForRentWarning(walletManager: WalletManager?) {
val rentProvider = walletManager as? RentProvider ?: return
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean {
return balance < rentExempt
}
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(
PendingTransactionType.Outgoing,
).filterByCoin()
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amountValue ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
val currency = walletManager.wallet.blockchain.currency
if (show) {
dispatchOnMain(
WalletAction.SetWalletRent(
wallet = walletManager.wallet,
minRent = "${rentProvider.rentAmount().stripZeroPlainString()} $currency",
rentExempt = "${rentExempt.stripZeroPlainString()} $currency",
),
)
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet))
}
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
}
suspend fun handleBasicAnalyticsEvent() {

View file

@ -1,173 +1,13 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.R
import java.math.BigDecimal
class MultiWalletReducer {
@Suppress("LongMethod", "ComplexMethod")
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
val walletStores: List<WalletStore> = action.blockchains.map { blockchain ->
val walletManager = action.walletManagers.firstOrNull {
it.wallet.blockchain == blockchain.blockchain &&
it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath
}
val wallet = walletManager?.wallet
val walletData = WalletData(
currencyData = BalanceWidgetData(
status = BalanceStatus.Loading,
currency = blockchain.blockchain.fullName,
currencySymbol = blockchain.blockchain.currency,
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(
blockchain.blockchain,
blockchain.derivationPath,
),
existentialDepositString = getExistentialDeposit(walletManager),
historyTransactions = walletManager?.getBlockchainTxHistory(),
)
WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchain,
walletsData = listOf(walletData),
)
}
state.copy(
walletsStores = walletStores,
selectedCurrency = findSelectedCurrency(
walletsStores = walletStores,
currentSelectedCurrency = state.selectedCurrency,
isMultiWalletAllowed = state.isMultiwalletAllowed,
),
)
}
is WalletAction.MultiWallet.AddBlockchain -> {
val walletManager = action.walletManager ?: state.getWalletManager(action.blockchain)
val wallet = walletManager?.wallet
val walletData = WalletData(
currencyData = BalanceWidgetData(
status = BalanceStatus.Loading,
currency = action.blockchain.blockchain.fullName,
currencySymbol = action.blockchain.blockchain.currency,
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath,
),
existentialDepositString = getExistentialDeposit(walletManager),
historyTransactions = walletManager?.getBlockchainTxHistory(),
)
val walletStore = WalletStore(
walletManager = walletManager,
blockchainNetwork = action.blockchain,
walletsData = listOf(walletData),
)
val newState = state.updateWalletStore(walletStore)
if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) {
OnWalletLoadedReducer().reduce(wallet, action.blockchain, newState)
} else {
newState
}
}
is WalletAction.MultiWallet.AddTokens -> addTokens(action.tokens, action.blockchain, state)
is WalletAction.MultiWallet.AddToken -> addTokens(listOf(action.token), action.blockchain, state)
is WalletAction.MultiWallet.TokenLoaded -> {
val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token)
val walletManager = state.getWalletManager(currency)
if (walletManager == null) {
val screen = if (userWalletsListManager.hasSavedUserWallets) {
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
FirebaseCrashlytics.getInstance().recordException(
IllegalStateException("MultiWallet.TokenLoaded: walletManager is null"),
)
store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found)
return state
}
val wallet = walletManager.wallet
val pendingTransactions = wallet.getPendingTransactions()
val tokenPendingTransactions = pendingTransactions.filterByToken(action.token)
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(currency)
val isTokenSendButtonEnabled = tokenWalletData?.shouldEnableTokenSendButton() == true &&
pendingTransactions.isEmpty()
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
amount = action.amount.value,
amountFormatted = action.amount.value?.toFormattedCurrencyString(
decimals = action.amount.decimals,
currency = action.amount.currencySymbol,
),
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
action.amount.value?.toFiatString(it, store.state.globalState.appCurrency.symbol)
} ?: UNKNOWN_AMOUNT_SIGN,
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO,
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
currency = Currency.Token(
token = action.token,
blockchain = action.blockchain.blockchain,
derivationPath = action.blockchain.derivationPath,
),
walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)),
historyTransactions = walletManager.getTokenTxHistory(action.token),
)
state.updateWalletData(newTokenWalletData)
}
is WalletAction.MultiWallet.SetIsMultiwalletAllowed ->
state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed)
is WalletAction.MultiWallet.SelectWallet -> {
state.copy(selectedCurrency = action.currency)
}
@ -175,16 +15,6 @@ class MultiWalletReducer {
state.copy(selectedCurrency = action.currency)
}
is WalletAction.MultiWallet.TryToRemoveWallet -> state
is WalletAction.MultiWallet.RemoveWallet -> {
state.removeWalletData(state.getWalletData(action.currency))
}
is WalletAction.MultiWallet.RemoveWallets -> {
var updatedState = state
action.currencies.forEach { updatedState = updatedState.removeWalletData(state.getWalletData(it)) }
updatedState
}
is WalletAction.MultiWallet.SaveCurrencies -> state
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(showBackupWarning = action.show)
is WalletAction.MultiWallet.ScheduleCheckForMissingDerivation -> state.copy(
derivationsCheckIsScheduled = true,
)
@ -194,39 +24,7 @@ class MultiWalletReducer {
)
is WalletAction.MultiWallet.BackupWallet -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(state = ProgressState.Loading)
else -> state
}
}
private fun findWalletRent(walletStore: WalletStore?): WalletRent? {
return walletStore?.walletsData?.firstOrNull { it.walletRent != null }?.walletRent
}
private fun getExistentialDeposit(walletManager: WalletManager?): String? {
return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()?.toPlainString()
}
private fun addTokens(tokens: List<Token>, blockchain: BlockchainNetwork, state: WalletState): WalletState {
val wallets = tokens.mapNotNull { token -> token.toWallet(state, blockchain) }
return state.updateWalletsData(wallets)
}
}
fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? {
val currency = Currency.fromBlockchainNetwork(blockchain, this)
if (state.currencies.contains(currency)) return null
val walletManager = state.getWalletManager(currency)
val walletAddresses = createAddressList(walletManager?.wallet)
return WalletData(
currencyData = BalanceWidgetData(
status = BalanceStatus.Loading,
currency = this.name,
currencySymbol = this.symbol,
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = currency,
historyTransactions = walletManager?.getTokenTxHistory(this),
)
}

View file

@ -1,154 +0,0 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
class OnWalletLoadedReducer {
fun reduce(wallet: Wallet, blockchainNetwork: BlockchainNetwork, walletState: WalletState): WalletState {
return if (!walletState.isMultiwalletAllowed) {
onSingleWalletLoaded(wallet, walletState)
} else {
onMultiWalletLoaded(wallet, blockchainNetwork, walletState)
}
}
@Suppress("LongMethod")
private fun onMultiWalletLoaded(
wallet: Wallet,
blockchainNetwork: BlockchainNetwork,
walletState: WalletState,
): WalletState {
val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState
val fiatCurrency = store.state.globalState.appCurrency
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency,
)
val pendingTransactions = wallet.getPendingTransactions()
val isCoinSendButtonEnabled = coinAmountValue?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val fiatAmount = walletData.fiatRate?.let { coinAmountValue?.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.symbol) ?: UNKNOWN_AMOUNT_SIGN
val newWalletData = walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus,
currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
blockchainAmount = coinAmountValue,
amount = coinAmountValue,
amountFormatted = formattedAmount,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isCoinSendButtonEnabled),
currency = Currency.fromBlockchainNetwork(blockchainNetwork),
)
val tokens = wallet.getTokens().mapNotNull { token ->
val currency = Currency.fromBlockchainNetwork(blockchainNetwork, token)
val tokenWalletData = walletState.getWalletData(currency)
val tokenPendingTransactions = pendingTransactions.filterByToken(token)
val tokenBalanceStatus = when {
tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenAmountValue = wallet.getTokenAmount(token)?.value
val tokenFiatAmount = tokenWalletData?.fiatRate?.let { tokenAmountValue?.toFiatValue(it) }
val tokenFiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
?: UNKNOWN_AMOUNT_SIGN
val isTokenSendButtonEnabled = tokenWalletData?.shouldEnableTokenSendButton() == true &&
pendingTransactions.isEmpty()
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
blockchainAmount = coinAmountValue,
amount = tokenAmountValue,
amountFormatted = tokenAmountValue?.toFormattedCurrencyString(
token.decimals,
token.symbol,
),
fiatAmount = tokenFiatAmount,
fiatAmountFormatted = tokenFiatAmountFormatted,
),
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
)
}
val newWalletsData = tokens + newWalletData
val walletsData = walletState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData)
return walletState.updateWalletsData(walletsData)
}
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
val fiatCurrencyName = store.state.globalState.appCurrency.code
val amount = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency,
)
val fiatAmount = walletState.primaryWalletData?.fiatRate?.let { amount?.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencyName) ?: UNKNOWN_AMOUNT_SIGN
val pendingTransactions = wallet.getPendingTransactions()
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.primaryWalletData?.copy(
currencyData = BalanceWidgetData(
status = balanceStatus,
currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
blockchainAmount = amount,
amount = amount,
amountFormatted = formattedAmount,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
)
val wallets = listOfNotNull(walletData)
val updatedStore = walletState.getWalletStore(walletData?.currency)?.updateWallets(wallets)
return walletState.updateWalletStore(updatedStore).copy(
state = ProgressState.Done,
error = null,
)
}
}

View file

@ -13,12 +13,6 @@ fun List<WalletData>.findProgressState(initialState: ProgressState = ProgressSta
.reduce(ProgressState::or)
}
fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
return this
.map { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
fun List<WalletData>.calculateTotalCryptoAmount(): BigDecimal {
return this
.map { it.currencyData.amount ?: BigDecimal.ZERO }

View file

@ -1,23 +1,13 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.ErrorType
@ -25,15 +15,12 @@ import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletAddresses
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
import java.math.BigDecimal
object WalletReducer {
fun reduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState =
@ -43,7 +30,6 @@ object WalletReducer {
@Suppress("LongMethod", "ComplexMethod")
private fun internalReduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState {
val multiWalletReducer = MultiWalletReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
val appCurrencyReducer = AppCurrencyReducer()
if (action !is WalletAction) return state.walletState
@ -53,36 +39,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
when (action) {
is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState)
is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState)
is WalletAction.ResetState -> {
newState = WalletState(
cardId = action.newCard.cardId,
walletCardsCount = action.newCard.findCardsCount(),
)
}
is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet)
is WalletAction.EmptyWallet -> {
newState = newState.copy(
state = ProgressState.Done,
walletsStores = listOf(
WalletStore(
walletManager = null,
blockchainNetwork = BlockchainNetwork(
Blockchain.Unknown,
null,
emptyList(),
),
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
currency = Currency.Blockchain(Blockchain.Unknown, null),
),
),
),
),
)
}
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
@ -133,7 +89,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
}
}
}
is WalletAction.LoadData -> {
newState = newState.copy(
state = ProgressState.Loading,
@ -146,152 +101,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
error = null,
)
}
is WalletAction.LoadWallet -> {
val balanceStatus = if (newState.state == ProgressState.Refreshing) {
BalanceStatus.Refreshing
} else {
BalanceStatus.Loading
}
if (action.blockchain == null) {
val wallets = newState.walletsStores.map { walletStore ->
walletStore.copy(
walletsData = walletStore.walletsData.map { walletData ->
walletData.copy(
currencyData = walletData.currencyData.copy(
status =
if (walletStore.walletManager != null) balanceStatus else BalanceStatus.Unreachable,
currency = walletData.currencyData.currency,
currencySymbol = walletData.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
)
},
)
}
newState = newState.copy(
state = ProgressState.Loading,
walletsStores = wallets,
)
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
val currencies = listOf(Currency.fromBlockchainNetwork(action.blockchain)) +
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(action.blockchain, it)
}
val newWalletsData = newState.walletsDataFromStores.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = balanceStatus,
currency = wallet.currencyData.currency,
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
)
}
val walletsData = newState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData)
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(walletsData)
newState = newState.updateWalletStore(walletStore)
}
}
is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(
wallet = action.wallet,
blockchainNetwork = action.blockchain,
walletState = newState,
)
is WalletAction.LoadWallet.NoAccount -> {
val amount = BigDecimal.ZERO
val fiatAmount = BigDecimal.ZERO
val walletData = newState.getWalletData(action.blockchain)?.let { walletData ->
val walletBlockchain = walletData.currency.blockchain
walletData.copy(
currencyData = BalanceWidgetData(
status = BalanceStatus.NoAccount,
currency = action.wallet.blockchain.fullName,
currencySymbol = action.wallet.blockchain.currency,
amount = amount,
amountFormatted = amount.toFormattedCurrencyString(
decimals = walletBlockchain.decimals(),
currency = walletBlockchain.currency,
),
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount.toFormattedFiatValue(
fiatCurrencyName = state.globalState.appCurrency.symbol,
),
amountToCreateAccount = action.amountToCreateAccount,
),
)
}
val updatedWalletStore = newState.getWalletStore(action.blockchain)
?.updateWallets(listOfNotNull(walletData))
newState = newState.updateWalletStore(updatedWalletStore)
}
is WalletAction.LoadWallet.Failure -> {
val message = if (newState.error == ErrorType.NoInternetConnection) {
null
} else {
action.errorMessage
}
val walletStore = newState.getWalletStore(action.wallet)
val walletData = walletStore?.walletsData?.first { it.currency is Currency.Blockchain }
val newWalletData = walletData?.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Unreachable,
errorMessage = message,
),
)
val tokenWallets = action.wallet.getTokens()
.mapNotNull { token ->
walletStore?.blockchainNetwork?.let {
newState.getWalletData(Currency.fromBlockchainNetwork(it, token))
}
}
.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable,
errorMessage = message,
),
)
}
val updatedWallets = walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData
newState = newState.updateWalletsData(updatedWallets)
val progressState =
if (newState.walletsDataFromStores.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = progressState,
)
}
is WalletAction.SetArtworkId -> {
val cardImage = if (newState.cardImage?.artworkId == action.artworkId) {
newState.cardImage
} else {
null
}
newState = newState.copy(cardImage = cardImage)
}
is WalletAction.LoadFiatRate.Success -> {
newState = setNewFiatRate(action.fiatRates, state.globalState.appCurrency, newState)
}
is WalletAction.LoadArtwork -> {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (state.twinCardsState.cardNumber) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.TradeCryptoAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val walletAddresses = newState.getWalletData(newState.selectedCurrency)?.walletAddresses
@ -308,22 +117,9 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
),
)
}
is WalletAction.SetWalletRent -> {
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletRent = WalletRent(action.minRent, action.rentExempt)
val walletsData = walletStore.walletsData.map { it.copy(walletRent = walletRent) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.RemoveWalletRent -> {
val walletStore = newState.getWalletStore(action.wallet) ?: return newState
val walletsData = walletStore.walletsData.map { it.copy(walletRent = null) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}
is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true)
is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false)
is WalletAction.UserWalletChanged -> with(action.userWallet) {
val card = scanResponse.card
newState = WalletState(
@ -367,6 +163,11 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
),
)
}
is WalletAction.UpdateCanSaveUserWallets -> {
newState = newState.copy(
canSaveUserWallets = action.canSaveUserWallets,
)
}
else -> Unit
}
appStateHolder.walletState = newState
@ -390,20 +191,6 @@ private fun CardDTO.findCardsCount(): Int? {
return (this.backupStatus as? CardDTO.BackupStatus.Active)?.cardCount?.inc()
}
fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? {
if (wallet == null) return null
val listOfAddressData = wallet.createAddressesData()
// restore a selected wallet address
var indexOfSelectedWallet = 0
walletAddresses?.let {
val index =
listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address }
if (index != -1) indexOfSelectedWallet = index
}
return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData)
}
fun Wallet.createAddressesData(): List<AddressData> {
val listOfAddressData = mutableListOf<AddressData>()
// put a defaultAddress at the first place
@ -435,41 +222,4 @@ private fun handleCheckSignedHashesActions(
is WalletAction.Warnings.Set -> state.copy(mainWarningsList = action.warningList)
else -> state
}
}
private fun setNewFiatRate(
fiatRates: Map<Currency, BigDecimal?>,
appCurrency: FiatCurrency,
state: WalletState,
): WalletState {
val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal ->
rate.toFiatRateString(fiatCurrencyName = appCurrency.symbol)
}
val newWalletsData = fiatRates.mapNotNullValues { it.value }.mapNotNull { (currency, rate) ->
val walletStore = state.getWalletStore(currency) ?: return@mapNotNull null
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return@mapNotNull null
val currencyData = walletData.currencyData
var fiatAmount = when (currency) {
is Currency.Blockchain -> wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token -> wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
}
if (currencyData.status == BalanceStatus.NoAccount && fiatAmount == null) {
fiatAmount = BigDecimal.ZERO.setScale(2)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.symbol)
walletData.copy(
currencyData = currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount,
),
fiatRate = rate,
fiatRateString = rateFormatter(rate),
)
}
return state.updateWalletsData(newWalletsData)
}

View file

@ -20,8 +20,10 @@ import com.badoo.mvicore.DiffStrategy
import com.badoo.mvicore.ModelWatcher
import com.badoo.mvicore.modelWatcher
import com.tangem.common.doOnResult
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler
@ -30,7 +32,6 @@ import com.tangem.tap.common.analytics.events.DetailsScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.appendIfNotNull
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
@ -39,7 +40,6 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.PendingTransactionType
@ -63,8 +63,8 @@ import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.StoreSubscriber
import timber.log.Timber
import javax.inject.Inject
@Suppress("LargeClass", "MagicNumber")
@ -248,24 +248,16 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
if (currencyData.status != BalanceStatus.Loading && currencyData.status != BalanceStatus.Refreshing) {
Analytics.send(Token.Refreshed())
lifecycleScope.launch(Dispatchers.Default) {
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) {
walletCurrenciesManager.update(selectedUserWallet, currency)
.doOnResult {
withContext(Dispatchers.Main) {
binding.srlWalletDetails.isRefreshing = false
}
}
} else {
val blockchainNetwork = BlockchainNetwork(
blockchain = currency.blockchain,
derivationPath = currency.derivationPath,
tokens = emptyList(),
)
store.dispatchOnMain(WalletAction.LoadWallet(blockchainNetwork))
store.dispatchOnMain(WalletAction.LoadFiatRate(coinsList = listOf(currency)))
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard {
Timber.e("Unable to refresh wallet details screen, no user wallet selected")
return@launch
}
walletCurrenciesManager.update(selectedUserWallet, currency)
.doOnResult {
withMainContext {
binding.srlWalletDetails.isRefreshing = false
}
}
}
}
}

View file

@ -43,7 +43,6 @@ import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.wallet.saltPay.SaltPayWalletView
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
@ -81,13 +80,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val popBackTo = if (userWalletsListManager.hasSavedUserWallets) {
userWalletsListManager.lock()
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatch(NavigationAction.PopBackTo(popBackTo))
store.dispatch(WalletAction.PopBackToInitialScreen)
}
},
)
@ -201,7 +194,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
val navigationIconRes = if (state.hasSavedWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24
val navigationIconRes = if (state.canSaveUserWallets) {
R.drawable.ic_wallet_24
} else {
R.drawable.ic_tap_card_24
}
binding.toolbar.setNavigationIcon(navigationIconRes)
}

View file

@ -3,28 +3,55 @@ package com.tangem.tap.features.wallet.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
// TODO: Kill me, please
@OptIn(ExperimentalCoroutinesApi::class)
internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListManager?> {
private var observeWalletStoresUpdatesJob: Job? = null
set(value) {
field?.cancel()
field = value
}
init {
subscribeToUserWalletsListManagerUpdates()
}
override fun onCleared() {
store.unsubscribe(this)
}
override fun newState(state: UserWalletsListManager?) {
// Restarting observing of wallet store updates when the manager changes
if (state != null) {
bootstrapSelectedWalletStoresChanges(state)
}
}
internal class WalletViewModel : ViewModel() {
fun launch() {
bootstrapSelectedWalletStoresChanges()
val manager = store.state.globalState.userWalletsListManager
if (manager != null) {
bootstrapSelectedWalletStoresChanges(manager)
}
bootstrapShowSaveWalletIfNeeded()
}
private fun bootstrapSelectedWalletStoresChanges() {
userWalletsListManager.selectedUserWallet
private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) {
observeWalletStoresUpdatesJob = manager.selectedUserWallet
.map { it.walletId }
.distinctUntilChanged()
.flatMapLatest { selectedUserWalletId ->
walletStoresManager.get(selectedUserWalletId)
}
@ -40,4 +67,14 @@ internal class WalletViewModel : ViewModel() {
store.dispatchOnMain(WalletAction.ShowSaveWalletIfNeeded)
}
}
private fun subscribeToUserWalletsListManagerUpdates() {
store.subscribe(this) { appState ->
appState
.skip { old, new ->
old.globalState.userWalletsListManager == new.globalState.userWalletsListManager
}
.select { it.globalState.userWalletsListManager }
}
}
}

View file

@ -1,10 +1,8 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.widget.Button
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.badoo.mvicore.DiffStrategy
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@ -20,7 +18,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -36,13 +33,6 @@ class MultiWalletView : WalletView() {
private lateinit var walletsAdapter: WalletAdapter
private val watcher = modelWatcher<WalletState> {
val totalBalanceStrategy: DiffStrategy<WalletState> = { old, new ->
old.cardId != new.cardId ||
old.totalBalance != new.totalBalance ||
old.state != new.state ||
old.walletsStores.size != new.walletsStores.size
}
// !!! Workaround !!!
// Checking state properties instead of state params can reduce application performance,
// but here it is necessary because the WalletStore has an unsuitable equals method
@ -67,12 +57,11 @@ class MultiWalletView : WalletView() {
handleBackupWarning(it, showBackupWarnings)
}
}
watch({ it }, totalBalanceStrategy) { walletState ->
(WalletState::totalBalance or WalletState::walletsDataFromStores) { walletState ->
binding?.let {
handleTotalBalance(
binding = it,
totalBalance = walletState.totalBalance,
progressState = walletState.state,
walletsCount = walletState.walletsDataFromStores.size,
)
}
@ -179,10 +168,9 @@ class MultiWalletView : WalletView() {
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,
progressState: ProgressState,
walletsCount: Int,
) = with(binding.lCardTotalBalance) {
isGone = progressState == ProgressState.Done && walletsCount == 0
isVisible = walletsCount > 0
onChangeFiatCurrencyClick = {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.wallet.ui.wallet.saltPay
import android.view.LayoutInflater
import android.view.ViewGroup
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.debounce
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.ShimmerData
import com.tangem.tap.common.ShimmerRecyclerAdapter
@ -21,7 +20,6 @@ import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryItemData
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryTransactionData
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.TxHistoryAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.scope
import com.tangem.tap.store
@ -31,7 +29,6 @@ import com.tangem.wallet.databinding.LayoutSaltPayBalanceBinding
import com.tangem.wallet.databinding.LayoutSaltPayTxHistoryBinding
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
import kotlinx.coroutines.launch
import org.rekotlin.Action
import timber.log.Timber
/**
@ -41,8 +38,6 @@ class SaltPayWalletView : WalletView() {
private var saltPayBinding: LayoutSaltPayWalletBinding? = null
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
private val balanceWidget: LayoutSaltPayBalanceBinding?
get() = saltPayBinding?.lSaltPayBalance
@ -131,7 +126,6 @@ class SaltPayWalletView : WalletView() {
if (tokenData.currencyData.fiatAmount == null) {
veilBalance.veil()
actionDebouncer(WalletAction.LoadFiatRate())
} else {
veilBalance.unVeil()
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(

View file

@ -23,6 +23,7 @@ import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
@ -63,7 +64,7 @@ internal class WalletSelectorMiddleware {
updateBalances(action.walletsStores, state)
}
is WalletSelectorAction.UnlockWithBiometry -> {
unlockWalletsWithBiometry()
unlockWallets()
}
is WalletSelectorAction.AddWallet -> {
addWallet()
@ -115,11 +116,11 @@ internal class WalletSelectorMiddleware {
}
}
private fun unlockWalletsWithBiometry() {
private fun unlockWallets() {
Analytics.send(MyWallets.Button.UnlockWithBiometrics())
scope.launch {
userWalletsListManager.unlockWithBiometry()
userWalletsListManager.unlockIfLockable()
.doOnFailure { error ->
Timber.e(error, "Unable to unlock all user wallets")
store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error))
@ -195,7 +196,7 @@ internal class WalletSelectorMiddleware {
if (userWallet.isLocked) {
unlockUserWalletWithScannedCard(userWallet)
} else {
userWalletsListManager.selectWallet(userWalletId)
userWalletsListManager.select(userWalletId)
}
}
.doOnFailure { error ->

View file

@ -8,6 +8,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.userWalletList.isLocked
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState

View file

@ -17,6 +17,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.intentHandler
import com.tangem.tap.preferencesStorage
@ -66,7 +67,7 @@ internal class WelcomeMiddleware {
private fun proceedWithBiometrics(state: WelcomeState) {
scope.launch {
userWalletsListManager.unlockWithBiometry()
userWalletsListManager.unlockIfLockable()
.doOnFailure { error ->
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error))
}

View file

@ -29,7 +29,7 @@ class DerivationManagerImpl(
private val appStateHolder: AppStateHolder,
) : DerivationManager {
override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine<Boolean> { continuation ->
override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation ->
val blockchain = Blockchain.fromNetworkId(currency.networkId)
val card = appStateHolder.getActualCard()
if (blockchain != null && card != null) {
@ -89,7 +89,7 @@ class DerivationManagerImpl(
}
scope.launch {
val selectedWallet = appStateHolder.userWalletsListManager?.selectedUserWalletSync
val selectedUserWallet = appStateHolder.userWalletsListManager?.selectedUserWalletSync
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
scanResponse.card.cardId,
@ -110,8 +110,8 @@ class DerivationManagerImpl(
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
if (selectedWallet != null) {
val userWallet = selectedWallet.copy(
if (selectedUserWallet != null) {
val userWallet = selectedUserWallet.copy(
scanResponse = updatedScanResponse,
)
@ -166,14 +166,10 @@ class DerivationManagerImpl(
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
alreadyDerivedKeys = alreadyDerivedKeys,
mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey,
)
}
private class DerivationData(
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
val alreadyDerivedKeys: ExtendedPublicKeysMap,
val mapKeyOfWalletPublicKey: ByteArrayKey,
)
}

View file

@ -17,31 +17,41 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.isNetworkError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
@Suppress("LargeClass")
class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
private val analytics: AnalyticsEventHandler,
) : TransactionManager {
override suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
derivationPath: String?,
): SendTxResult {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val walletManager = getActualWalletManager(blockchain, derivationPath)
walletManager.update()
val amount = Amount(value = BigDecimal.ZERO, blockchain = blockchain)
return sendTransactionInternal(
@ -49,7 +59,7 @@ class TransactionManagerImpl(
amount = amount,
blockchain = blockchain,
feeAmount = feeAmount,
estimatedGas = estimatedGas,
gasLimit = gasLimit,
destinationAddress = destinationAddress,
dataToSign = dataToSign,
)
@ -59,14 +69,15 @@ class TransactionManagerImpl(
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
isSwap: Boolean,
currencyToSend: Currency,
derivationPath: String?,
): SendTxResult {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val walletManager = getActualWalletManager(blockchain, derivationPath)
walletManager.update()
val amount = if (isSwap) {
createAmountForSwap(amountToSend, currencyToSend, blockchain)
@ -78,7 +89,7 @@ class TransactionManagerImpl(
amount = amount,
blockchain = blockchain,
feeAmount = feeAmount,
estimatedGas = estimatedGas,
gasLimit = gasLimit,
destinationAddress = destinationAddress,
dataToSign = dataToSign,
)
@ -90,7 +101,7 @@ class TransactionManagerImpl(
amount: Amount,
blockchain: Blockchain,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
): SendTxResult {
@ -98,12 +109,12 @@ class TransactionManagerImpl(
amount = amount,
fee = Amount(value = feeAmount, blockchain = blockchain),
destination = destinationAddress,
).copy(hash = dataToSign, extras = createExtras(walletManager, estimatedGas, dataToSign))
).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign))
val signer = transactionSigner(walletManager)
val sendResult = try {
(walletManager as TransactionSender).send(txData, signer)
(walletManager as? TransactionSender)?.send(txData, signer) ?: error("Cannot cast to TransactionSender")
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
return SendTxResult.UnknownError(ex)
@ -120,9 +131,9 @@ class TransactionManagerImpl(
return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found")
}
override suspend fun updateWalletManager(networkId: String) {
override suspend fun updateWalletManager(networkId: String, derivationPath: String?) {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
getActualWalletManager(blockchain).update()
getActualWalletManager(blockchain, derivationPath).update()
}
override fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal {
@ -137,19 +148,55 @@ class TransactionManagerImpl(
amountToSend: BigDecimal,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount {
data: String?,
derivationPath: String?,
): ProxyFee {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val fee = (walletManager as TransactionSender).getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
)
when (fee) {
is Result.Success -> {
return convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found"))
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
blockchain = blockchain,
amount = amountToSend,
currency = currencyToSend,
destinationAddress = destinationAddress,
data = data,
)
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
val fee = gasLimit.multiply(gasPrice.data).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = fee,
decimals = blockchain.decimals(),
),
)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
}
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
} else {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
when (fee) {
is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now
return ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")),
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
}
@ -163,9 +210,43 @@ class TransactionManagerImpl(
)
}
@Suppress("LongParameterList")
private suspend fun getGasLimit(
evmWalletManager: EthereumWalletManager,
blockchain: Blockchain,
amount: BigDecimal,
currency: Currency,
destinationAddress: String,
data: String?,
): BigInteger {
val result = if (data.isNullOrEmpty()) {
evmWalletManager.getGasLimit(
amount = createAmount(amount, currency, blockchain),
destination = destinationAddress,
)
} else {
evmWalletManager.getGasLimit(
amount = createAmount(amount, currency, blockchain),
destination = destinationAddress,
data = data,
)
}
when (result) {
is Result.Success -> {
return result.data
}
is Result.Failure -> {
error(result.error.message ?: result.error.customMessage)
}
}
}
private fun handleSendResult(result: SimpleResult): SendTxResult {
when (result) {
is SimpleResult.Success -> return SendTxResult.Success
is SimpleResult.Success -> {
analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Swap))
return SendTxResult.Success
}
is SimpleResult.Failure -> {
if (result.isNetworkError()) return SendTxResult.NetworkError(result.error)
val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError()
@ -215,31 +296,22 @@ class TransactionManagerImpl(
}
}
private fun getActualWalletManager(blockchain: Blockchain): WalletManager {
val card = appStateHolder.getActualCard()
if (card != null) {
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
if (walletManager != null) {
return walletManager
} else {
error("no wallet manager found")
}
} else {
error("card not found")
}
private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList())
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
return requireNotNull(walletManager) { "no wallet manager found" }
}
private fun createExtras(
walletManager: WalletManager,
estimatedGas: Int,
gasLimit: Int,
transactionHash: String,
): TransactionExtras? {
return when (walletManager) {
is EthereumWalletManager -> {
return EthereumTransactionExtras(
data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(),
gasLimit = estimatedGas.toBigInteger(),
gasLimit = gasLimit.toBigInteger(),
)
}
else -> {

View file

@ -2,13 +2,9 @@ package com.tangem.tap.proxy
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
@ -19,20 +15,17 @@ import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFiatCurrency
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.delay
import org.rekotlin.Action
import timber.log.Timber
import java.math.BigDecimal
import com.tangem.tap.features.wallet.models.Currency as WalletCurrency
class UserWalletManagerImpl(
private val appStateHolder: AppStateHolder,
private val walletManagerFactory: WalletManagerFactory,
) : UserWalletManager {
override suspend fun getUserTokens(networkId: String, isExcludeCustom: Boolean): List<Currency> {
@ -92,67 +85,49 @@ class UserWalletManagerImpl(
?: ""
}
override suspend fun isTokenAdded(currency: Currency): Boolean {
val card = requireNotNull(appStateHolder.getActualCard()) { "card is null" }
override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean {
val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
if (walletManager != null) {
return walletManager.cardTokens.any {
it.id == currency.id
}
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.cardTokens.any {
it.id == currency.id
}
return false
}
override suspend fun addToken(currency: Currency) {
val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" }
override suspend fun addToken(currency: Currency, derivationPath: String?) {
val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
walletCurrenciesManager.addCurrencies(
userWallet = selectedUserWallet,
currenciesToAdd = listOf(currency.toWalletCurrency(blockchainNetwork)),
)
} else {
val walletManager = getOrCreateBlockchain(blockchainNetwork, blockchain)
if (currency is NonNativeToken && !walletManager.cardTokens.contains(currency.toSdkToken())) {
val action = addNonNativeTokenToWalletAction(currency, card, blockchain)
val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" }
mainStore.dispatchOnMain(action)
delay(DELAY_UPDATE_WALLET)
}
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add token, no user wallet selected")
return
}
walletCurrenciesManager.addCurrencies(
userWallet = selectedUserWallet,
currenciesToAdd = listOf(currency.toWalletCurrency(blockchainNetwork)),
)
}
override fun getWalletAddress(networkId: String): String {
override fun getWalletAddress(networkId: String, derivationPath: String?): String {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
if (walletManager != null) {
return walletManager.wallet.address
} else {
error("no wallet manager found")
}
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.wallet.address
}
override fun getLastTransactionHash(networkId: String): String? {
override fun getLastTransactionHash(networkId: String, derivationPath: String?): String? {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
return walletManager?.wallet?.recentTransactions?.lastOrNull()?.hash?.let { HEX_PREFIX + it }
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.wallet.recentTransactions
.lastOrNull { it.hash?.isNotEmpty() == true }
?.hash?.let { HEX_PREFIX + it }
}
override suspend fun getCurrentWalletTokensBalance(
networkId: String,
extraTokens: List<Currency>,
derivationPath: String?,
): Map<String, ProxyAmount> {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val walletManager = getActualWalletManager(blockchain, derivationPath)
// workaround for get balance for tokens that doesn't exist in wallet
val extraTokensToLoadBalance = extraTokens
@ -177,9 +152,9 @@ class UserWalletManagerImpl(
return balances
}
override fun getNativeTokenBalance(networkId: String): ProxyAmount? {
override fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.wallet.amounts.firstNotNullOfOrNull {
it.takeIf { it.key is AmountType.Coin }
}?.value?.let {
@ -205,63 +180,13 @@ class UserWalletManagerImpl(
)
}
private suspend fun getOrCreateBlockchain(
blockchainNetwork: BlockchainNetwork,
blockchain: Blockchain,
): WalletManager {
val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" }
val scanResponse = requireNotNull(appStateHolder.scanResponse) { "scanResponse not found" }
var walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
if (walletManager == null) {
walletManager = walletManagerFactory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = blockchain,
derivationParams = createDerivationParams(card.derivationStyle),
)
val action = WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork,
walletManager = walletManager,
save = true,
)
val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" }
mainStore.dispatchOnMain(action)
// workaround to wait until blockchain adds to walletStores and update appStateHolder.walletState
delay(DELAY_UPDATE_WALLET)
}
return requireNotNull(walletManager) { "cant create walletManager" }
}
private fun addNonNativeTokenToWalletAction(token: NonNativeToken, card: CardDTO, blockchain: Blockchain): Action {
return WalletAction.MultiWallet.AddToken(
token = Token(
id = token.id,
name = token.name,
symbol = token.symbol,
contractAddress = token.contractAddress,
decimals = token.decimalCount,
),
blockchain = BlockchainNetwork(
blockchain,
card,
),
save = true,
)
}
override fun refreshWallet() {
// workaround, should update wallet after transaction
appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh)
}
private fun createDerivationParams(derivationStyle: DerivationStyle?): DerivationParams? {
// todo clarify if its need to add Custom
return derivationStyle?.let { DerivationParams.Default(derivationStyle) }
}
private fun getActualWalletManager(blockchain: Blockchain): WalletManager {
val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList())
return requireNotNull(appStateHolder.walletState?.getWalletManager(blockchainNetwork)) {
"No wallet manager found"
}
@ -269,7 +194,6 @@ class UserWalletManagerImpl(
companion object {
private const val HEX_PREFIX = "0x"
private const val DELAY_UPDATE_WALLET = 500L
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.proxy.di
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
@ -29,14 +29,16 @@ class ProxyModule {
fun provideUserWalletManager(appStateHolder: AppStateHolder): UserWalletManager {
return UserWalletManagerImpl(
appStateHolder = appStateHolder,
walletManagerFactory = WalletManagerFactory(),
)
}
@Provides
@Singleton
fun provideTransactionManager(appStateHolder: AppStateHolder): TransactionManager {
return TransactionManagerImpl(appStateHolder)
fun provideTransactionManager(
appStateHolder: AppStateHolder,
analytics: AnalyticsEventHandler,
): TransactionManager {
return TransactionManagerImpl(appStateHolder, analytics)
}
@Provides

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="42dp">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="42dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
android:id="@+id/guideline_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/tv_currency"
@ -64,16 +64,19 @@
android:id="@+id/tv_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text_tertiary"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_amount"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_horizontal"
app:lineHeight="20dp"
tools:text="Unreachable..." />
tools:text="Transaction in progress and it very long text with some strange status"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_exchange_rate"
@ -86,5 +89,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_horizontal"
app:lineHeight="20dp"
tools:text="46 908 $" />
tools:text="46 908 $"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -62,11 +62,11 @@ object Versions {
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-168"
const val tangemBlockchainSdk = "develop-174"
// const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds
const val tangemCardSdk = "develop-191"
// const val tangemCardSgk = "0.0.1" // Keep it! - used for local builds
const val tangemCardSdk = "develop-199"
// const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds
// endregion Tangem
// region Tools

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.api.common
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.common.json.TangemSdkAdapter
import retrofit2.converter.moshi.MoshiConverterFactory
/**
@ -15,6 +16,7 @@ object MoshiConverter {
val networkMoshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(BigDecimalAdapter())
.add(TangemSdkAdapter.ByteArrayAdapter())
.build()
val networkMoshiConverter: MoshiConverterFactory = MoshiConverterFactory.create(networkMoshi)

View file

@ -99,7 +99,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
),
infuraProjectId = configValues.infuraProjectId,
tronGridApiKey = configValues.tronGridApiKey,
saltPayAuthToken = configValues.saltPay.credentials.token,
saltPayAuthToken = configValues.saltPay.credentials.basicAuthToken,
nowNodeCredentials = NowNodeCredentials(configValues.nowNodesApiKey),
getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey),
),

View file

@ -1,7 +1,5 @@
package com.tangem.datasource.config.models
import org.spongycastle.util.encoders.Base64.toBase64String
/**
[REDACTED_AUTHOR]
*/
@ -34,5 +32,5 @@ data class Credentials(
val user: String,
val password: String,
) {
val token: String by lazy { "Basic ${toBase64String("$user:$password".toByteArray())}" }
val basicAuthToken: String by lazy { okhttp3.Credentials.basic(user, password) }
}

View file

@ -13,15 +13,12 @@ val FirmwareVersion.Companion.SolanaTokensAvailable
get() = FirmwareVersion(4, 52)
fun CardDTO.supportedBlockchains(): List<Blockchain> {
val supportedBlockchains = when {
firmwareVersion < FirmwareVersion.MultiWalletAvailable -> {
Blockchain.fromCurve(EllipticCurve.Secp256k1)
}
else -> {
(Blockchain.fromCurve(EllipticCurve.Secp256k1) + Blockchain.fromCurve(EllipticCurve.Ed25519)).distinct()
}
val supportedBlockchains = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
Blockchain.fromCurve(EllipticCurve.Secp256k1)
} else {
wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct()
}
return supportedBlockchains
.filter { isTestCard == it.isTestnet() }
.filter { it.isSupportedInApp() }
@ -38,12 +35,8 @@ fun CardDTO.supportedTokens(): List<Blockchain> {
}
}
}
val filtered = tokensSupportedByCard.filter { isTestCard == it.isTestnet() }
return filtered
}
fun CardDTO.canHandleBlockchain(blockchain: Blockchain): Boolean {
return this.supportedBlockchains().contains(blockchain)
return tokensSupportedByCard.filter { isTestCard == it.isTestnet() }
}
fun CardDTO.canHandleToken(blockchain: Blockchain): Boolean {

View file

@ -26,7 +26,7 @@ internal class ReferralInteractorImpl(
if (tokensForReferral.isNotEmpty()) {
val currency = tokensConverter.convert(tokensForReferral.first())
deriveOrAddTokens(currency)
val publicAddress = userWalletManager.getWalletAddress(currency.networkId)
val publicAddress = userWalletManager.getWalletAddress(currency.networkId, null)
return repository.startReferral(
walletId = userWalletManager.getWalletId(),
networkId = currency.networkId,
@ -42,8 +42,8 @@ internal class ReferralInteractorImpl(
if (!derivationManager.hasDerivation(currency.networkId)) {
derivationManager.deriveMissingBlockchains(currency)
}
if (!userWalletManager.isTokenAdded(currency)) {
userWalletManager.addToken(currency)
if (!userWalletManager.isTokenAdded(currency, null)) {
userWalletManager.addToken(currency, null)
}
}

View file

@ -35,8 +35,24 @@ internal class SwapRepositoryImpl @Inject constructor(
private val approveConverter = ApproveConverter()
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
// workaround cause backend do not return arbitrum and optimism rates
val addedTokens = if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) {
tokenIds.toMutableList().apply {
add(ETHEREUM_ID)
}
} else {
tokenIds
}
return withContext(coroutineDispatcher.io) {
tangemTechApi.getRates(currencyId.lowercase(), tokenIds.joinToString(",")).rates
val rates = tangemTechApi.getRates(currencyId.lowercase(), addedTokens.joinToString(",")).rates
val ethRate = rates[ETHEREUM_ID]
rates.mapValues {
if (it.key == OPTIMISM_ID || it.key == ARBITRUM_ID) {
ethRate ?: 0.0
} else {
it.value
}
}
}
}
@ -136,4 +152,11 @@ internal class SwapRepositoryImpl @Inject constructor(
private fun getOneInchApi(networkId: String): OneInchApi {
return oneInchApiFactory.getApi(networkId)
}
companion object {
// TODO("get this ids from blockchain enum later")
private const val OPTIMISM_ID = "optimistic-ethereum"
private const val ARBITRUM_ID = "arbitrum-one"
private const val ETHEREUM_ID = "ethereum"
}
}

View file

@ -1,16 +1,18 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokensDataState
import com.tangem.feature.swap.domain.models.ui.TxState
interface SwapInteractor {
fun initDerivationPath(derivationPath: String?)
/**
* Init tokens to swap, load tokens list available to swap for given network
*
@ -42,15 +44,13 @@ interface SwapInteractor {
* Gives permission to swap, this starts scan card process
*
* @param networkId network in which selected token
* @param estimatedGas estimated gas for transaction
* @param transactionData tx data to give approve, it loaded from 1inch in findBestQuote if needed
* @param approveData tx data to give approve, it loaded from 1inch in findBestQuote if needed
* @param forTokenContractAddress token contract address for which needs permission
*/
@Throws(IllegalStateException::class)
suspend fun givePermissionToSwap(
networkId: String,
estimatedGas: Int,
transactionData: ApproveModel,
approveData: RequestApproveStateData,
forTokenContractAddress: String,
): TxState
@ -76,7 +76,7 @@ interface SwapInteractor {
* Starts swap transaction, perform sign transaction
*
* @param networkId network for tokens
* @param swapData tx data to swap, contains data to sign
* @param swapStateData tx data to swap, contains data to sign
* @param currencyToSend [Currency]
* @param currencyToGet [Currency]
* @param amountToSwap amount to swap
@ -85,7 +85,7 @@ interface SwapInteractor {
@Throws(IllegalStateException::class)
suspend fun onSwap(
networkId: String,
swapData: SwapDataModel,
swapStateData: SwapStateData,
currencyToSend: Currency,
currencyToGet: Currency,
amountToSwap: String,

View file

@ -4,10 +4,8 @@ import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.AmountFormatter
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
@ -15,6 +13,7 @@ import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.PreselectTokens
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokenBalanceData
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
@ -40,6 +39,11 @@ internal class SwapInteractorImpl @Inject constructor(
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
private val amountFormatter = AmountFormatter()
private var derivationPath: String? = null
override fun initDerivationPath(derivationPath: String?) {
this.derivationPath = derivationPath
}
override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState {
val networkId = initialCurrency.networkId
@ -64,7 +68,7 @@ internal class SwapInteractorImpl @Inject constructor(
.filter {
!loadedOnWalletsMap.contains(it.symbol)
}
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList())
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList(), derivationPath)
.mapValues { SwapAmount(it.value.value, it.value.decimals) }
val appCurrency = userWalletManager.getUserAppCurrency()
val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id })
@ -110,24 +114,21 @@ internal class SwapInteractorImpl @Inject constructor(
override suspend fun givePermissionToSwap(
networkId: String,
estimatedGas: Int,
transactionData: ApproveModel,
approveData: RequestApproveStateData,
forTokenContractAddress: String,
): TxState {
val increasedEstimatedGas = increaseByPercents(TWENTY_FIVE_PERCENTS, estimatedGas)
val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice")
val fee = transactionManager.calculateFee(networkId, gasPrice.toPlainString(), increasedEstimatedGas)
val result = transactionManager.sendApproveTransaction(
networkId = networkId,
feeAmount = fee,
estimatedGas = increasedEstimatedGas,
destinationAddress = transactionData.toAddress,
dataToSign = transactionData.data,
feeAmount = approveData.fee,
gasLimit = approveData.gasLimit,
destinationAddress = approveData.approveModel.toAddress,
dataToSign = approveData.approveModel.data,
derivationPath = derivationPath,
)
return when (result) {
is SendTxResult.Success -> {
allowPermissionsHandler.addAddressToInProgress(forTokenContractAddress)
TxState.TxSent(txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "")
TxState.TxSent(txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "")
}
SendTxResult.UserCancelledError -> TxState.UserCancelled
is SendTxResult.BlockchainSdkError -> TxState.BlockchainError
@ -152,19 +153,12 @@ internal class SwapInteractorImpl @Inject constructor(
val fromTokenAddress = getTokenAddress(fromToken)
val toTokenAddress = getTokenAddress(toToken)
val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress)
val fee = getAndUpdateFee(networkId, fromToken)
val isBalanceEnough = isBalanceEnough(fromToken, amount, fee)
val isFeeEnough = checkFeeIsEnough(fee, amount, networkId, fromToken)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId)
transactionManager.updateWalletManager(networkId, derivationPath)
}
val preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceEnough,
isFeeEnough = isFeeEnough,
)
return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) {
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
loadSwapData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
@ -172,7 +166,6 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken = fromToken,
toToken = toToken,
amount = amount,
preparedSwapConfigState = preparedSwapConfigState,
)
} else {
loadQuoteData(
@ -182,44 +175,45 @@ internal class SwapInteractorImpl @Inject constructor(
amount = amount,
fromToken = fromToken,
toToken = toToken,
preparedSwapConfigState = preparedSwapConfigState,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
}
override suspend fun onSwap(
networkId: String,
swapData: SwapDataModel,
swapStateData: SwapStateData,
currencyToSend: Currency,
currencyToGet: Currency,
amountToSwap: String,
): TxState {
val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" }
val estimatedGas =
increaseByPercents(TWENTY_FIVE_PERCENTS, swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS)
val fee = transactionManager.calculateFee(
networkId = networkId,
gasPrice = swapData.transaction.gasPrice,
estimatedGas = estimatedGas,
)
val result = transactionManager.sendTransaction(
networkId = networkId,
amountToSend = amount,
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
feeAmount = fee,
estimatedGas = estimatedGas,
destinationAddress = swapData.transaction.toWalletAddress,
dataToSign = swapData.transaction.data,
feeAmount = swapStateData.fee,
gasLimit = swapStateData.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data,
isSwap = true,
derivationPath = derivationPath,
)
return when (result) {
is SendTxResult.Success -> {
userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet))
userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet), derivationPath)
userWalletManager.refreshWallet()
TxState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(swapData.fromTokenAmount, currencyToSend.symbol),
toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToGet.symbol),
txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "",
fromAmount = amountFormatter.formatSwapAmountToUI(
swapStateData.swapModel.fromTokenAmount,
currencyToSend.symbol,
),
toAmount = amountFormatter.formatSwapAmountToUI(
swapStateData.swapModel.toTokenAmount,
currencyToGet.symbol,
),
txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "",
)
}
SendTxResult.UserCancelledError -> TxState.UserCancelled
@ -294,30 +288,11 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun getAndUpdateFee(networkId: String, fromToken: Currency): BigDecimal? {
val lastFee = cache.getLastFeeForNetwork(networkId)
if (lastFee == null) {
if (userWalletManager.getNativeTokenBalance(networkId)?.value?.compareTo(BigDecimal.ZERO) == 0) {
return null
}
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
val fee = transactionManager.getFee(
networkId,
BigDecimal.ZERO,
cryptoCurrencyConverter.convert(fromToken),
transactionData.toAddress,
).value
cache.cacheLastFeeForNetwork(fee, networkId)
return fee
}
return lastFee
}
private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean {
val allowance = repository.checkTokensSpendAllowance(
networkId = networkId,
tokenAddress = fromTokenAddress,
walletAddress = userWalletManager.getWalletAddress(networkId),
walletAddress = userWalletManager.getWalletAddress(networkId, derivationPath),
)
return allowance.error == DataError.NoError && allowance.dataModel != ZERO_BALANCE
}
@ -351,7 +326,8 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
fromToken: Currency,
toToken: Currency,
preparedSwapConfigState: PreparedSwapConfigState,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): SwapState {
repository.findBestQuote(
networkId = networkId,
@ -361,35 +337,25 @@ internal class SwapInteractorImpl @Inject constructor(
).let { quotes ->
val quoteDataModel = quotes.dataModel
if (quoteDataModel != null) {
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
val fee = transactionManager.calculateFee(
networkId = networkId,
estimatedGas = quoteDataModel.estimatedGas,
gasPrice = transactionData.gasPrice,
)
val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = fee,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val swapState = updateBalances(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
fromTokenAmount = quoteDataModel.fromTokenAmount,
toTokenAmount = quoteDataModel.toTokenAmount,
formattedFee = formattedFee,
preparedSwapConfigState = preparedSwapConfigState,
swapDataModel = null,
swapStateData = null,
formattedFee = null,
)
return updatePermissionState(
networkId = networkId,
fromToken = fromToken,
quotesLoadedState = swapState,
estimatedGas = quoteDataModel.estimatedGas,
transactionData = transactionData,
formattedFee = formattedFee,
).copy(
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
isFeeEnough = true,
),
)
} else {
return SwapState.SwapError(quotes.error)
@ -399,13 +365,11 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun getFormattedFiatFee(
networkId: String,
fromTokenId: String,
toTokenId: String,
fee: BigDecimal,
): String {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(appCurrency.code, listOf(fromTokenId, toTokenId, nativeToken.id))
val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id))
return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
}.orEmpty()
@ -422,7 +386,6 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken: Currency,
toToken: Currency,
amount: SwapAmount,
preparedSwapConfigState: PreparedSwapConfigState,
): SwapState {
repository.prepareSwapTransaction(
networkId = networkId,
@ -434,17 +397,27 @@ internal class SwapInteractorImpl @Inject constructor(
).let {
val swapData = it.dataModel
if (swapData != null) {
val fee = transactionManager.calculateFee(
val feeData = transactionManager.getFee(
networkId = networkId,
estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS,
gasPrice = swapData.transaction.gasPrice,
amountToSend = amount.value,
currencyToSend = cryptoCurrencyConverter.convert(fromToken),
destinationAddress = swapData.transaction.toWalletAddress,
data = swapData.transaction.data,
derivationPath = derivationPath,
)
val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee)
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = fee,
amount = feeData.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeData.fee.value)
val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,
)
val swapState = updateBalances(
networkId = networkId,
fromToken = fromToken,
@ -452,11 +425,19 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount = swapData.fromTokenAmount,
toTokenAmount = swapData.toTokenAmount,
formattedFee = formattedFee,
preparedSwapConfigState = preparedSwapConfigState,
swapDataModel = swapData,
swapStateData = SwapStateData(
gasLimit = feeData.gasLimit.toInt(),
fee = feeData.fee.value,
swapModel = swapData,
),
)
return swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
isFeeEnough = isFeeEnough,
),
)
} else {
return SwapState.SwapError(it.error)
@ -471,9 +452,8 @@ internal class SwapInteractorImpl @Inject constructor(
toToken: Currency,
fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount,
formattedFee: String,
preparedSwapConfigState: PreparedSwapConfigState,
swapDataModel: SwapDataModel?,
formattedFee: String?,
swapStateData: SwapStateData?,
): SwapState.QuotesLoadedState {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
@ -511,21 +491,18 @@ internal class SwapInteractorImpl @Inject constructor(
toRate = rates[toToken.id] ?: 0.0,
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
preparedSwapConfigState = preparedSwapConfigState,
swapDataModel = swapDataModel,
swapDataModel = swapStateData,
tangemFee = getTangemFee(),
)
}
@Suppress("LongParameterList")
private fun updatePermissionState(
private suspend fun updatePermissionState(
networkId: String,
fromToken: Currency,
quotesLoadedState: SwapState.QuotesLoadedState,
estimatedGas: Int,
transactionData: ApproveModel,
formattedFee: String,
): SwapState.QuotesLoadedState {
// if token balance ZERO not show permission state to avoid user to spend money for fee
val isTokenZeroBalance = getTokenBalance(fromToken).value.compareTo(BigDecimal.ZERO) == 0
if (isTokenZeroBalance) {
return quotesLoadedState.copy(
@ -537,6 +514,21 @@ internal class SwapInteractorImpl @Inject constructor(
permissionState = PermissionDataState.PermissionLoading,
)
}
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId),
destinationAddress = transactionData.toAddress,
data = transactionData.data,
derivationPath = derivationPath,
)
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeData.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
return quotesLoadedState.copy(
permissionState = PermissionDataState.PermissionReadyForRequest(
currency = fromToken.symbol,
@ -545,7 +537,8 @@ internal class SwapInteractorImpl @Inject constructor(
spenderAddress = transactionData.toAddress,
fee = formattedFee,
requestApproveData = RequestApproveStateData(
estimatedGas = estimatedGas,
fee = feeData.fee.value,
gasLimit = feeData.gasLimit.toInt(),
approveModel = transactionData,
),
),
@ -559,6 +552,7 @@ internal class SwapInteractorImpl @Inject constructor(
userWalletManager.getCurrentWalletTokensBalance(
networkId = networkId,
extraTokens = tokensToSync.map { cryptoCurrencyConverter.convert(it) },
derivationPath = derivationPath,
)
cache.cacheBalances(tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) })
}
@ -574,7 +568,7 @@ internal class SwapInteractorImpl @Inject constructor(
}
private fun getWalletAddress(networkId: String): String {
return userWalletManager.getWalletAddress(networkId)
return userWalletManager.getWalletAddress(networkId, derivationPath)
}
private fun getTokenAddress(currency: Currency): String {
@ -597,7 +591,7 @@ internal class SwapInteractorImpl @Inject constructor(
if (fee == null) {
return false
}
val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId)
val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId, derivationPath)
val percentsToFeeIncrease = BigDecimal.valueOf(INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT)
return when (fromToken) {
is Currency.NativeToken -> {
@ -613,11 +607,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
@Suppress("MagicNumber")
private fun increaseByPercents(percents: Int, value: Int): Int {
return value * (percents / 100 + 1)
}
private fun toBigDecimalOrNull(amountToSwap: String): BigDecimal? {
return amountToSwap.replace(",", ".").toBigDecimalOrNull()
}
@ -636,10 +625,8 @@ internal class SwapInteractorImpl @Inject constructor(
companion object {
private const val DEFAULT_SLIPPAGE = 2
private const val ZERO_BALANCE = "0"
private const val DEFAULT_GAS = 300000
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
private const val TWENTY_FIVE_PERCENTS = 25
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.5
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4
private const val USDT_SYMBOL = "USDT"
private const val USDC_SYMBOL = "USDC"
private const val INFINITY_SYMBOL = ""

View file

@ -5,18 +5,23 @@ import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import java.math.BigDecimal
sealed interface SwapState {
data class QuotesLoadedState(
val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo,
val fee: String,
val fee: String?,
val priceImpact: Float,
val networkCurrency: String,
val preparedSwapConfigState: PreparedSwapConfigState,
val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = false,
isBalanceEnough = false,
isFeeEnough = false,
),
val permissionState: PermissionDataState = PermissionDataState.Empty,
val swapDataModel: SwapDataModel? = null,
val swapDataModel: SwapStateData? = null,
val tangemFee: Double,
) : SwapState
@ -55,6 +60,13 @@ data class TokenSwapInfo(
)
data class RequestApproveStateData(
val estimatedGas: Int,
val fee: BigDecimal,
val gasLimit: Int,
val approveModel: ApproveModel,
)
data class SwapStateData(
val fee: BigDecimal,
val gasLimit: Int,
val swapModel: SwapDataModel,
)

View file

@ -74,6 +74,7 @@ dependencies {
/** Other libraries */
implementation(Library.composeShimmer)
implementation(Library.kotlinSerialization)
implementation(Library.timber)
/** DI */
implementation(Library.hilt)

View file

@ -72,5 +72,6 @@ class SwapFragment : Fragment() {
companion object {
const val CURRENCY_BUNDLE_KEY = "swap_currency"
const val DERIVATION_PATH = "DERIVATION_STYLE"
}
}

View file

@ -131,9 +131,9 @@ internal class StateBuilder(val actions: UiActions) {
warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt()))
}
val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) {
FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee)
FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
} else {
FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee)
FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
}
return uiStateHolder.copy(
sendCardData = SwapCardData(

View file

@ -41,6 +41,7 @@ import com.tangem.core.ui.components.SimpleOkDialog
import com.tangem.core.ui.components.SmallInfoCard
import com.tangem.core.ui.components.SmallInfoCardWithDisclaimer
import com.tangem.core.ui.components.SmallInfoCardWithWarning
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.WarningCard
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.keyboardAsState
@ -254,12 +255,14 @@ private fun FeeItem(feeState: FeeState, currency: String) {
val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%")
when (feeState) {
is FeeState.Loaded -> {
SmallInfoCardWithDisclaimer(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
isLoading = false,
)
if (feeState.fee.isNotEmpty()) {
SmallInfoCardWithDisclaimer(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
isLoading = false,
)
}
}
FeeState.Loading -> {
SmallInfoCardWithDisclaimer(
@ -270,16 +273,18 @@ private fun FeeItem(feeState: FeeState, currency: String) {
)
}
is FeeState.NotEnoughFundsWarning -> {
SmallInfoCardWithWarning(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
warningText = stringResource(
id = R.string.swapping_not_enough_funds_for_fee,
currency,
currency,
),
)
if (feeState.fee.isNotEmpty()) {
SmallInfoCardWithWarning(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
warningText = stringResource(
id = R.string.swapping_not_enough_funds_for_fee,
currency,
currency,
),
)
}
}
is FeeState.Empty -> {
SmallInfoCard(startText = titleString, endText = "")
@ -343,6 +348,7 @@ private fun SwapWarnings(
// )
// }
}
SpacerH8()
}
}
}

View file

@ -1,15 +1,14 @@
package com.tangem.feature.swap.viewmodels
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapStateData
data class SwapProcessDataState(
val networkId: String,
val fromCurrency: Currency? = null,
val toCurrency: Currency? = null,
val amount: String? = null,
val estimatedGas: Int? = null,
val approveModel: ApproveModel? = null,
val swapModel: SwapDataModel? = null,
val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapStateData? = null,
)

View file

@ -1,6 +1,5 @@
package com.tangem.feature.swap.viewmodels
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -13,11 +12,11 @@ import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.UiActions
@ -32,6 +31,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
@ -52,6 +52,7 @@ internal class SwapViewModel @Inject constructor(
savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY]
?: error("no expected parameter Currency found"),
)
private val derivationPath = savedStateHandle.get<String>(SwapFragment.DERIVATION_PATH)
private val stateBuilder = StateBuilder(
actions = createUiActions(),
@ -78,6 +79,7 @@ internal class SwapViewModel @Inject constructor(
get() = swapRouter.currentScreen
init {
swapInteractor.initDerivationPath(derivationPath)
initTokens(currency)
}
@ -122,7 +124,7 @@ internal class SwapViewModel @Inject constructor(
)
}
.onFailure {
Log.e("SwapViewModel", it.message ?: it.cause.toString())
Timber.e(it)
}
}
}
@ -169,9 +171,8 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
dataState = dataState.copy(
amount = amount,
swapModel = null,
estimatedGas = null,
approveModel = null,
swapDataModel = null,
approveDataModel = null,
)
swapInteractor.findBestQuote(
networkId = dataState.networkId,
@ -198,25 +199,26 @@ internal class SwapViewModel @Inject constructor(
)
}
is SwapState.SwapError -> {
Timber.e("SwapError when loading quotes ${swapState.error}")
uiState = stateBuilder.mapError(uiState, swapState.error) { startLoadingQuotesFromLastState() }
}
}
},
onError = {
Timber.e("Error when loading quotes: $it")
uiState = stateBuilder.addWarning(uiState, null) { startLoadingQuotesFromLastState() }
},
)
}
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapDataModel?) {
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) {
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
dataState.copy(
estimatedGas = permissionState.requestApproveData.estimatedGas,
approveModel = permissionState.requestApproveData.approveModel,
approveDataModel = permissionState.requestApproveData,
)
} else {
dataState.copy(
swapModel = swapDataModel,
swapDataModel = swapDataModel,
)
}
}
@ -228,7 +230,7 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
swapInteractor.onSwap(
networkId = dataState.networkId,
swapData = requireNotNull(dataState.swapModel),
swapStateData = requireNotNull(dataState.swapDataModel),
currencyToSend = requireNotNull(dataState.fromCurrency),
currencyToGet = requireNotNull(dataState.toCurrency),
amountToSwap = requireNotNull(dataState.amount),
@ -274,8 +276,7 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
swapInteractor.givePermissionToSwap(
networkId = dataState.networkId,
estimatedGas = dataState.estimatedGas!!,
transactionData = dataState.approveModel!!,
approveData = dataState.approveDataModel!!,
forTokenContractAddress = (dataState.fromCurrency as? Currency.NonNativeToken)?.contractAddress
?: "",
)

View file

@ -1,20 +1,22 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
interface TransactionManager {
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
derivationPath: String?,
): SendTxResult
@Suppress("LongParameterList")
@ -23,26 +25,30 @@ interface TransactionManager {
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
isSwap: Boolean,
currencyToSend: Currency,
derivationPath: String?,
): SendTxResult
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun getFee(
networkId: String,
amountToSend: BigDecimal,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount
data: String?,
derivationPath: String?,
): ProxyFee
@Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int
@Throws(IllegalStateException::class)
suspend fun updateWalletManager(networkId: String)
suspend fun updateWalletManager(networkId: String, derivationPath: String?)
fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal

View file

@ -29,15 +29,16 @@ interface UserWalletManager {
* @param currency to receive referral payments
*/
@Throws(IllegalStateException::class)
suspend fun isTokenAdded(currency: Currency): Boolean
suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean
/**
* Adds token to wallet if its not
*
* @param currency to add to wallet
* @param derivationPath if null uses default
*/
@Throws(IllegalStateException::class)
suspend fun addToken(currency: Currency)
suspend fun addToken(currency: Currency, derivationPath: String?)
fun refreshWallet()
@ -45,21 +46,27 @@ interface UserWalletManager {
* Returns wallet public address for token
*
* @param networkId for currency
* @param derivationPath if null uses default
*/
@Throws(IllegalStateException::class)
fun getWalletAddress(networkId: String): String
fun getWalletAddress(networkId: String, derivationPath: String?): String
/**
* Return balances from wallet found by networkId
*
* @param networkId
* @param derivationPath if null uses default
* @return map of <Symbol, [ProxyAmount]>
*/
@Throws(IllegalStateException::class)
suspend fun getCurrentWalletTokensBalance(networkId: String, extraTokens: List<Currency>): Map<String, ProxyAmount>
suspend fun getCurrentWalletTokensBalance(
networkId: String,
extraTokens: List<Currency>,
derivationPath: String?,
): Map<String, ProxyAmount>
@Throws(IllegalStateException::class)
fun getNativeTokenBalance(networkId: String): ProxyAmount?
fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount?
/**
* @param networkId
@ -74,5 +81,5 @@ interface UserWalletManager {
fun getUserAppCurrency(): ProxyFiatCurrency
@Throws(IllegalStateException::class)
fun getLastTransactionHash(networkId: String): String?
fun getLastTransactionHash(networkId: String, derivationPath: String?): String?
}

View file

@ -0,0 +1,8 @@
package com.tangem.lib.crypto.models
import java.math.BigInteger
data class ProxyFee(
val gasLimit: BigInteger,
val fee: ProxyAmount,
)