Updated on 2026-08-14
This commit is contained in:
commit
629d42fc34
79 changed files with 1241 additions and 2127 deletions
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = "∞"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -74,6 +74,7 @@ dependencies {
|
|||
/** Other libraries */
|
||||
implementation(Library.composeShimmer)
|
||||
implementation(Library.kotlinSerialization)
|
||||
implementation(Library.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(Library.hilt)
|
||||
|
|
|
|||
|
|
@ -72,5 +72,6 @@ class SwapFragment : Fragment() {
|
|||
|
||||
companion object {
|
||||
const val CURRENCY_BUNDLE_KEY = "swap_currency"
|
||||
const val DERIVATION_PATH = "DERIVATION_STYLE"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
?: "",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.lib.crypto.models
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
data class ProxyFee(
|
||||
val gasLimit: BigInteger,
|
||||
val fee: ProxyAmount,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue