diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 299c51efa0..9ee7b7b254 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ In case you have found any issues with an existing app, feel free to report it d * Was your app affected eventually or right after installing the update? * Do you use the Google Play version of application, APK file from releases or running it via Android Studio (if other, mention what)? If you installed it manually, please provide backtrace if possible. * Provide steps to reproduce your bug -* If helpful, provide screenshots (or videos) of every step (addresses and balances can be blured on your choice) +* If helpful, provide screenshots (or videos) of every step (addresses and balances can be blurred on your choice) * (OPTIONAL) Provide your telegram username, and be sure to join [Our Community Channel](https://t.me/fearlesshappiness) on Telegram, so our admins can ping you here in case they need some assistance from your side Notice, that if you don't leave your Telegram username, we might ask directly in your created issue for more information if such is required. @@ -34,7 +34,7 @@ Even if you provide mockups, please understand that our design team will review If you would like to help us by contributing writing the code, please follow next steps: * Always create and issue prior to opening Pull Request (hereinafter the PR), or your PR less likely to be reviewed -* Remember, that in that case you are required to provide full description for you feature in created issue, otherwise it would be hard for us to understand what you're trying to add to our codebase +* Remember, that in that case you are required to provide full description of your feature in created issue, otherwise it would be hard for us to understand what you're trying to add to our codebase * Follow the coding standards guidelines (TO BE PROVIDED LATER), or you will be asked to make changes to follow them * Please avoid huge PRs, and if your contribution really requires lots of files, please make a base branch with series of small PRs on your fork, and then provide link to those PRs in your big one PR in our repository * Provide steps for QA engineer to test your functionality (they should cover requirements from your issue) diff --git a/README.md b/README.md index 69d1792282..8cabeee351 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,15 @@ Track features development: [board link](https://soramitsucoltd.aha.io/shared/34 ## How to build -To build Fearless Wallet Android project, you need to provide several keys either in enviroment variables or in `local.properties` file: +To build Fearless Wallet Android project, you need to provide several keys either in environment variables or in `local.properties` file: ### Moonpay properties ``` MOONPAY_TEST_SECRET=stub MOONPAY_PRODUCTION_SECRET=stub ``` -Note, that with stub keys buy via moonpay will not work correctly. However, other parts of application will not be affected. + +Note, that with stub keys buy via moonpay will not work correctly. However, other parts of the application will not be affected. ### Sora CARD SDK diff --git a/app/build.gradle b/app/build.gradle index 7f3ef17680..83ef89f8bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,8 +130,8 @@ android { resources.excludes.add("META-INF/*") } - configurations{ - all*.exclude module: 'bcprov-jdk15on' + configurations.configureEach { + exclude group: "org.bouncycastle", module: "bcprov-jdk15on" } } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 095be24e50..2b5dfd5fee 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -158,6 +158,7 @@ -dontwarn org.jetbrains.kotlin.diagnostics.rendering.Renderers -dontwarn org.jetbrains.kotlin.diagnostics.rendering.SmartDescriptorRenderer -dontwarn org.jetbrains.kotlin.diagnostics.rendering.SmartTypeRenderer +-dontwarn org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar -dontwarn javax.naming.InvalidNameException -dontwarn javax.naming.NamingException @@ -250,7 +251,3 @@ # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken - --keepclassmembers class * { - @com.google.api.client.util.Key ; -} \ No newline at end of file diff --git a/app/src/androidTest/java/jp/co/soramitsu/ChainSyncIntegrationTest.kt b/app/src/androidTest/java/jp/co/soramitsu/ChainSyncIntegrationTest.kt deleted file mode 100644 index 0f3296e43f..0000000000 --- a/app/src/androidTest/java/jp/co/soramitsu/ChainSyncIntegrationTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package jp.co.soramitsu - -import androidx.room.Room -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import dagger.Component -import jp.co.soramitsu.common.data.network.NetworkApiCreator -import jp.co.soramitsu.common.di.CommonApi -import jp.co.soramitsu.common.di.FeatureContainer -import jp.co.soramitsu.coredb.AppDatabase -import jp.co.soramitsu.runtime.multiNetwork.chain.ChainSyncService -import jp.co.soramitsu.runtime.multiNetwork.chain.remote.ChainFetcher -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import javax.inject.Inject - - -@Component( - dependencies = [ - CommonApi::class, - ] -) -interface TestAppComponent { - - fun inject(test: ChainSyncServiceIntegrationTest) -} - -@RunWith(AndroidJUnit4::class) -class ChainSyncServiceIntegrationTest { - - private val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext - private val featureContainer = context as FeatureContainer - - @Inject - lateinit var networkApiCreator: NetworkApiCreator - - lateinit var chainSyncService: ChainSyncService - - @Before - fun setup() { - val component = DaggerTestAppComponent.builder() - .commonApi(featureContainer.commonApi()) - .build() - - component.inject(this) - - val chainDao = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) - .build() - .chainDao() - - chainSyncService = ChainSyncService(chainDao, networkApiCreator.create(ChainFetcher::class.java)) - } - - @Test - fun shouldFetchAndStoreRealChains() = runBlocking { - chainSyncService.syncUp() - } -} diff --git a/app/src/main/java/jp/co/soramitsu/app/root/di/RootFeatureModule.kt b/app/src/main/java/jp/co/soramitsu/app/root/di/RootFeatureModule.kt index b25ac10926..5e2d91ad99 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/di/RootFeatureModule.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/di/RootFeatureModule.kt @@ -6,9 +6,12 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Named import jp.co.soramitsu.account.api.domain.PendulumPreInstalledAccountsScenario +import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository +import jp.co.soramitsu.account.impl.domain.WalletSyncService import jp.co.soramitsu.app.root.domain.RootInteractor import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.core.updater.UpdateSystem +import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository @InstallIn(SingletonComponent::class) @@ -20,13 +23,19 @@ class RootFeatureModule { walletRepository: WalletRepository, @Named("BalancesUpdateSystem") walletUpdateSystem: UpdateSystem, pendulumPreInstalledAccountsScenario: PendulumPreInstalledAccountsScenario, - preferences: Preferences + preferences: Preferences, + accountRepository: AccountRepository, + walletSyncService: WalletSyncService, + chainRegistry: ChainRegistry ): RootInteractor { return RootInteractor( walletUpdateSystem, walletRepository, pendulumPreInstalledAccountsScenario, - preferences + preferences, + accountRepository, + walletSyncService, + chainRegistry ) } } diff --git a/app/src/main/java/jp/co/soramitsu/app/root/domain/RootInteractor.kt b/app/src/main/java/jp/co/soramitsu/app/root/domain/RootInteractor.kt index 033c4d2f2f..0eab597bf6 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/domain/RootInteractor.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/domain/RootInteractor.kt @@ -2,6 +2,8 @@ package jp.co.soramitsu.app.root.domain import com.walletconnect.web3.wallet.client.Web3Wallet import jp.co.soramitsu.account.api.domain.PendulumPreInstalledAccountsScenario +import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository +import jp.co.soramitsu.account.impl.domain.WalletSyncService import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.common.data.storage.appConfig import jp.co.soramitsu.common.domain.model.AppConfig @@ -10,10 +12,13 @@ import jp.co.soramitsu.common.utils.inBackground import jp.co.soramitsu.common.utils.requireValue import jp.co.soramitsu.core.updater.UpdateSystem import jp.co.soramitsu.core.updater.Updater +import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.wallet.impl.data.buyToken.ExternalProvider import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.withContext @@ -21,10 +26,26 @@ class RootInteractor( private val updateSystem: UpdateSystem, private val walletRepository: WalletRepository, private val pendulumPreInstalledAccountsScenario: PendulumPreInstalledAccountsScenario, - private val preferences: Preferences + private val preferences: Preferences, + private val accountRepository: AccountRepository, + private val walletSyncService: WalletSyncService, + private val chainRegistry: ChainRegistry, ) { + suspend fun syncChainsConfigs(): Result { + return withContext(Dispatchers.Default) { + return@withContext chainRegistry.syncConfigs() + } + } - fun runBalancesUpdate(): Flow = updateSystem.start().inBackground() + fun runWalletsSync() { + walletSyncService.start() + } + + suspend fun runBalancesUpdate(): Flow = withContext(Dispatchers.Default) { + // await all accounts initialized + accountRepository.allMetaAccountsFlow().filter { accounts -> accounts.all { it.initialized } }.filter { it.isNotEmpty() }.first() + return@withContext updateSystem.start().inBackground() + } fun isBuyProviderRedirectLink(link: String) = ExternalProvider.REDIRECT_URL_BASE in link @@ -50,9 +71,9 @@ class RootInteractor( } } - fun chainRegistrySyncUp() = walletRepository.chainRegistrySyncUp() + fun chainRegistrySyncUp() = chainRegistry.syncUp() - suspend fun fetchFeatureToggle() = withContext(Dispatchers.Default){ pendulumPreInstalledAccountsScenario.fetchFeatureToggle() } + suspend fun fetchFeatureToggle() = withContext(Dispatchers.Default) { pendulumPreInstalledAccountsScenario.fetchFeatureToggle() } suspend fun getPendingListOfSessionRequests(topic: String) = withContext(Dispatchers.Default){ Web3Wallet.getPendingListOfSessionRequests(topic) } } diff --git a/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt b/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt index 58898cbf63..2ca6d0a52a 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt @@ -162,10 +162,10 @@ import jp.co.soramitsu.wallet.impl.presentation.transaction.detail.reward.Reward import jp.co.soramitsu.wallet.impl.presentation.transaction.detail.reward.RewardDetailsPayload import jp.co.soramitsu.wallet.impl.presentation.transaction.detail.swap.SwapDetailFragment import jp.co.soramitsu.wallet.impl.presentation.transaction.detail.transfer.TransferDetailFragment -import jp.co.soramitsu.walletconnect.impl.presentation.sessionproposal.SessionProposalFragment import jp.co.soramitsu.walletconnect.impl.presentation.chainschooser.ChainChooseFragment import jp.co.soramitsu.walletconnect.impl.presentation.connectioninfo.ConnectionInfoFragment import jp.co.soramitsu.walletconnect.impl.presentation.requestpreview.RequestPreviewFragment +import jp.co.soramitsu.walletconnect.impl.presentation.sessionproposal.SessionProposalFragment import jp.co.soramitsu.walletconnect.impl.presentation.sessionrequest.SessionRequestFragment import jp.co.soramitsu.walletconnect.impl.presentation.transactionrawdata.RawDataFragment import kotlin.coroutines.coroutineContext @@ -177,7 +177,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onCompletion @@ -185,7 +184,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.job import kotlinx.parcelize.Parcelize -import jp.co.soramitsu.common.utils.combine as combineLiveData @Parcelize class NavComponentDelayedNavigation(val globalActionId: Int, val extras: Bundle? = null) : DelayedNavigation @@ -218,14 +216,21 @@ class Navigator : activity = null } - override fun openAddFirstAccount() { + override fun openOnboarding() { navController?.navigate(R.id.action_to_onboarding, WelcomeFragment.getBundle(false)) } + override fun openCreatePincode() { + val action = PinCodeAction.Create(NavComponentDelayedNavigation(R.id.action_open_main)) + val bundle = PincodeFragment.getPinCodeBundle(action) + navController?.navigate(R.id.pincodeFragment, bundle) + } + + override fun openInitialCheckPincode() { val action = PinCodeAction.Check(NavComponentDelayedNavigation(R.id.action_open_main), ToolbarConfiguration()) val bundle = PincodeFragment.getPinCodeBundle(action) - navController?.navigateSafe(R.id.action_splash_to_pin, bundle) + navController?.navigateSafe(R.id.pincodeFragment, bundle) } private fun NavController.navigateSafe(@IdRes resId: Int, args: Bundle?) { @@ -337,12 +342,6 @@ class Navigator : navController?.navigate(delayedNavigation.globalActionId, delayedNavigation.extras, navOptions) } - override fun openCreatePincode() { - val bundle = buildCreatePinBundle() - - navController?.navigate(R.id.pincodeFragment, bundle) - } - override fun openConfirmMnemonicOnCreate(confirmMnemonicPayload: ConfirmMnemonicPayload) { val bundle = ConfirmMnemonicFragment.getBundle(confirmMnemonicPayload) @@ -1021,8 +1020,8 @@ class Navigator : } @SuppressLint("RestrictedApi") - override fun openOperationSuccessAndPopUpToNearestRelatedScreen(operationHash: String?, chainId: ChainId?, customMessage: String?) { - val bundle = SuccessFragment.getBundle(operationHash, chainId, customMessage) + override fun openOperationSuccessAndPopUpToNearestRelatedScreen(operationHash: String?, chainId: ChainId?, customMessage: String?, customTitle: String?) { + val bundle = SuccessFragment.getBundle(operationHash, chainId, customMessage, customTitle) val latestAvailableWalletConnectRelatedDestinationId = navController?.currentBackStack?.replayCache?.firstOrNull()?.last { @@ -1042,8 +1041,8 @@ class Navigator : navController?.popBackStack() } - override fun openTransferDetail(transaction: OperationParcelizeModel.Transfer, assetPayload: AssetPayload, chainHistoryType: Chain.ExternalApi.Section.Type?) { - val bundle = TransferDetailFragment.getBundle(transaction, assetPayload, chainHistoryType) + override fun openTransferDetail(transaction: OperationParcelizeModel.Transfer, assetPayload: AssetPayload, chainExplorerType: Chain.Explorer.Type?) { + val bundle = TransferDetailFragment.getBundle(transaction, assetPayload, chainExplorerType) navController?.navigate(R.id.open_transfer_detail, bundle) } @@ -1276,16 +1275,6 @@ class Navigator : navController?.navigate(R.id.root_nav_graph, bundle) } - private fun buildCreatePinBundle(): Bundle { - val delayedNavigation = NavComponentDelayedNavigation(R.id.action_open_main) - val action = PinCodeAction.Create(delayedNavigation) - return PincodeFragment.getPinCodeBundle(action) - } - - override fun openEducationalStories(stories: StoryGroupModel) { - navController?.navigate(R.id.action_splash_to_stories, StoryFragment.getBundle(stories)) - } - override fun openSelectWallet() { navController?.navigate(R.id.selectWalletFragment) } @@ -1341,17 +1330,6 @@ class Navigator : navController?.navigateUp() } - override val educationalStoriesCompleted: Flow - get() { - return combineLiveData( - navController?.currentBackStackEntry?.lifecycle?.onResumeObserver() ?: return flowOf(false), - navController?.currentBackStackEntry?.savedStateHandle?.getLiveData(StoryFragment.KEY_STORY) ?: return flowOf(false), - combiner = { (isResumed: Boolean, storiesCompleted: Boolean) -> - isResumed && storiesCompleted - } - ).asFlow() - } - override fun openExperimentalFeatures() { navController?.navigate(R.id.experimentalFragment) } @@ -1526,6 +1504,10 @@ class Navigator : navController?.navigate(R.id.nftFiltersFragment) } + override fun openManageAssets() { + navController?.navigate(R.id.manageAssetsFragment) + } + override fun openServiceScreen() { navController?.navigate(R.id.serviceFragment) } diff --git a/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootActivity.kt b/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootActivity.kt index f645a39689..bd48324f1c 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootActivity.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootActivity.kt @@ -9,7 +9,6 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri import android.os.Bundle -import android.util.Log import android.view.View import android.view.animation.Animation import android.view.animation.TranslateAnimation @@ -25,6 +24,9 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import dagger.hilt.android.AndroidEntryPoint +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL import javax.inject.Inject import jp.co.soramitsu.app.R import jp.co.soramitsu.app.root.navigation.Navigator @@ -36,6 +38,12 @@ import jp.co.soramitsu.common.utils.observe import jp.co.soramitsu.common.utils.showToast import jp.co.soramitsu.common.utils.updatePadding import jp.co.soramitsu.common.view.bottomSheet.AlertBottomSheet +import jp.co.soramitsu.runtime.BuildConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @AndroidEntryPoint class RootActivity : BaseActivity(), LifecycleObserver { @@ -66,7 +74,10 @@ class RootActivity : BaseActivity(), LifecycleObserver { navigator.attach(navController, this) rootNetworkBar.setOnApplyWindowInsetsListener { view, insets -> - view.updatePadding(top = WindowInsetsCompat.toWindowInsetsCompat(insets, view).getInsets(WindowInsetsCompat.Type.systemBars()).top) + view.updatePadding( + top = WindowInsetsCompat.toWindowInsetsCompat(insets, view) + .getInsets(WindowInsetsCompat.Type.systemBars()).top + ) insets } @@ -83,6 +94,25 @@ class RootActivity : BaseActivity(), LifecycleObserver { super.onAvailable(network) viewModel.onNetworkAvailable() } + + override fun onLost(network: Network) { + super.onLost(network) + viewModel.onConnectionLost() + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + super.onCapabilitiesChanged(network, networkCapabilities) + val hasInternetCapability = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + if (hasInternetCapability) { + viewModel.onNetworkAvailable() + } else { + viewModel.onConnectionLost() + } + } } val networkRequest = NetworkRequest.Builder() @@ -91,8 +121,44 @@ class RootActivity : BaseActivity(), LifecycleObserver { .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .build() - val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager + val connectivityManager = + getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + + lifecycleScope.launch { + while (isActive) { + delay(5_000) + val isConnected = checkNetworkStatus(connectivityManager) + val hasInternetAccess = hasInternetAccess() + withContext(Dispatchers.Main){ + if(isConnected || hasInternetAccess){ + viewModel.onNetworkAvailable() + } else { + viewModel.onConnectionLost() + } + } + } + } + } + + private suspend fun hasInternetAccess(): Boolean { + return withContext(Dispatchers.IO) { + try { + val url = URL(BuildConfig.CHAINS_URL) + val urlConnection = url.openConnection() as HttpURLConnection + urlConnection.connectTimeout = 1000 + urlConnection.connect() + urlConnection.responseCode == 200 + } catch (e: IOException) { + false + } + } + } + + private fun checkNetworkStatus(connectivityManager: ConnectivityManager): Boolean { + val activeNetwork = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) + return networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true } override fun onDestroy() { @@ -133,7 +199,7 @@ class RootActivity : BaseActivity(), LifecycleObserver { } override fun subscribe(viewModel: RootViewModel) { - viewModel.showConnectingBarFlow.observe(lifecycleScope) { show -> + viewModel.showConnectingBar.observe(lifecycleScope) { show -> when { show -> showBadConnectionView() else -> hideBadConnectionView() diff --git a/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootViewModel.kt b/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootViewModel.kt index d1d8342836..40b3426436 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootViewModel.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/presentation/RootViewModel.kt @@ -12,8 +12,6 @@ import javax.inject.Inject import jp.co.soramitsu.app.R import jp.co.soramitsu.app.root.domain.RootInteractor import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.common.mixin.api.NetworkStateUi import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.core.runtime.ChainConnection @@ -23,11 +21,14 @@ import kotlin.concurrent.timerTask import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel @@ -35,9 +36,8 @@ class RootViewModel @Inject constructor( private val interactor: RootInteractor, private val rootRouter: RootRouter, private val externalConnectionRequirementFlow: MutableStateFlow, - private val resourceManager: ResourceManager, - private val networkStateMixin: NetworkStateMixin -) : BaseViewModel(), NetworkStateUi by networkStateMixin { + private val resourceManager: ResourceManager +) : BaseViewModel() { companion object { private const val IDLE_MINUTES: Long = 20 } @@ -64,21 +64,28 @@ class RootViewModel @Inject constructor( init { viewModelScope.launch { - interactor.fetchFeatureToggle() + syncConfigs() } - checkAppVersion() observeWalletConnectEvents() } - private fun checkAppVersion() { - viewModelScope.launch { + private suspend fun syncConfigs() { + coroutineScope { + checkAppVersion() + interactor.fetchFeatureToggle() + interactor.syncChainsConfigs().onFailure { + _showNoInternetConnectionAlert.value = Event(Unit) + } + } + } + + private suspend fun checkAppVersion() { val appConfigResult = interactor.getRemoteConfig() if (appConfigResult.getOrNull()?.isCurrentVersionSupported == false) { _showUnsupportedAppVersionAlert.value = Event(Unit) } else { runBalancesUpdate() } - } } private fun runBalancesUpdate() { @@ -86,10 +93,12 @@ class RootViewModel @Inject constructor( shouldHandleResumeInternetConnection = false interactor.chainRegistrySyncUp() } - interactor.runBalancesUpdate() - .onEach { handleUpdatesSideEffect(it) } - .launchIn(this) - + viewModelScope.launch { + interactor.runWalletsSync() + interactor.runBalancesUpdate() + .onEach { handleUpdatesSideEffect(it) } + .launchIn(this) + } updatePhishingAddresses() } @@ -170,16 +179,25 @@ class RootViewModel @Inject constructor( } fun retryLoadConfigClicked() { - checkAppVersion() + viewModelScope.launch { + syncConfigs() + } } + private val _showConnectingBar = MutableStateFlow(false) + val showConnectingBar: StateFlow = _showConnectingBar fun onNetworkAvailable() { + _showConnectingBar.update { false } // todo this code triggers redundant requests and balance updates. Needs research // viewModelScope.launch { // checkAppVersion() // } } + fun onConnectionLost() { + _showConnectingBar.update { true } + } + private fun observeWalletConnectEvents() { WCDelegate.walletEvents.onEach { when (it) { diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 904ee0cb70..699ee2d05f 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -813,6 +813,11 @@ android:name="jp.co.soramitsu.nft.impl.presentation.NFTFlowFragment" android:label="nftFlowFragment" /> + + - - - - - - + tools:layout="@layout/fragment_splash"/> Unit, content: @Composable RowScope.() -> Unit ) { - TextButton( - modifier = modifier, - onClick = onClick, - shape = FearlessCorneredShape(cornerRadius = 4.dp, cornerCutLength = 6.dp), - colors = customButtonColors(backgroundColor), - border = border, - enabled = enabled, - contentPadding = contentPadding, - content = content - ) + CompositionLocalProvider( + LocalMinimumInteractiveComponentEnforcement provides false, + ) { + TextButton( + modifier = modifier, + onClick = onClick, + shape = FearlessCorneredShape(cornerRadius = 4.dp, cornerCutLength = 6.dp), + colors = customButtonColors(backgroundColor), + border = border, + enabled = enabled, + contentPadding = contentPadding, + content = content + ) + } } @Composable diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/InputWithHint.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/InputWithHint.kt index 84f25498ca..3211d07d41 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/InputWithHint.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/InputWithHint.kt @@ -80,7 +80,6 @@ private fun PreviewInputWithHint() { onInput = { }, Hint = { Row { - MarginHorizontal(margin = 6.dp) B1(text = "Public address".withNoFontPadding()) } } diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/QuickInput.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/QuickInput.kt index 31ef074515..be637cd867 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/QuickInput.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/QuickInput.kt @@ -42,46 +42,47 @@ fun QuickInput( onQuickAmountInput: (amount: Double) -> Unit = {}, onDoneClick: () -> Unit = {} ) { - val keyboardController = LocalSoftwareKeyboardController.current - Row( - modifier = modifier - .background(color = backgroundBlack.copy(alpha = 0.75f)) - .height(44.dp) - .padding(horizontal = 10.dp) - ) { - values.map { - Box( - modifier = Modifier - .fillMaxHeight() - .clickable { - onQuickAmountInput(it.value) - } - ) { - B1( - text = it.label, - modifier = Modifier - .align(Alignment.Center) - .padding(horizontal = 6.dp) - ) - } - } - Spacer(modifier = Modifier.weight(1f)) - Box( - modifier = Modifier - .fillMaxHeight() - .clickable { - keyboardController?.hide() - onDoneClick() - } - ) { - H5( - text = stringResource(id = R.string.common_done), - modifier = Modifier - .align(Alignment.Center) - .padding(horizontal = 6.dp) - ) - } - } + // todo temporary disable it till the business logic will be refactored +// val keyboardController = LocalSoftwareKeyboardController.current +// Row( +// modifier = modifier +// .background(color = backgroundBlack.copy(alpha = 0.75f)) +// .height(44.dp) +// .padding(horizontal = 10.dp) +// ) { +// values.map { +// Box( +// modifier = Modifier +// .fillMaxHeight() +// .clickable { +// onQuickAmountInput(it.value) +// } +// ) { +// B1( +// text = it.label, +// modifier = Modifier +// .align(Alignment.Center) +// .padding(horizontal = 6.dp) +// ) +// } +// } +// Spacer(modifier = Modifier.weight(1f)) +// Box( +// modifier = Modifier +// .fillMaxHeight() +// .clickable { +// keyboardController?.hide() +// onDoneClick() +// } +// ) { +// H5( +// text = stringResource(id = R.string.common_done), +// modifier = Modifier +// .align(Alignment.Center) +// .padding(horizontal = 6.dp) +// ) +// } +// } } private enum class TestQuickInput( diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/Text.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/Text.kt index 615f678b2b..f7eaefb207 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/Text.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/Text.kt @@ -1,20 +1,16 @@ package jp.co.soramitsu.common.compose.component -import android.text.TextUtils -import android.widget.TextView import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.res.ResourcesCompat -import jp.co.soramitsu.common.R +import io.github.mataku.middleellipsistext.MiddleEllipsisText import jp.co.soramitsu.common.compose.theme.bold import jp.co.soramitsu.common.compose.theme.customTypography @@ -26,12 +22,13 @@ fun B1( color: Color = Color.Unspecified, overflow: TextOverflow = TextOverflow.Clip, maxLines: Int = Int.MAX_VALUE, - onTextLayout: (TextLayoutResult) -> Unit = {} + onTextLayout: (TextLayoutResult) -> Unit = {}, + fontWeight: FontWeight = FontWeight.Normal ) { Text( textAlign = textAlign, text = text, - style = MaterialTheme.customTypography.body1, + style = MaterialTheme.customTypography.body1.copy(fontWeight = fontWeight), modifier = modifier, color = color, overflow = overflow, @@ -64,20 +61,15 @@ fun B1( fun B1EllipsizeMiddle( modifier: Modifier = Modifier, text: String, - color: Color = Color.White + textAlign: TextAlign? = null, + color: Color = Color.Unspecified ) { - AndroidView( + MiddleEllipsisText( + textAlign = textAlign, + text = text, + style = MaterialTheme.customTypography.body1, modifier = modifier, - factory = { context -> - TextView(context).apply { - setTextColor(color.toArgb()) - typeface = ResourcesCompat.getFont(context, R.font.sora_regular) - textSize = 14f - maxLines = 1 - ellipsize = TextUtils.TruncateAt.MIDDLE - } - }, - update = { it.text = text } + color = color ) } diff --git a/common/src/main/java/jp/co/soramitsu/common/data/OnboardingStoriesDataSource.kt b/common/src/main/java/jp/co/soramitsu/common/data/OnboardingStoriesDataSource.kt deleted file mode 100644 index dfecdd7648..0000000000 --- a/common/src/main/java/jp/co/soramitsu/common/data/OnboardingStoriesDataSource.kt +++ /dev/null @@ -1,42 +0,0 @@ -package jp.co.soramitsu.common.data - -import jp.co.soramitsu.common.domain.model.StoryGroup -import jp.co.soramitsu.common.R - -class OnboardingStoriesDataSource { - val stories: StoryGroup.Onboarding - get() = StoryGroup.Onboarding( - listOf( - StoryGroup.Story.Onboarding( - R.string.stories_version2_slide1_title, - R.string.stories_version2_slide1_subtitle, - R.drawable.background_story_networks, - null - ), - StoryGroup.Story.Onboarding( - R.string.stories_version2_slide2_title, - R.string.stories_version2_slide2_subtitle, - R.drawable.background_story_wallet, - null - ), - StoryGroup.Story.Onboarding( - R.string.stories_version2_slide3_title, - R.string.stories_version2_slide3_subtitle, - R.drawable.background_story_networks_2, - null - ), - StoryGroup.Story.Onboarding( - R.string.stories_version2_slide4_title, - R.string.stories_version2_slide4_subtitle, - R.drawable.background_story_chain_accounts, - null - ), - StoryGroup.Story.Onboarding( - R.string.stories_version2_slide5_title, - R.string.stories_version2_slide5_subtitle, - R.drawable.background_story_ecosystem, - R.string.stories_bottom_close_button - ) - ) - ) -} diff --git a/common/src/main/java/jp/co/soramitsu/common/data/network/BlockExplorerUrlBuilder.kt b/common/src/main/java/jp/co/soramitsu/common/data/network/BlockExplorerUrlBuilder.kt index b0203a0b3b..cedb3f3129 100644 --- a/common/src/main/java/jp/co/soramitsu/common/data/network/BlockExplorerUrlBuilder.kt +++ b/common/src/main/java/jp/co/soramitsu/common/data/network/BlockExplorerUrlBuilder.kt @@ -2,7 +2,7 @@ package jp.co.soramitsu.common.data.network class BlockExplorerUrlBuilder(private val baseUrl: String, private val types: List) { enum class Type { - EXTRINSIC, ACCOUNT, EVENT, TX, TRANSFER; + EXTRINSIC, ACCOUNT, EVENT, TX, TRANSFER, ADDRESS; val nameLowercase = name.lowercase() } diff --git a/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/AccountInfo.kt b/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/AccountInfo.kt index a97af3a062..4817fe4c42 100644 --- a/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/AccountInfo.kt +++ b/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/AccountInfo.kt @@ -15,6 +15,50 @@ import jp.co.soramitsu.shared_utils.runtime.metadata.storage interface AssetBalanceData +data class AssetBalance( + val freeInPlanks: BigInteger? = null, + val reservedInPlanks: BigInteger? = null, + val miscFrozenInPlanks: BigInteger? = null, + val feeFrozenInPlanks: BigInteger? = null, + val bondedInPlanks: BigInteger? = null, + val redeemableInPlanks: BigInteger? = null, + val unbondingInPlanks: BigInteger? = null, + val status: String? = null +) + +fun AssetBalanceData?.toAssetBalance(): AssetBalance? { + return when (this) { + null, is EmptyBalance -> AssetBalance(freeInPlanks = BigInteger.ZERO) + is AccountInfo -> { + AssetBalance( + freeInPlanks = data.free, + reservedInPlanks = data.reserved, + miscFrozenInPlanks = data.miscFrozen, + feeFrozenInPlanks = data.feeFrozen + ) + } + is AssetsAccountInfo -> { + AssetBalance( + freeInPlanks = balance + ) + } + is OrmlTokensAccountData -> { + AssetBalance( + freeInPlanks = free, + reservedInPlanks = reserved, + miscFrozenInPlanks = frozen + ) + } + is SimpleBalanceData -> { + AssetBalance( + freeInPlanks = balance + ) + } + + else -> null + } +} + object EmptyBalance : AssetBalanceData class AccountData( val free: BigInteger, @@ -96,7 +140,8 @@ class DataPoint( fun bindAccountData(dynamicInstance: Struct.Instance?) = AccountData( free = (dynamicInstance?.get("free") as? BigInteger).orZero(), reserved = (dynamicInstance?.get("reserved") as? BigInteger).orZero(), - miscFrozen = (dynamicInstance?.get("miscFrozen") as? BigInteger) ?: (dynamicInstance?.get("frozen") as? BigInteger).orZero(), + miscFrozen = (dynamicInstance?.get("miscFrozen") as? BigInteger) + ?: (dynamicInstance?.get("frozen") as? BigInteger).orZero(), feeFrozen = (dynamicInstance?.get("feeFrozen") as? BigInteger).orZero() ) @@ -134,14 +179,15 @@ fun bindEquilibriumAssetRates(scale: String?, runtime: RuntimeSnapshot): EqOracl val type = runtime.metadata.module(Modules.ORACLE).storage("PricePoints").returnType() val dynamicInstance = type.fromHexOrNull(runtime, scale).cast() - val dataPoints = dynamicInstance.getList("dataPoints").filterIsInstance().map { dataPointStruct -> - DataPoint( - price = bindNumber(dataPointStruct["price"]), - accountId = bindAccountId(dataPointStruct["accountId"]), - blockNumber = bindNumber(dataPointStruct["blockNumber"]), - timestamp = bindNumber(dataPointStruct["timestamp"]) - ) - } + val dataPoints = dynamicInstance.getList("dataPoints").filterIsInstance() + .map { dataPointStruct -> + DataPoint( + price = bindNumber(dataPointStruct["price"]), + accountId = bindAccountId(dataPointStruct["accountId"]), + blockNumber = bindNumber(dataPointStruct["blockNumber"]), + timestamp = bindNumber(dataPointStruct["timestamp"]) + ) + } return EqOraclePricePoint( blockNumber = bindNumber(dynamicInstance["blockNumber"]), timestamp = bindNumber(dynamicInstance["timestamp"]), @@ -156,7 +202,8 @@ fun bindEquilibriumAccountData(dynamicInstance: Struct.Instance?): EqAccountData val balances = balanceList?.mapNotNull { (it.getOrNull(0) as? BigInteger)?.let { eqAssetId -> val balanceEnum: DictEnum.Entry? = it.getOrNull(1).cast() - val balanceValue = if (balanceEnum?.name == "Positive") balanceEnum.value else BigInteger.ZERO + val balanceValue = + if (balanceEnum?.name == "Positive") balanceEnum.value else BigInteger.ZERO eqAssetId to balanceValue } }?.toMap().orEmpty() diff --git a/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/Primitive.kt b/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/Primitive.kt index dcf75513cf..78f8a20c84 100644 --- a/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/Primitive.kt +++ b/common/src/main/java/jp/co/soramitsu/common/data/network/runtime/binding/Primitive.kt @@ -1,9 +1,10 @@ package jp.co.soramitsu.common.data.network.runtime.binding import java.math.BigInteger +import jp.co.soramitsu.common.utils.orZero @HelperBinding -fun bindNumber(dynamicInstance: Any?): BigInteger = dynamicInstance.cast() +fun bindNumber(dynamicInstance: Any?): BigInteger = runCatching { dynamicInstance.cast() }.getOrNull().orZero() @HelperBinding fun bindString(dynamicInstance: Any?): String = dynamicInstance.cast().decodeToString() diff --git a/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt b/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt index 4bfb3daa92..570256b38a 100644 --- a/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt +++ b/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt @@ -30,9 +30,8 @@ import jp.co.soramitsu.common.data.storage.encrypt.EncryptedPreferences import jp.co.soramitsu.common.data.storage.encrypt.EncryptedPreferencesImpl import jp.co.soramitsu.common.data.storage.encrypt.EncryptionUtil import jp.co.soramitsu.common.interfaces.FileProvider -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin import jp.co.soramitsu.common.mixin.api.UpdatesMixin -import jp.co.soramitsu.common.mixin.impl.NetworkStateProvider +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.mixin.impl.UpdatesProvider import jp.co.soramitsu.common.resources.ClipboardManager import jp.co.soramitsu.common.resources.ContextManager @@ -217,5 +216,5 @@ class CommonModule { @Provides @Singleton - fun provideNetworkStateMixin(): NetworkStateMixin = NetworkStateProvider() + fun provideNetworkStateMixin() = NetworkStateService() } diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/AppVersion.kt b/common/src/main/java/jp/co/soramitsu/common/domain/AppVersion.kt index 3a25005619..ad5a95475c 100644 --- a/common/src/main/java/jp/co/soramitsu/common/domain/AppVersion.kt +++ b/common/src/main/java/jp/co/soramitsu/common/domain/AppVersion.kt @@ -2,15 +2,15 @@ package jp.co.soramitsu.common.domain import jp.co.soramitsu.common.BuildConfig -data class AppVersion(val major: Int, val minor: Int, val buildNum: Int) { +data class AppVersion(val major: Int, val minor: Int, val buildNum: Int) : Comparable { companion object { fun fromString(appVersionName: String): AppVersion { return appVersionName.split(".").let { AppVersion( - major = it[0].toIntOrNull() ?: 0, - minor = it[1].toIntOrNull() ?: 0, - buildNum = it[2].toIntOrNull() ?: 0 + major = it.getOrNull(0)?.toIntOrNull() ?: 0, + minor = it.getOrNull(1)?.toIntOrNull() ?: 0, + buildNum = it.getOrNull(2)?.toIntOrNull() ?: 0 ) } } @@ -30,6 +30,13 @@ data class AppVersion(val major: Int, val minor: Int, val buildNum: Int) { } } } + + override fun compareTo(other: AppVersion): Int = when { + major != other.major -> major - other.major + minor != other.minor -> minor - other.minor + buildNum != other.buildNum -> buildNum - other.buildNum + else -> 0 + } } // "2.0.1".before("2.0.1") false diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/GetEducationalStoriesUseCase.kt b/common/src/main/java/jp/co/soramitsu/common/domain/GetEducationalStoriesUseCase.kt deleted file mode 100644 index a0d9d05ff9..0000000000 --- a/common/src/main/java/jp/co/soramitsu/common/domain/GetEducationalStoriesUseCase.kt +++ /dev/null @@ -1,7 +0,0 @@ -package jp.co.soramitsu.common.domain - -import jp.co.soramitsu.common.data.OnboardingStoriesDataSource - -class GetEducationalStoriesUseCase(private val storiesDataSource: OnboardingStoriesDataSource) { - operator fun invoke() = storiesDataSource.stories -} diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/NetworkStateService.kt b/common/src/main/java/jp/co/soramitsu/common/domain/NetworkStateService.kt new file mode 100644 index 0000000000..f1adf4b721 --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/domain/NetworkStateService.kt @@ -0,0 +1,49 @@ +package jp.co.soramitsu.common.domain + +import jp.co.soramitsu.common.domain.model.NetworkIssueType +import jp.co.soramitsu.core.models.ChainId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update + +class NetworkStateService { + + private val connectionPoolProblems = MutableStateFlow>(emptySet()) + private val chainsSyncProblems = MutableStateFlow>(emptySet()) + + val networkIssuesFlow: Flow> = combine( + connectionPoolProblems, + chainsSyncProblems + ) { connectionPoolProblems, chainsSyncProblems -> + val nodesIssues = connectionPoolProblems.map { it to NetworkIssueType.Node } + val runtimeIssues = chainsSyncProblems.map { it to NetworkIssueType.Network } + (nodesIssues + runtimeIssues).toMap() + } + + fun notifyConnectionProblem(chainId: ChainId) { + connectionPoolProblems.update { it + chainId } + } + + fun notifyConnectionSuccess(chainId: ChainId) { + connectionPoolProblems.update { + it - chainId + } + } + + fun updateNetworkIssues(list: List) { + connectionPoolProblems.value = list.toSet() + } + + fun notifyChainSyncProblem(chainId: ChainId) { + chainsSyncProblems.update { + it + chainId + } + } + + fun notifyChainSyncSuccess(id: ChainId) { + chainsSyncProblems.update { + it - id + } + } +} diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/ShouldShowEducationalStoriesUseCase.kt b/common/src/main/java/jp/co/soramitsu/common/domain/ShouldShowEducationalStoriesUseCase.kt deleted file mode 100644 index cb0ccf1c4d..0000000000 --- a/common/src/main/java/jp/co/soramitsu/common/domain/ShouldShowEducationalStoriesUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package jp.co.soramitsu.common.domain - -import jp.co.soramitsu.common.data.storage.Preferences -import kotlin.reflect.KProperty - -class ShouldShowEducationalStoriesUseCase(private val preferences: Preferences) { - - private val key = "shouldShowEducationalStories" - - operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { - return preferences.getBoolean(key, true) - } - - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { - preferences.putBoolean(key, value) - } -} diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/model/NetworkState.kt b/common/src/main/java/jp/co/soramitsu/common/domain/model/NetworkState.kt new file mode 100644 index 0000000000..a154043b3a --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/domain/model/NetworkState.kt @@ -0,0 +1,10 @@ +package jp.co.soramitsu.common.domain.model + +data class NetworkIssue ( + val type: NetworkIssueType, + val chainId: String +) + +enum class NetworkIssueType { + Node, Network, Account +} \ No newline at end of file diff --git a/common/src/main/java/jp/co/soramitsu/common/mixin/api/NetworkStateMixin.kt b/common/src/main/java/jp/co/soramitsu/common/mixin/api/NetworkStateMixin.kt deleted file mode 100644 index 146594ff6b..0000000000 --- a/common/src/main/java/jp/co/soramitsu/common/mixin/api/NetworkStateMixin.kt +++ /dev/null @@ -1,28 +0,0 @@ -package jp.co.soramitsu.common.mixin.api - -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState -import jp.co.soramitsu.core.models.ChainId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -interface NetworkStateMixin : NetworkStateUi - -interface NetworkStateUi { - val showConnectingBarFlow: StateFlow - - val networkIssuesFlow: Flow> - - val chainConnectionsFlow: StateFlow> - - fun updateShowConnecting(isShow: Boolean) - - fun updateNetworkIssues(list: List) - - fun updateChainConnection(map: Map) - - fun notifyAssetsProblem(items: Set) - - fun isAssetHasProblems(assetId: String): Boolean - fun notifyChainSyncProblem(issue: NetworkIssueItemState) - fun notifyChainSyncSuccess(id: ChainId) -} diff --git a/common/src/main/java/jp/co/soramitsu/common/mixin/impl/NetworkStateProvider.kt b/common/src/main/java/jp/co/soramitsu/common/mixin/impl/NetworkStateProvider.kt deleted file mode 100644 index 57900e8607..0000000000 --- a/common/src/main/java/jp/co/soramitsu/common/mixin/impl/NetworkStateProvider.kt +++ /dev/null @@ -1,61 +0,0 @@ -package jp.co.soramitsu.common.mixin.impl - -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.core.models.ChainId -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine - -class NetworkStateProvider : NetworkStateMixin { - - private val connectionPoolProblems = MutableStateFlow>(emptySet()) - private val chainsSyncProblems = MutableStateFlow>(emptySet()) - private val assetsUpdateProblems = MutableStateFlow>(emptySet()) - - private val _showConnectingBarFlow = MutableStateFlow(false) - override val showConnectingBarFlow = _showConnectingBarFlow - - override val networkIssuesFlow = combine( - connectionPoolProblems, - assetsUpdateProblems, - chainsSyncProblems - ) { connectionPoolProblems, assetsUpdateProblems, chainsSyncProblems -> - connectionPoolProblems + assetsUpdateProblems + chainsSyncProblems - } - - private val _chainConnectionsFlow = MutableStateFlow>(emptyMap()) - override val chainConnectionsFlow = _chainConnectionsFlow - - override fun updateShowConnecting(isShow: Boolean) { - _showConnectingBarFlow.value = isShow - } - - override fun updateNetworkIssues(list: List) { - connectionPoolProblems.value = list.toSet() - } - - override fun updateChainConnection(map: Map) { - _chainConnectionsFlow.value = map - } - - override fun notifyAssetsProblem(items: Set) { - assetsUpdateProblems.value = items - } - - override fun isAssetHasProblems(assetId: String): Boolean { - val hasConnectionPoolProblems = connectionPoolProblems.value.any { it.assetId == assetId } - val hasAssetUpdateProblems = assetsUpdateProblems.value.any { it.assetId == assetId } - return hasConnectionPoolProblems && hasAssetUpdateProblems - } - - override fun notifyChainSyncProblem(issue: NetworkIssueItemState) { - val previousSet = chainsSyncProblems.value - val newSet = previousSet + issue - chainsSyncProblems.value = newSet - } - - override fun notifyChainSyncSuccess(id: ChainId) { - val newSet = chainsSyncProblems.value.toMutableSet().apply { removeIf { it.chainId == id } } - chainsSyncProblems.value = newSet - } -} diff --git a/common/src/main/java/jp/co/soramitsu/common/model/AssetBooleanState.kt b/common/src/main/java/jp/co/soramitsu/common/model/AssetBooleanState.kt new file mode 100644 index 0000000000..c726ef598e --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/model/AssetBooleanState.kt @@ -0,0 +1,9 @@ +package jp.co.soramitsu.common.model + +import jp.co.soramitsu.core.models.ChainId + +data class AssetBooleanState( + val chainId: ChainId, + val assetId: String, + val value: Boolean +) \ No newline at end of file diff --git a/common/src/main/java/jp/co/soramitsu/common/utils/FlowExt.kt b/common/src/main/java/jp/co/soramitsu/common/utils/FlowExt.kt index 5549c10bae..5b8f5a257a 100644 --- a/common/src/main/java/jp/co/soramitsu/common/utils/FlowExt.kt +++ b/common/src/main/java/jp/co/soramitsu/common/utils/FlowExt.kt @@ -94,12 +94,12 @@ data class ListDiff( val all: List ) -fun Flow>.diffed(): Flow> { +fun Flow>.diffed(): Flow> { return zipWithPrevious().map { (previous, new) -> val addedOrModified = new - previous.orEmpty().toSet() val removed = if (previous != null && previous.size != new.size) previous - new.toSet() else emptyList() - ListDiff(removed = removed, addedOrModified = addedOrModified, all = new) + ListDiff(removed = removed, addedOrModified = addedOrModified, all = new.toList()) } } diff --git a/common/src/main/java/jp/co/soramitsu/common/utils/KoltinExt.kt b/common/src/main/java/jp/co/soramitsu/common/utils/KoltinExt.kt index d5c5d574af..cdedeba64d 100644 --- a/common/src/main/java/jp/co/soramitsu/common/utils/KoltinExt.kt +++ b/common/src/main/java/jp/co/soramitsu/common/utils/KoltinExt.kt @@ -99,6 +99,13 @@ fun BigDecimal?.isZero(): Boolean = this?.compareTo(BigDecimal.ZERO) == 0 fun BigDecimal?.isNotZero(): Boolean = !isZero() fun BigDecimal.greaterThen(other: BigDecimal): Boolean = this.compareTo(other) == 1 +fun BigInteger.greaterThen(other: BigInteger): Boolean = this.compareTo(other) == 1 fun BigInteger?.isZero(): Boolean = this?.compareTo(BigInteger.ZERO) == 0 +fun BigInteger?.greaterThanOrEquals(other: BigInteger): Boolean = this?.compareTo(other) == 1 || this?.compareTo(other) == 0 +fun BigInteger?.lessThan(other: BigInteger): Boolean = this?.compareTo(other) == -1 fun BigInteger?.isNotZero(): Boolean = !isZero() + +fun BigInteger?.positiveOrNull(): BigInteger? { + return this?.takeIf { it.greaterThen(BigInteger.ZERO) } +} diff --git a/common/src/main/res/drawable/ic_edit_20.xml b/common/src/main/res/drawable/ic_edit_20.xml new file mode 100644 index 0000000000..d9d1a3e5d6 --- /dev/null +++ b/common/src/main/res/drawable/ic_edit_20.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/common/src/main/res/drawable/ic_eye.xml b/common/src/main/res/drawable/ic_eye.xml deleted file mode 100644 index 68385e5f5d..0000000000 --- a/common/src/main/res/drawable/ic_eye.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/common/src/main/res/values-in/strings.xml b/common/src/main/res/values-in/strings.xml index 0b3f72e74c..cd5f2ee784 100644 --- a/common/src/main/res/values-in/strings.xml +++ b/common/src/main/res/values-in/strings.xml @@ -390,7 +390,7 @@ Pelajari crowdloan %s Pelajari tentang bonus %s Jangka waktu sewa - Pilih terjun payung untuk menyumbangkan %s. Anda akan menerima hadiah jika parachain memperoleh slot pada akhir lelang. + Pilih terjun payung untuk menyumbangkan. Anda akan menerima hadiah jika parachain memperoleh slot pada akhir lelang. Crowdfloans aktif \n akan muncul di sini Saya sudah membaca dan setuju atas Ketentuan dan Kebijakan Privasi Dibesarkan @@ -747,7 +747,7 @@ Sosial media Jumlah yang Anda coba transfer tidak cukup untuk menutupi biaya transaksi di %s jaringan. Meskipun transaksi tidak dapat diproses, Anda tetap akan dikenakan biaya di jaringan Sora. Saat ini,ada jumlah minimal%suntuk menjembatani guna menjamin stabilitas dan keamanan jaringan SORA. Kami menghargai pengertian anda. - 0 € biaya layanan tahunan + €0 biaya layanan tahunan Tidak termasuk aplikasi negara tertentu
tidak dapat mengajukan untuk Kartu SORA saat ini
See the list]]>
Aktifkan kartu @@ -1073,6 +1073,7 @@ Vesting Terkunci Lihat di %s Lihat dompet + Kamu sudah menyembunyikan semua aset Beli dengan Menerima Terima %s diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index 5359d59f7a..4081b7217f 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -1050,6 +1050,7 @@ バリデーターが見つかりません %sで見る ウォレットを見る + 全アセットを非表示にしました で購入 受け取る 受信 %s diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index 91e73058ea..9c2cdb7660 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -1048,6 +1048,7 @@ Não foram encontrados validadores Ver em %s Ver carteira + Você escondeu todos os ativos Comprar %s com Receber Receber %s diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 38cb412fd5..edc09f5d79 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -205,6 +205,12 @@ Модуль Сеть Комиссия сети + + %d сеть + %d сети + %d сетей + %d сеть + Далее Нет Не делайте скриншоты, которые могут быть собраны сторонними вредоносными программами diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index 06ea2dcb56..50de2725a1 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -1075,6 +1075,7 @@ Vesting Kilitli %s\'da görüntüle Cüzdanı görün + Değiştirilebilir jetonlar %s ile al Al Alın%s diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index 393f6483ed..85397407cd 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -91,6 +91,7 @@ Hiển thị cụm từ ghi nhớ Hiển thị Raw Seed Nếu bạn mất quyền truy cập vào thiết bị này, tiền của bạn sẽ bị mất, trừ khi bạn sao lưu! + Bị chặn Quản trị Pool thanh khoản Nhóm Nomination @@ -244,6 +245,9 @@ Mạng Phí mạng Quản lý mạng + + mạng + Kế tiếp Không Không chụp ảnh màn hình vì có thể bị phần mềm độc hại của bên thứ ba thu thập @@ -287,6 +291,7 @@ Bỏ qua quá trình Sắp xếp theo Staking + Bắt đầu cho đến %s Thời gian còn lại Chuỗi chéo @@ -506,6 +511,7 @@ Thêm tài khoản Chuyển node Thêm tài khoản + Lỗi kết nối: Không thể kết nối mạng. Vui lòng thử lại. Mạng không khả dụng Node không khả dụng Sự cố mạng @@ -746,7 +752,7 @@ Chia sẻ mã giới thiệu Truyền thông xã hội Số tiền bạn đang cố chuyển không đủ để trả phí giao dịch trên mạng %s . Mặc dù giao dịch không được xử lý nhưng bạn vẫn sẽ bị tính phí trên mạng Sora. - Hiện tại, có một phút. số tiền %s để bắc cầu nhằm đảm bảo tính ổn định và bảo mật của Mạng SORA. Chúng tôi đánh giá cao sự hiểu biết của bạn. + Hiện tại, cần số tiền tối thiếu là %s để bắc cầu nhằm đảm bảo tính ổn định và bảo mật của Mạng SORA. Cảm ơn vì sự thông cảm của bạn. 0 € phí dịch vụ hàng năm Không bao gồm ứng dụng một số quốc gia
không thể đăng ký Thẻ SORA tại thời điểm
nàyXem danh sách]]>
@@ -1067,6 +1073,8 @@ Tên này chỉ hiển thị với bạn và lưu trên điện thoại cá nhân. Tạo một ví mới Sứ mệnh + Lượng token tối thiểu để stake ở nominator này là %s . Để nhận được phần thưởng, bạn phải stake nhiều hơn. + Lượng token tối thiểu để stake với những nominator đang hoạt động Tài khoản này không được mạng (network) đề cử ở era hiện tại Không tìm thấy validator nào Do lịch trình trao quyền duy nhất của mỗi parachain, ứng dụng của chúng tôi không thể hiển thị số lượng mã thông báo bị khóa đủ điều kiện để yêu cầu. Xin lưu ý rằng việc bắt đầu yêu cầu bồi thường có thể không thực tế nếu số tiền tiềm năng là rất nhỏ và có thể so sánh được với phí giao dịch liên quan. Để có thông tin chi tiết toàn diện về số dư bị khóa của bạn, hãy tham khảo Subscan blockexplorer. Chúng tôi khuyên bạn nên đánh giá thông tin một cách cẩn thận và tiến hành theo ý riêng của bạn. @@ -1074,6 +1082,7 @@ Vesting Locked Xem trong %s Xem ví + Bạn đã ẩn tất cả nội dung Mua %s với Nhận Nhận %s @@ -1107,6 +1116,7 @@ Số dư tối thiểu Xác nhận chuyển khoản Giao dịch chuyển tiền của bạn sẽ không thành công vì số tiền cuối cùng trên tài khoản đích sẽ ít hơn số dư tối thiểu. Hãy cố gắng tăng số lượng. + Chuyển khoản của bạn sẽ không thành công vì số tiền cuối cùng (%s) trên tài khoản đích sẽ nhỏ hơn số dư tối thiểu (%s). Vui lòng tăng thêm %s Số dư Ethereum không đủ trong tài khoản của người nhận sẽ ngăn cản việc hoàn tất chuyển mã thông báo ERC20. Vui lòng đảm bảo người nhận có đủ Ethereum để tiến hành chuyển khoản. Chuyển khoản của bạn sẽ xóa tài khoản khỏi blockstore vì nó sẽ làm cho tổng số dư còn lại thấp hơn số dư tối thiểu. Chuyển khoản sẽ xóa tài khoản diff --git a/common/src/main/res/values-zh/strings.xml b/common/src/main/res/values-zh/strings.xml index 816418cdee..113c1f43ea 100644 --- a/common/src/main/res/values-zh/strings.xml +++ b/common/src/main/res/values-zh/strings.xml @@ -155,6 +155,7 @@ 数量 金额太低 + %s和其他 已应用 应用 法币余额 @@ -176,6 +177,7 @@ 领取 可领取 关闭 + 佣金 已完成 已完成( %s ) 确认 @@ -184,6 +186,7 @@ 已确认 连接 连接问题 + 联系人 继续 复制到剪贴板 复制 @@ -342,6 +345,7 @@ 按连接搜索 联系地址 联系人姓名 + 未找到联系人 贡献类型 直接DOT lcDOT @@ -1069,6 +1073,7 @@ 归属锁定 在%s中查看 查看钱包 + 你已经隐藏所有资产 购买%s用 接收 接收%s diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index f741f5c0cd..c9900f0d0d 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ Show mnemonic phrase Show Raw Seed If you loose access to this device, your funds will be lost, unless you back up! + Blocked Governance Liquidity Pools Nomination Pools @@ -246,6 +247,10 @@ Network Network fee Network management + + %d network + %d networks + Next No Do not take screenshots, they may be collected by third-party malware @@ -300,6 +305,7 @@ Total Transaction Transaction raw data + Transaction sent Transaction submitted Transferable: %s Try again @@ -509,6 +515,7 @@ Add Account Switch Node Add an account + Connection Error: Unable to connect to the network. Please try again. Network is unavailable Node is unavailable Network Issues @@ -752,7 +759,7 @@ Social Media The amount you\'re trying to transfer is insufficient to cover the transaction fees on the %s network. Although the transaction won\'t process, you\'ll still be charged the fees on the Sora network. Currently, there\'s a min. amount %s for bridging to ensure the stability and security of the SORA Network. We appreciate your understanding. - 0 € annual service fee + €0 annual service fee Excluded of application certain countries
can not apply for SORA Card at this moment
See the list]]>
Enable card @@ -1034,6 +1041,7 @@ Substrate keypair crypto type Substrate secret derivation path\n You can return to your browser now + Your transaction has been successfully sent to blockchain Support & Feedback Switch node Auto select nodes @@ -1072,6 +1080,8 @@ This name will be displayed only to you and stored locally on your mobile device. Create a new wallet Comission + Minimum stake among active nominators is %s. To get rewards you have to stake more. + Minimum stake among active nominators This account has not been elected by the network to participate in the current era No validators found Due to the unique vesting schedules of each parachain, our app is unable to display the quantity of locked tokens eligible for claiming. Please be advised that initiating a claim may be impractical if the prospective amount is marginal and comparable to the transaction fee involved. For comprehensive insights into your locked balances, consult the Subscan blockexplorer. We urge you to assess the information carefully and proceed at your own discretion. @@ -1079,6 +1089,7 @@ Vesting Locked View in %s View wallet + You have hidden all assets Buy %s with Receive Receive %s diff --git a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/Helpers.kt b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/Helpers.kt index b505a30c5f..b93321504d 100644 --- a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/Helpers.kt +++ b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/Helpers.kt @@ -45,7 +45,8 @@ fun chainOf( paraId = null, rank = null, isChainlinkProvider = false, - supportNft = false + supportNft = false, + isUsesAppId = false ) fun ChainLocal.nodeOf( diff --git a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/MetaAccountDaoTest.kt b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/MetaAccountDaoTest.kt index de463f006d..4114489016 100644 --- a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/MetaAccountDaoTest.kt +++ b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/dao/MetaAccountDaoTest.kt @@ -81,7 +81,8 @@ class MetaAccountDaoTest : DaoTest(AppDatabase::metaAccountDao) ethereumAddress = null, position = 0, googleBackupAddress = null, - isBackedUp = false + isBackedUp = false, + initialized = false ) private fun chainAccount(metaId: Long) = ChainAccountLocal( @@ -90,6 +91,7 @@ class MetaAccountDaoTest : DaoTest(AppDatabase::metaAccountDao) publicKey = byteArrayOf(), cryptoType = CryptoType.SR25519, accountId = byteArrayOf(), - name = "" + name = "", + initialized = true ) } diff --git a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt index 844329bcca..3e6fe6f0cf 100644 --- a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt +++ b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt @@ -238,7 +238,10 @@ class V2MigrationTest { ethereumAddress = getBlob(getColumnIndex(Column.ETHEREUM_ADDRESS)), name = getString(getColumnIndex(Column.NAME)), isSelected = getInt(getColumnIndex(Column.IS_SELECTED)) == 1, - position = getInt(getColumnIndex(Column.POSITION)) + position = getInt(getColumnIndex(Column.POSITION)), + isBackedUp = false, + googleBackupAddress = null, + initialized = true ) metaAccount.id = getLong(getColumnIndex(Column.ID)) diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt index ef55f93c49..1f65e03a36 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt @@ -68,6 +68,7 @@ import jp.co.soramitsu.coredb.migrations.Migration_61_62 import jp.co.soramitsu.coredb.migrations.Migration_62_63 import jp.co.soramitsu.coredb.migrations.Migration_63_64 import jp.co.soramitsu.coredb.migrations.Migration_64_65 +import jp.co.soramitsu.coredb.migrations.Migration_65_66 import jp.co.soramitsu.coredb.migrations.RemoveAccountForeignKeyFromAsset_17_18 import jp.co.soramitsu.coredb.migrations.RemoveLegacyData_35_36 import jp.co.soramitsu.coredb.migrations.RemoveStakingRewardsTable_22_23 @@ -93,7 +94,7 @@ import jp.co.soramitsu.coredb.model.chain.FavoriteChainLocal import jp.co.soramitsu.coredb.model.chain.MetaAccountLocal @Database( - version = 65, + version = 66, entities = [ AccountLocal::class, AddressBookContact::class, @@ -179,6 +180,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(Migration_62_63) .addMigrations(Migration_63_64) .addMigrations(Migration_64_65) + .addMigrations(Migration_65_66) .build() } return instance!! diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/AssetDao.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/AssetDao.kt index 06d5e32ea0..4256a02302 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/AssetDao.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/AssetDao.kt @@ -51,6 +51,7 @@ private const val RETRIEVE_ACCOUNT_ASSETS_QUERY = """ interface AssetReadOnlyCache { fun observeAssets(metaId: Long): Flow> + fun observeAllEnabledAssets(): Flow> suspend fun getAssets(metaId: Long): List fun observeAsset(metaId: Long, accountId: AccountId, chainId: String, assetId: String): Flow @@ -65,6 +66,9 @@ abstract class AssetDao : AssetReadOnlyCache { @Query(RETRIEVE_ACCOUNT_ASSETS_QUERY) abstract override fun observeAssets(metaId: Long): Flow> + @Query("SELECT * FROM assets WHERE enabled = 1") + abstract override fun observeAllEnabledAssets(): Flow> + @Query(RETRIEVE_ACCOUNT_ASSETS_QUERY) abstract override suspend fun getAssets(metaId: Long): List @@ -110,6 +114,9 @@ abstract class AssetDao : AssetReadOnlyCache { @Insert(onConflict = OnConflictStrategy.REPLACE) abstract suspend fun insertAsset(asset: AssetLocal) + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insertAssets(assets: List) + @Update(entity = AssetLocal::class) abstract suspend fun updateAssets(item: List): Int @@ -137,6 +144,9 @@ abstract class AssetDao : AssetReadOnlyCache { } } + @Query("UPDATE assets SET enabled = CASE WHEN EXISTS (SELECT 1 FROM assets WHERE metaId = :metaId AND freeInPlanks > 0) THEN 0 ELSE enabled END WHERE metaId = :metaId AND (freeInPlanks IS NULL OR freeInPlanks = 0)") + abstract fun hideEmptyAssetsIfThereAreAtLeastOnePositiveBalance(metaId: Long) + @Query( """ SELECT symbol FROM chain_assets WHERE chain_assets.id = :assetId diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/ChainDao.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/ChainDao.kt index 7ec48297f3..28e361bb0c 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/ChainDao.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/ChainDao.kt @@ -172,9 +172,8 @@ abstract class ChainDao { """ SELECT c.*, a.*, tp.* FROM chains c JOIN chain_assets ca ON ca.chainId = c.id AND ca.symbol in (:assetSymbol, '$xcPrefix'||:assetSymbol) - LEFT JOIN assets a ON a.chainId = c.id AND a.id = ca.id + LEFT JOIN assets a ON a.chainId = c.id AND a.id = ca.id AND a.metaId = :accountMetaId AND a.enabled = 1 LEFT JOIN token_price tp ON tp.priceId = a.tokenPriceId - AND a.metaId = :accountMetaId """ ) protected abstract fun observeChainsWithBalanceByName( diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/MetaAccountDao.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/MetaAccountDao.kt index 5c121c7f60..ccc75a4ce9 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/MetaAccountDao.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/MetaAccountDao.kt @@ -40,6 +40,12 @@ interface MetaAccountDao { @Insert suspend fun insertChainAccount(chainAccount: ChainAccountLocal) + @Query("SELECT * FROM chain_accounts WHERE initialized = 0") + fun observeNotInitializedChainAccounts(): Flow> + + @Query("UPDATE chain_accounts SET initialized = 1 WHERE metaId = :metaId AND chainId = :chainId") + suspend fun markChainAccountInitialized(metaId: Long, chainId: String) :Int + @Query("SELECT * FROM meta_accounts") fun getMetaAccounts(): List @@ -118,4 +124,11 @@ interface MetaAccountDao { @Query("SELECT * FROM favorite_chains WHERE metaId = :metaId") fun observeFavoriteChains(metaId: Long): Flow> + + @Query("UPDATE meta_accounts SET initialized = 1 WHERE id in (:ids)") + suspend fun markAccountsInitialized(ids: List) :Int + + @Query("SELECT * FROM meta_accounts WHERE initialized = 0") + @Transaction + fun observeNotInitializedMetaAccounts(): Flow> } diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt index 3990515e7a..2a2394acbc 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt @@ -3,6 +3,15 @@ package jp.co.soramitsu.coredb.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +val Migration_65_66 = object : Migration(65, 66) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE meta_accounts ADD COLUMN `initialized` INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE chain_accounts ADD COLUMN `initialized` INTEGER NOT NULL DEFAULT 0") + db.execSQL("DELETE FROM assets") + db.execSQL("ALTER TABLE chains ADD COLUMN `identityChain` TEXT NULL DEFAULT NULL") + } +} + val Migration_64_65 = object : Migration(64, 65) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("DELETE FROM storage") diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/V2Migration.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/V2Migration.kt index 063ec2b093..2821e929ea 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/V2Migration.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/V2Migration.kt @@ -88,7 +88,8 @@ class V2Migration( isSelected = isSelected, position = index, isBackedUp = false, - googleBackupAddress = null + googleBackupAddress = null, + initialized = false ) val metaId = insertMetaAccount(metaAccount, database) diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/model/AssetLocal.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/model/AssetLocal.kt index 51c1a5192b..c265e9a624 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/model/AssetLocal.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/model/AssetLocal.kt @@ -7,7 +7,14 @@ import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.coredb.model.chain.ChainLocal import jp.co.soramitsu.shared_utils.runtime.AccountId import java.math.BigInteger +import jp.co.soramitsu.common.utils.positiveOrNull +/*** This table is used for storing assets in database. + * freeInPlanks - has three states: + * null - loading is in progress + * -1 - error + * 0 or positive number - free amount + */ @Entity( tableName = "assets", primaryKeys = ["id", "chainId", "accountId", "metaId"], @@ -42,27 +49,29 @@ data class AssetLocal( companion object { fun createEmpty( accountId: AccountId, - assetId: String, + id: String, chainId: String, metaId: Long, - priceId: String? + tokenPriceId: String?, + enabled: Boolean? = null ) = AssetLocal( - id = assetId, + id = id, chainId = chainId, accountId = accountId, metaId = metaId, - tokenPriceId = priceId + tokenPriceId = tokenPriceId, + enabled = enabled ) } val totalInPlanks: BigInteger - get() = freeInPlanks.orZero() + reservedInPlanks.orZero() + get() = freeInPlanks.positiveOrNull().orZero() + reservedInPlanks.orZero() private val locked: BigInteger get() = maxOf(miscFrozenInPlanks.orZero(), feeFrozenInPlanks.orZero()) val transferableInPlanks: BigInteger - get() = maxOf(freeInPlanks.orZero() - locked, BigInteger.ZERO) + get() = maxOf(freeInPlanks.positiveOrNull().orZero() - locked, BigInteger.ZERO) } data class AssetUpdateItem( diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/ChainLocal.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/ChainLocal.kt index fcb7c13f08..dfaf154c4e 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/ChainLocal.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/ChainLocal.kt @@ -23,7 +23,8 @@ class ChainLocal( val isEthereumChain: Boolean, val isChainlinkProvider: Boolean, val supportNft: Boolean, - val isUsesAppId: Boolean + val isUsesAppId: Boolean, + val identityChain: String? ) { class ExternalApi( diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/MetaAccountLocal.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/MetaAccountLocal.kt index 621d73fe82..971920a67c 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/MetaAccountLocal.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/model/chain/MetaAccountLocal.kt @@ -6,6 +6,7 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import androidx.room.Relation +import jp.co.soramitsu.core.models.Chain import jp.co.soramitsu.core.models.CryptoType @Entity( @@ -25,7 +26,8 @@ class MetaAccountLocal( val isSelected: Boolean, val position: Int, val isBackedUp: Boolean, - val googleBackupAddress: String? + val googleBackupAddress: String?, + val initialized: Boolean ) { companion object Table { @@ -79,7 +81,8 @@ class ChainAccountLocal( val publicKey: ByteArray, val accountId: ByteArray, val cryptoType: CryptoType, - val name: String + val name: String, + val initialized: Boolean ) interface JoinedMetaAccountInfo { diff --git a/feature-account-api/src/main/java/jp/co/soramitsu/account/api/domain/model/MetaAccount.kt b/feature-account-api/src/main/java/jp/co/soramitsu/account/api/domain/model/MetaAccount.kt index e372d12c6a..1ce6290e4f 100644 --- a/feature-account-api/src/main/java/jp/co/soramitsu/account/api/domain/model/MetaAccount.kt +++ b/feature-account-api/src/main/java/jp/co/soramitsu/account/api/domain/model/MetaAccount.kt @@ -23,6 +23,7 @@ interface LightMetaAccount { val isSelected: Boolean val name: String val isBackedUp: Boolean + val initialized: Boolean } fun LightMetaAccount( @@ -34,7 +35,8 @@ fun LightMetaAccount( ethereumPublicKey: ByteArray?, isSelected: Boolean, name: String, - isBackedUp: Boolean + isBackedUp: Boolean, + initialized: Boolean ) = object : LightMetaAccount { override val id: Long = id override val substratePublicKey: ByteArray = substratePublicKey @@ -45,6 +47,7 @@ fun LightMetaAccount( override val isSelected: Boolean = isSelected override val name: String = name override val isBackedUp: Boolean = isBackedUp + override val initialized: Boolean = initialized } data class MetaAccount( @@ -59,7 +62,8 @@ data class MetaAccount( override val isSelected: Boolean, override val isBackedUp: Boolean, val googleBackupAddress: String?, - override val name: String + override val name: String, + override val initialized: Boolean ) : LightMetaAccount { class ChainAccount( @@ -98,6 +102,7 @@ data class MetaAccount( } else if (other.ethereumPublicKey != null) return false if (isSelected != other.isSelected) return false if (name != other.name) return false + if (initialized != other.initialized) return false return true } @@ -113,6 +118,7 @@ data class MetaAccount( result = 31 * result + (ethereumPublicKey?.contentHashCode() ?: 0) result = 31 * result + isSelected.hashCode() result = 31 * result + name.hashCode() + result = 31 * result + initialized.hashCode() return result } } @@ -156,3 +162,4 @@ fun MetaAccount.accountId(chain: Chain): ByteArray? { else -> substrateAccountId } } + diff --git a/feature-account-impl/build.gradle b/feature-account-impl/build.gradle index c5494c8da9..6ea2148632 100644 --- a/feature-account-impl/build.gradle +++ b/feature-account-impl/build.gradle @@ -94,4 +94,5 @@ dependencies { api libs.sharedFeaturesCoreDep implementation libs.sharedFeaturesBackupDep + implementation libs.web3jDep } \ No newline at end of file diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/mappers/Mappers.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/mappers/Mappers.kt index 9df626d097..13cd096a4d 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/mappers/Mappers.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/mappers/Mappers.kt @@ -67,7 +67,8 @@ fun mapMetaAccountLocalToLightMetaAccount( ethereumPublicKey = ethereumPublicKey, isSelected = isSelected, name = name, - isBackedUp = isBackedUp + isBackedUp = isBackedUp, + initialized = initialized, ) } @@ -112,7 +113,8 @@ fun mapMetaAccountLocalToMetaAccount( isSelected = isSelected, name = name, isBackedUp = isBackedUp, - googleBackupAddress = googleBackupAddress + googleBackupAddress = googleBackupAddress, + initialized = initialized, ) } return metaAccount diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/AccountRepositoryImpl.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/AccountRepositoryImpl.kt index 5028593f21..4ef4ca5169 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/AccountRepositoryImpl.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/AccountRepositoryImpl.kt @@ -12,6 +12,7 @@ import jp.co.soramitsu.account.api.domain.model.MetaAccountOrdering import jp.co.soramitsu.account.api.domain.model.address import jp.co.soramitsu.account.api.domain.model.cryptoType import jp.co.soramitsu.account.api.domain.model.hasChainAccount +import jp.co.soramitsu.account.impl.data.mappers.mapMetaAccountLocalToMetaAccount import jp.co.soramitsu.account.impl.data.repository.datasource.AccountDataSource import jp.co.soramitsu.backup.domain.models.BackupAccountType import jp.co.soramitsu.common.data.Keypair @@ -30,13 +31,17 @@ import jp.co.soramitsu.core.crypto.mapEncryptionToCryptoType import jp.co.soramitsu.core.model.Language import jp.co.soramitsu.core.model.SecuritySource import jp.co.soramitsu.core.models.CryptoType +import jp.co.soramitsu.core.models.accountIdOf import jp.co.soramitsu.coredb.dao.AccountDao +import jp.co.soramitsu.coredb.dao.AssetDao import jp.co.soramitsu.coredb.dao.MetaAccountDao import jp.co.soramitsu.coredb.model.AccountLocal +import jp.co.soramitsu.coredb.model.AssetLocal import jp.co.soramitsu.coredb.model.chain.ChainAccountLocal import jp.co.soramitsu.coredb.model.chain.FavoriteChainLocal import jp.co.soramitsu.coredb.model.chain.MetaAccountLocal import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry +import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId @@ -57,8 +62,12 @@ import jp.co.soramitsu.shared_utils.runtime.AccountId import jp.co.soramitsu.shared_utils.scale.EncodableStruct import jp.co.soramitsu.shared_utils.ss58.SS58Encoder.addressByte import jp.co.soramitsu.shared_utils.ss58.SS58Encoder.toAccountId +import jp.co.soramitsu.wallet.api.data.cache.AssetCache +import jp.co.soramitsu.wallet.impl.domain.model.Asset +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -66,6 +75,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.bouncycastle.util.encoders.Hex @@ -77,11 +87,14 @@ class AccountRepositoryImpl( private val jsonSeedDecoder: JsonSeedDecoder, private val jsonSeedEncoder: JsonSeedEncoder, private val languagesHolder: LanguagesHolder, - private val chainRegistry: ChainRegistry + private val chainRegistry: ChainRegistry, + private val chainsRepository: ChainsRepository, + private val assetDao: AssetDao, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default ) : AccountRepository { override fun getEncryptionTypes(): List { - return CryptoType.values().toList() + return CryptoType.entries } override suspend fun selectAccount(metaAccountId: Long) { @@ -138,7 +151,7 @@ class AccountRepositoryImpl( override fun allMetaAccountsFlow(): StateFlow> { return accountDataSource.observeAllMetaAccounts() - .flowOn(Dispatchers.IO) + .flowOn(dispatcher) .stateIn(GlobalScope, SharingStarted.Eagerly, emptyList()) } @@ -232,7 +245,7 @@ class AccountRepositoryImpl( } override suspend fun getMyAccounts(query: String, chainId: String): Set { -// return withContext(Dispatchers.Default) { +// return withContext(dispatcher) { // accountDao.getAccounts(query, networkType) // .map { mapAccountLocalToAccount(it) } // .toSet() @@ -294,7 +307,7 @@ class AccountRepositoryImpl( ethSeed: String?, googleBackupAddress: String? ) { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val substrateSeedBytes = Hex.decode(seed.removePrefix("0x")) val ethSeedBytes = ethSeed?.let { Hex.decode(it.removePrefix("0x")) } @@ -332,12 +345,16 @@ class AccountRepositoryImpl( ethereumPublicKey = ethereumKeypair?.publicKey, ethereumAddress = ethereumKeypair?.publicKey?.ethereumAddressFromPublicKey(), isBackedUp = true, - googleBackupAddress = googleBackupAddress + googleBackupAddress = googleBackupAddress, + initialized = false ) val metaAccountId = insertAccount(metaAccount) - storeV2.putMetaAccountSecrets(metaAccountId, secretsV2) - selectAccount(metaAccountId) + + coroutineScope { + launch { storeV2.putMetaAccountSecrets(metaAccountId, secretsV2) } + launch { selectAccount(metaAccountId) } + }.join() } } @@ -349,10 +366,10 @@ class AccountRepositoryImpl( substrateDerivationPath: String, selectedEncryptionType: CryptoType ) { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val seedBytes = Hex.decode(seed.removePrefix("0x")) - val ethereumBased = chainRegistry.getChain(chainId).isEthereumBased + val ethereumBased = chainsRepository.getChain(chainId).isEthereumBased val keyPair = when { ethereumBased -> EthereumKeypairFactory.createWithPrivateKey(seedBytes) else -> { @@ -398,7 +415,8 @@ class AccountRepositoryImpl( publicKey = publicKey, accountId = accountId, cryptoType = crypto, - name = accountName + name = accountName, + initialized = false ) insertChainAccount(chainAccount) @@ -417,7 +435,7 @@ class AccountRepositoryImpl( ethJson: String?, googleBackupAddress: String? ) { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val substrateImportData = jsonSeedDecoder.decode(json, password) val ethImportData = ethJson?.let { jsonSeedDecoder.decode(ethJson, password) } @@ -445,13 +463,16 @@ class AccountRepositoryImpl( ethereumAddress = ethereumKeypair?.publicKey?.ethereumAddressFromPublicKey(), ethereumPublicKey = ethereumKeypair?.publicKey, isBackedUp = true, - googleBackupAddress = googleBackupAddress + googleBackupAddress = googleBackupAddress, + initialized = false ) val metaAccountId = insertAccount(metaAccount) - storeV2.putMetaAccountSecrets(metaAccountId, secretsV2) - selectAccount(metaAccountId) + coroutineScope { + launch { storeV2.putMetaAccountSecrets(metaAccountId, secretsV2) } + launch { selectAccount(metaAccountId) } + }.join() } } @@ -462,10 +483,10 @@ class AccountRepositoryImpl( json: String, password: String ) { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val importData = jsonSeedDecoder.decode(json, password) - val ethereumBased = chainRegistry.getChain(chainId).isEthereumBased + val ethereumBased = chainsRepository.getChain(chainId).isEthereumBased val keyPair = when { ethereumBased -> EthereumKeypairFactory.createWithPrivateKey(importData.keypair.privateKey) else -> importData.keypair @@ -494,7 +515,8 @@ class AccountRepositoryImpl( publicKey = publicKey, accountId = accountId, cryptoType = crypto, - name = accountName + name = accountName, + initialized = false ) insertChainAccount(chainAccount) @@ -515,7 +537,7 @@ class AccountRepositoryImpl( } override suspend fun generateMnemonic(): List { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val generationResult = MnemonicCreator.randomMnemonic(Mnemonic.Length.TWELVE) generationResult.wordList @@ -539,7 +561,7 @@ class AccountRepositoryImpl( } override suspend fun processAccountJson(json: String): ImportJsonData { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val importAccountMeta = jsonSeedDecoder.extractImportMetaData(json) with(importAccountMeta) { @@ -575,8 +597,8 @@ class AccountRepositoryImpl( } override suspend fun generateRestoreJson(metaId: Long, chainId: ChainId, password: String) = - withContext(Dispatchers.Default) { - val chain = chainRegistry.getChain(chainId) + withContext(dispatcher) { + val chain = chainsRepository.getChain(chainId) val metaAccount = getMetaAccount(metaId) val hasChainAccount = metaAccount.hasChainAccount(chainId) @@ -674,7 +696,7 @@ class AccountRepositoryImpl( isBackedUp: Boolean, googleBackupAddress: String? ): Long { - return withContext(Dispatchers.Default) { + return withContext(dispatcher) { val substrateDerivationPathOrNull = substrateDerivationPath.nullIfEmpty() val decodedDerivationPath = substrateDerivationPathOrNull?.let { SubstrateJunctionDecoder.decode(it) @@ -729,12 +751,12 @@ class AccountRepositoryImpl( isSelected = true, position = position, isBackedUp = isBackedUp, - googleBackupAddress = googleBackupAddress + googleBackupAddress = googleBackupAddress, + initialized = false, ) val metaAccountId = insertAccount(metaAccount) storeV2.putMetaAccountSecrets(metaAccountId, secretsV2) - metaAccountId } } @@ -774,7 +796,7 @@ class AccountRepositoryImpl( junctions = decodedEthereumDerivationPath.junctions ) - val ethereumBased = chainRegistry.getChain(chainId).isEthereumBased + val ethereumBased = chainsRepository.getChain(chainId).isEthereumBased val keyPair = when { ethereumBased -> ethereumKeypair else -> keys @@ -815,7 +837,8 @@ class AccountRepositoryImpl( publicKey = publicKey, accountId = accountId, cryptoType = crypto, - name = accountName + name = accountName, + initialized = false ) insertChainAccount(chainAccount) @@ -852,7 +875,7 @@ class AccountRepositoryImpl( override fun polkadotAddressForSelectedAccountFlow(): Flow { return selectedMetaAccountFlow().map { - val chain = chainRegistry.getChain(polkadotChainId) + val chain = chainsRepository.getChain(polkadotChainId) it.address(chain) ?: "" } } @@ -880,13 +903,13 @@ class AccountRepositoryImpl( override suspend fun googleBackupAddressForWallet(walletId: Long): String { val wallet = getMetaAccount(walletId) - val chain = chainRegistry.getChain(westendChainId) + val chain = chainsRepository.getChain(westendChainId) return wallet.googleBackupAddress ?: wallet.address(chain) ?: "" } override fun googleAddressAllWalletsFlow(): Flow> { return allMetaAccountsFlow().map { allMetaAccounts -> - val westendChain = chainRegistry.getChain(westendChainId) + val westendChain = chainsRepository.getChain(westendChainId) allMetaAccounts.mapNotNull { it.googleBackupAddress ?: it.address(westendChain) } @@ -894,7 +917,7 @@ class AccountRepositoryImpl( } override suspend fun getChain(chainId: ChainId): Chain { - return chainRegistry.getChain(chainId) + return chainsRepository.getChain(chainId) } override suspend fun updateFavoriteChain(metaAccountId: Long, chainId: ChainId, isFavorite: Boolean) { diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/datasource/AccountDataSourceImpl.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/datasource/AccountDataSourceImpl.kt index 9bd7edaaa1..eb4ca939da 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/datasource/AccountDataSourceImpl.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/data/repository/datasource/AccountDataSourceImpl.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn @@ -123,7 +123,7 @@ class AccountDataSourceImpl( } } - override suspend fun anyAccountSelected(): Boolean = selectedMetaAccountLocal.first() != null + override suspend fun anyAccountSelected(): Boolean = selectedMetaAccountLocal.firstOrNull() != null override suspend fun saveSelectedAccount(account: Account) = withContext(Dispatchers.Default) { val raw = jsonMapper.toJson(account) diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/di/AccountFeatureModule.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/di/AccountFeatureModule.kt index 330e80ab2d..3d68a6fbd9 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/di/AccountFeatureModule.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/di/AccountFeatureModule.kt @@ -26,7 +26,6 @@ import jp.co.soramitsu.account.impl.domain.NodeHostValidator import jp.co.soramitsu.account.impl.domain.account.details.AccountDetailsInteractor import jp.co.soramitsu.account.impl.presentation.common.mixin.api.CryptoTypeChooserMixin import jp.co.soramitsu.account.impl.presentation.common.mixin.impl.CryptoTypeChooser -import jp.co.soramitsu.common.data.OnboardingStoriesDataSource import jp.co.soramitsu.common.data.network.AppLinksProvider import jp.co.soramitsu.common.data.network.NetworkApiCreator import jp.co.soramitsu.common.data.network.coingecko.CoingeckoApi @@ -35,9 +34,7 @@ import jp.co.soramitsu.common.data.secrets.v2.SecretStoreV2 import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.common.data.storage.encrypt.EncryptedPreferences import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies -import jp.co.soramitsu.common.domain.GetEducationalStoriesUseCase import jp.co.soramitsu.common.domain.SelectedFiat -import jp.co.soramitsu.common.domain.ShouldShowEducationalStoriesUseCase import jp.co.soramitsu.common.interfaces.FileProvider import jp.co.soramitsu.common.resources.ClipboardManager import jp.co.soramitsu.common.resources.LanguagesHolder @@ -79,7 +76,9 @@ class AccountFeatureModule { jsonSeedDecoder: JsonSeedDecoder, jsonSeedEncoder: JsonSeedEncoder, languagesHolder: LanguagesHolder, - chainRegistry: ChainRegistry + chainRegistry: ChainRegistry, + chainsRepository: ChainsRepository, + assetDao: AssetDao ): AccountRepository { return AccountRepositoryImpl( accountDataSource, @@ -89,7 +88,9 @@ class AccountFeatureModule { jsonSeedDecoder, jsonSeedEncoder, languagesHolder, - chainRegistry + chainRegistry, + chainsRepository, + assetDao ) } @@ -209,23 +210,6 @@ class AccountFeatureModule { return AssetNotNeedAccountUseCaseImpl(chainRegistry, assetDao, tokenPriceDao, selectedFiat) } - @Provides - fun provideStoriesDataSource() = OnboardingStoriesDataSource() - - @Provides - fun provideShouldShowEducationalStories( - preferences: Preferences - ): ShouldShowEducationalStoriesUseCase { - return ShouldShowEducationalStoriesUseCase(preferences) - } - - @Provides - fun provideGetEducationalStories( - onboardingStoriesDataSource: OnboardingStoriesDataSource - ): GetEducationalStoriesUseCase { - return GetEducationalStoriesUseCase(onboardingStoriesDataSource) - } - @Provides fun provideBeaconConnectedUseCase(preferences: Preferences): BeaconConnectedUseCase { return BeaconConnectedUseCase(preferences) diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/AccountInteractorImpl.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/AccountInteractorImpl.kt index 1cd2d5bbd5..fb21a51fd4 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/AccountInteractorImpl.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/AccountInteractorImpl.kt @@ -11,6 +11,7 @@ import jp.co.soramitsu.common.interfaces.FileProvider import jp.co.soramitsu.core.model.Language import jp.co.soramitsu.core.models.CryptoType import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest @@ -19,7 +20,8 @@ import kotlinx.coroutines.withContext class AccountInteractorImpl( private val accountRepository: AccountRepository, - private val fileProvider: FileProvider + private val fileProvider: FileProvider, + private val context: CoroutineContext = Dispatchers.Default ) : AccountInteractor { override suspend fun generateMnemonic(): List { @@ -224,7 +226,7 @@ class AccountInteractorImpl( override fun selectedMetaAccountFlow() = accountRepository.selectedMetaAccountFlow() - override suspend fun selectedMetaAccount() = accountRepository.getSelectedMetaAccount() + override suspend fun selectedMetaAccount() = withContext(context) { accountRepository.getSelectedMetaAccount() } override suspend fun selectedLightMetaAccount() = accountRepository.getSelectedLightMetaAccount() diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/BalancesUtils.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/BalancesUtils.kt new file mode 100644 index 0000000000..49f49866bc --- /dev/null +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/BalancesUtils.kt @@ -0,0 +1,245 @@ +package jp.co.soramitsu.account.impl.domain + +import android.util.Log +import java.math.BigInteger +import jp.co.soramitsu.account.api.domain.model.MetaAccount +import jp.co.soramitsu.account.api.domain.model.accountId +import jp.co.soramitsu.common.data.network.runtime.binding.AssetBalanceData +import jp.co.soramitsu.common.data.network.runtime.binding.EmptyBalance +import jp.co.soramitsu.common.utils.Modules +import jp.co.soramitsu.common.utils.system +import jp.co.soramitsu.common.utils.tokens +import jp.co.soramitsu.core.models.Asset +import jp.co.soramitsu.core.models.ChainAssetType +import jp.co.soramitsu.core.utils.utilityAsset +import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.shared_utils.runtime.AccountId +import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.shared_utils.runtime.metadata.module +import jp.co.soramitsu.shared_utils.runtime.metadata.storage +import jp.co.soramitsu.shared_utils.runtime.metadata.storageKey +import jp.co.soramitsu.wallet.api.data.cache.bindAccountInfoOrDefault +import jp.co.soramitsu.wallet.api.data.cache.bindAssetsAccountData +import jp.co.soramitsu.wallet.api.data.cache.bindEquilibriumAccountData +import jp.co.soramitsu.wallet.api.data.cache.bindOrmlTokensAccountDataOrDefault +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.web3j.abi.FunctionEncoder +import org.web3j.abi.datatypes.Address +import org.web3j.abi.datatypes.Function +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.Ethereum +import org.web3j.protocol.core.methods.request.Transaction +import org.web3j.utils.Numeric + +fun buildStorageKeys( + chain: Chain, + metaAccount: MetaAccount, + runtime: RuntimeSnapshot +): Result> { + val accountId = metaAccount.accountId(chain) + ?: return Result.failure(RuntimeException("Can't get account id for meta account ${metaAccount.name}, chain: ${chain.name}")) + + return Result.success(buildStorageKeys(chain, runtime, metaAccount.id, accountId)) +} + +fun buildStorageKeys( + chain: Chain, + runtime: RuntimeSnapshot?, + metaAccountId: Long, + accountId: ByteArray +): List { + if (chain.utilityAsset != null && chain.utilityAsset?.typeExtra == ChainAssetType.Equilibrium) { + return listOf(buildEquilibriumStorageKeys(chain, runtime, metaAccountId, accountId)) + } + + return buildSubstrateStorageKeys(chain, runtime, metaAccountId, accountId) +} + +fun buildSubstrateStorageKeys(chain: Chain, + runtime: RuntimeSnapshot?, + metaAccountId: Long, + accountId: ByteArray): List{ + return chain.assets.map { asset -> + StorageKeyWithMetadata( + asset, metaAccountId, accountId, + runtime?.let { constructBalanceKey(it, asset, accountId) } + ) + } +} + +fun buildEquilibriumStorageKeys( + chain: Chain, + runtime: RuntimeSnapshot?, + metaAccountId: Long, + accountId: ByteArray +): StorageKeyWithMetadata { + val metadata = StorageKeyWithMetadata( + requireNotNull(chain.utilityAsset), + metaAccountId, + accountId, + null + ) + + return if (runtime == null) { + metadata + } else { + metadata.copy( + key = constructBalanceKey( + runtime, + requireNotNull(chain.utilityAsset), + accountId + ) + ) + } +} + +data class StorageKeyWithMetadata( + val asset: Asset, + val metaAccountId: Long, + val accountId: AccountId, + val key: String? +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StorageKeyWithMetadata + + if (asset != other.asset) return false + if (metaAccountId != other.metaAccountId) return false + if (!accountId.contentEquals(other.accountId)) return false + + return true + } + + override fun hashCode(): Int { + var result = asset.hashCode() + result = 31 * result + metaAccountId.hashCode() + result = 31 * result + accountId.contentHashCode() + return result + } + + override fun toString(): String { + return "StorageKeyWithMetadata(asset=${asset.name}, metaAccountId=$metaAccountId, key='$key')" + } +} + + +fun constructBalanceKey( + runtime: RuntimeSnapshot, + asset: Asset, + accountId: ByteArray +): String? { + val keyConstructionResult = runCatching { + val currency = + asset.currency ?: return@runCatching runtime.metadata.system().storage("Account") + .storageKey(runtime, accountId) + when (asset.typeExtra) { + null, ChainAssetType.Normal, + ChainAssetType.Equilibrium, + ChainAssetType.SoraUtilityAsset -> runtime.metadata.system().storage("Account") + .storageKey(runtime, accountId) + + ChainAssetType.OrmlChain, + ChainAssetType.OrmlAsset, + ChainAssetType.VToken, + ChainAssetType.VSToken, + ChainAssetType.Stable, + ChainAssetType.ForeignAsset, + ChainAssetType.StableAssetPoolToken, + ChainAssetType.SoraAsset, + ChainAssetType.AssetId, + ChainAssetType.Token2, + ChainAssetType.Xcm, + ChainAssetType.LiquidCrowdloan -> runtime.metadata.tokens().storage("Accounts") + .storageKey(runtime, accountId, currency) + + ChainAssetType.Assets -> runtime.metadata.module(Modules.ASSETS).storage("Account") + .storageKey(runtime, currency, accountId) + + ChainAssetType.Unknown -> error("Not supported type for token ${asset.symbol} in ${asset.chainName}") + } + } + return keyConstructionResult + .onFailure { + Log.d( + "BalancesUpdateSystem", + "Failed to construct storage key for asset ${asset.symbol} (${asset.id}) $it " + ) + } + .getOrNull() +} + +fun handleBalanceResponse( + runtime: RuntimeSnapshot, + asset: Asset, + scale: String? +): Result { + return runCatching { + when (asset.typeExtra) { + null, + ChainAssetType.Normal, + ChainAssetType.SoraUtilityAsset -> { + bindAccountInfoOrDefault(scale, runtime) + } + + ChainAssetType.OrmlChain, + ChainAssetType.OrmlAsset, + ChainAssetType.ForeignAsset, + ChainAssetType.StableAssetPoolToken, + ChainAssetType.LiquidCrowdloan, + ChainAssetType.VToken, + ChainAssetType.SoraAsset, + ChainAssetType.VSToken, + ChainAssetType.AssetId, + ChainAssetType.Token2, + ChainAssetType.Xcm, + ChainAssetType.Stable -> { + bindOrmlTokensAccountDataOrDefault(scale, runtime) + } + + ChainAssetType.Equilibrium -> { + bindEquilibriumAccountData(scale, runtime) ?: EmptyBalance + } + + ChainAssetType.Assets -> { + bindAssetsAccountData(scale, runtime) ?: EmptyBalance + } + + ChainAssetType.Unknown -> EmptyBalance + } + } +} + +suspend fun Ethereum.fetchEthBalance(asset: Asset, address: String): BigInteger { + return withTimeout(3000L) { + if (asset.isUtility) { + withContext(kotlinx.coroutines.Dispatchers.IO) { + ethGetBalance( + address, + DefaultBlockParameterName.LATEST + ).send().balance + } + } else { + val erc20GetBalanceFunction = Function( + "balanceOf", + listOf(Address(address)), + emptyList() + ) + + val erc20BalanceWei = withContext(kotlinx.coroutines.Dispatchers.IO) { + ethCall( + Transaction.createEthCallTransaction( + null, + asset.id, + FunctionEncoder.encode(erc20GetBalanceFunction) + ), + DefaultBlockParameterName.LATEST + ).send().value + } + + Numeric.decodeQuantity(erc20BalanceWei) + } + } +} diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/TotalBalanceUseCaseImpl.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/TotalBalanceUseCaseImpl.kt index 401eed8739..b4747f1719 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/TotalBalanceUseCaseImpl.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/TotalBalanceUseCaseImpl.kt @@ -12,6 +12,7 @@ import jp.co.soramitsu.common.utils.isNotZero import jp.co.soramitsu.common.utils.isZero import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.common.utils.percentageToFraction +import jp.co.soramitsu.common.utils.positiveOrNull import jp.co.soramitsu.coredb.dao.AssetDao import jp.co.soramitsu.coredb.model.AssetWithToken import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository @@ -60,7 +61,8 @@ class TotalBalanceUseCaseImpl( val filtered = assets .asSequence() - .filter { it.asset.freeInPlanks != null && it.asset.freeInPlanks.isNotZero() && it.token?.fiatSymbol != null } + .filter { it.asset.enabled == true } + .filter { it.asset.freeInPlanks != null && it.asset.freeInPlanks.positiveOrNull().isNotZero() && it.token?.fiatSymbol != null } .toList() // todo I did this workaround because sometimes there is a wrong symbol in asset list. Need research @@ -74,7 +76,7 @@ class TotalBalanceUseCaseImpl( ?: return@fold TotalBalance.Empty val total = - current.asset.freeInPlanks.orZero() + current.asset.reservedInPlanks.orZero() + current.asset.freeInPlanks.positiveOrNull().orZero() + current.asset.reservedInPlanks.orZero() val totalDecimal = total.toBigDecimal(scale = chainAsset.precision) val fiatAmount = totalDecimal.applyFiatRate(current.token?.fiatRate) diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/WalletSyncService.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/WalletSyncService.kt new file mode 100644 index 0000000000..53c92dfe3e --- /dev/null +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/WalletSyncService.kt @@ -0,0 +1,530 @@ +package jp.co.soramitsu.account.impl.domain + +import android.util.Log +import java.math.BigInteger +import jp.co.soramitsu.account.api.domain.model.MetaAccount +import jp.co.soramitsu.account.api.domain.model.accountId +import jp.co.soramitsu.account.impl.data.mappers.mapMetaAccountLocalToMetaAccount +import jp.co.soramitsu.common.data.network.runtime.binding.AssetBalance +import jp.co.soramitsu.common.data.network.runtime.binding.toAssetBalance +import jp.co.soramitsu.common.utils.orZero +import jp.co.soramitsu.common.utils.positiveOrNull +import jp.co.soramitsu.core.models.ChainAssetType +import jp.co.soramitsu.core.utils.utilityAsset +import jp.co.soramitsu.coredb.dao.AssetDao +import jp.co.soramitsu.coredb.dao.MetaAccountDao +import jp.co.soramitsu.coredb.model.AssetLocal +import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry +import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository +import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.runtime.multiNetwork.connection.EvmConnectionStatus +import jp.co.soramitsu.runtime.storage.source.RemoteStorageSource +import jp.co.soramitsu.shared_utils.extensions.toHexString +import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.wallet.api.data.cache.bindEquilibriumAccountData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.job +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull + +class WalletSyncService( + private val metaAccountDao: MetaAccountDao, + private val chainsRepository: ChainsRepository, + private val chainRegistry: ChainRegistry, + private val remoteStorageSource: RemoteStorageSource, + private val assetDao: AssetDao, + dispatcher: CoroutineDispatcher = Dispatchers.Default, +) { + companion object { + private const val CHAIN_SYNC_TIMEOUT_MILLIS: Long = 15_000L + } + + private val scope = + CoroutineScope(dispatcher + SupervisorJob() + CoroutineExceptionHandler { _, throwable -> + Log.d( + "WalletSyncService", + "WalletSyncService scope error: $throwable" + ) + }) + + private var syncJob: Job? = null + + fun start() { + observeNotInitializedMetaAccounts() + observeNotInitializedChainAccounts() + } + + private fun observeNotInitializedMetaAccounts() { + metaAccountDao.observeNotInitializedMetaAccounts().filter { it.isNotEmpty() } + .onEach { localMetaAccounts -> + syncJob?.cancel() + syncJob = scope.launch { + chainRegistry.configsSyncDeferred.joinAll() + + val chains = chainsRepository.getChains() + val ethereumChains = + chains.filter { it.isEthereumChain }.sortedByDescending { it.rank } + val substrateChains = + chains.filter { !it.isEthereumChain }.sortedByDescending { it.rank } + + val metaAccounts = + localMetaAccounts.map { accountInfo -> + mapMetaAccountLocalToMetaAccount( + chains.associateBy { it.id }, + accountInfo + ) + } + val accountHasAssetWithPositiveBalanceMap = mutableMapOf() + + supervisorScope { + launch { + ethereumChains.forEach { chain -> + launch { + val assetsDeferred = async { + if (chainRegistry.checkChainSyncedUp(chain).not()) { + chainRegistry.setupChain(chain) + } + val connection = + withTimeoutOrNull(CHAIN_SYNC_TIMEOUT_MILLIS) { + val connection = + chainRegistry.awaitEthereumConnection(chain.id) + // await connecting to the node + connection.statusFlow.first { it is EvmConnectionStatus.Connected } + connection + } + + metaAccounts.mapNotNull { metaAccount -> + val accountId = + metaAccount.accountId(chain) + ?: return@mapNotNull null + + chain.assets.map { chainAsset -> + val balance = kotlin.runCatching { + connection?.web3j?.fetchEthBalance( + chainAsset, + accountId.toHexString(true) + ) + }.getOrNull() + + if (balance.positiveOrNull() != null) { + accountHasAssetWithPositiveBalanceMap[metaAccount.id] = + true + } + + val isPopularUtilityAsset = + chain.rank != null && chainAsset.isUtility + val accountHasAssetWithPositiveBalance = + accountHasAssetWithPositiveBalanceMap[metaAccount.id] == true + + AssetLocal( + id = chainAsset.id, + chainId = chain.id, + accountId = accountId, + metaId = metaAccount.id, + tokenPriceId = chainAsset.priceId, + freeInPlanks = balance, + reservedInPlanks = BigInteger.ZERO, + miscFrozenInPlanks = BigInteger.ZERO, + feeFrozenInPlanks = BigInteger.ZERO, + bondedInPlanks = BigInteger.ZERO, + redeemableInPlanks = BigInteger.ZERO, + unbondingInPlanks = BigInteger.ZERO, + enabled = balance.positiveOrNull() != null || (!accountHasAssetWithPositiveBalance && isPopularUtilityAsset) + ) + } + }.flatten() + } + val localAssets = assetsDeferred.await() + assetDao.insertAssets(localAssets) + hideEmptyAssetsIfThereAreAtLeastOnePositiveBalanceByMetaAccounts(metaAccounts) + } + } + } + launch { + substrateChains.onEach { chain -> + launch { + val assetsDeferred = async { + val emptyAssets: MutableList = mutableListOf() + val runtime = withTimeoutOrNull(CHAIN_SYNC_TIMEOUT_MILLIS) { + if (chainRegistry.checkChainSyncedUp(chain).not()) { + chainRegistry.setupChain(chain) + } + + // awaiting runtime snapshot + chainRegistry.awaitRuntimeProvider(chain.id).get() + } + + val isEquilibriumTypeChain = + chain.utilityAsset != null && chain.utilityAsset!!.typeExtra == ChainAssetType.Equilibrium + + if (isEquilibriumTypeChain) { + buildEquilibriumAssetsByMetaAccounts(metaAccounts, chain, runtime) + } else { + val allAccountsStorageKeys = + metaAccounts.mapNotNull { metaAccount -> + val accountId = + metaAccount.accountId(chain) + ?: return@mapNotNull null + buildSubstrateStorageKeys( + chain, + runtime, + metaAccount.id, + accountId + ) + }.flatten() + + val keysToQuery = + allAccountsStorageKeys.mapNotNull { metadata -> + // if storage key build is failed - we put the empty assets + if (metadata.key == null) { + emptyAssets.add( + AssetLocal( + accountId = metadata.accountId, + id = metadata.asset.id, + chainId = metadata.asset.chainId, + metaId = metadata.metaAccountId, + tokenPriceId = metadata.asset.priceId, + enabled = false, + freeInPlanks = BigInteger.valueOf(-1) + ) + ) + } + metadata.key + }.toList() + + val storageKeyToResult = remoteStorageSource.queryKeys( + keysToQuery, + chain.id, + null + ) + + allAccountsStorageKeys.map { metadata -> + val hexRaw = + storageKeyToResult.getOrDefault( + metadata.key, + null + ) + + val assetBalance = + runtime?.let { + handleBalanceResponse( + it, + metadata.asset, + hexRaw + ).getOrNull().toAssetBalance() + } ?: AssetBalance() + + if (assetBalance.freeInPlanks.positiveOrNull() != null) { + accountHasAssetWithPositiveBalanceMap[metadata.metaAccountId] = + true + } + + val isPopularUtilityAsset = + chain.rank != null && metadata.asset.isUtility + val accountHasAssetWithPositiveBalance = + accountHasAssetWithPositiveBalanceMap[metadata.metaAccountId] == true + + AssetLocal( + id = metadata.asset.id, + chainId = chain.id, + accountId = metadata.accountId, + metaId = metadata.metaAccountId, + tokenPriceId = metadata.asset.priceId, + freeInPlanks = assetBalance.freeInPlanks, + reservedInPlanks = assetBalance.reservedInPlanks, + miscFrozenInPlanks = assetBalance.miscFrozenInPlanks, + feeFrozenInPlanks = assetBalance.feeFrozenInPlanks, + bondedInPlanks = assetBalance.bondedInPlanks, + redeemableInPlanks = assetBalance.redeemableInPlanks, + unbondingInPlanks = assetBalance.unbondingInPlanks, + enabled = assetBalance.freeInPlanks.positiveOrNull() != null || (!accountHasAssetWithPositiveBalance && isPopularUtilityAsset) + ) + } + emptyAssets + } + } + val localAssets = assetsDeferred.await() + assetDao.insertAssets(localAssets) + hideEmptyAssetsIfThereAreAtLeastOnePositiveBalanceByMetaAccounts(metaAccounts) + } + } + } + + this + }.coroutineContext.job.join() + + coroutineScope { + metaAccountDao.markAccountsInitialized(metaAccounts.map { it.id }) + hideEmptyAssetsIfThereAreAtLeastOnePositiveBalanceByMetaAccounts(metaAccounts) + } + } + } + .launchIn(scope) + } + + private fun observeNotInitializedChainAccounts() { + metaAccountDao.observeNotInitializedChainAccounts().filter { it.isNotEmpty() } + .onEach { chainAccounts -> + chainRegistry.configsSyncDeferred.joinAll() + val chains = chainAccounts.map { chainRegistry.getChain(it.chainId) }.associateBy { it.id } + val ethereumChains = + chains.values.filter { it.isEthereumChain }.sortedByDescending { it.rank } + val substrateChains = + chains.values.filter { !it.isEthereumChain }.sortedByDescending { it.rank } + + supervisorScope { + launch { + ethereumChains.forEach { chain -> + launch { + val assetsDeferred = async { + if (chainRegistry.checkChainSyncedUp(chain).not()) { + chainRegistry.setupChain(chain) + } + val connection = + withTimeoutOrNull(CHAIN_SYNC_TIMEOUT_MILLIS) { + val connection = + chainRegistry.awaitEthereumConnection(chain.id) + // await connecting to the node + connection.statusFlow.first { it is EvmConnectionStatus.Connected } + connection + } + + chainAccounts.map { chainAccount -> + chainAccount.accountId + val accountId = chainAccount.accountId + + chain.assets.map { chainAsset -> + val balance = kotlin.runCatching { + connection?.web3j?.fetchEthBalance( + chainAsset, + accountId.toHexString(true) + ) + }.getOrNull() + + AssetLocal( + id = chainAsset.id, + chainId = chain.id, + accountId = accountId, + metaId = chainAccount.metaId, + tokenPriceId = chainAsset.priceId, + freeInPlanks = balance, + reservedInPlanks = BigInteger.ZERO, + miscFrozenInPlanks = BigInteger.ZERO, + feeFrozenInPlanks = BigInteger.ZERO, + bondedInPlanks = BigInteger.ZERO, + redeemableInPlanks = BigInteger.ZERO, + unbondingInPlanks = BigInteger.ZERO, + enabled = balance.positiveOrNull()!= null || chainAsset.isUtility + ) + } + }.flatten() + } + val localAssets = assetsDeferred.await() + assetDao.insertAssets(localAssets) + } + } + } + launch { + substrateChains.onEach { chain -> + launch { + val assetsDeferred = async { + val emptyAssets: MutableList = mutableListOf() + val runtime = withTimeoutOrNull(CHAIN_SYNC_TIMEOUT_MILLIS) { + if (chainRegistry.checkChainSyncedUp(chain).not()) { + chainRegistry.setupChain(chain) + } + + // awaiting runtime snapshot + chainRegistry.awaitRuntimeProvider(chain.id).get() + } + + val isEquilibriumTypeChain = + chain.utilityAsset != null && chain.utilityAsset!!.typeExtra == ChainAssetType.Equilibrium + + if (isEquilibriumTypeChain) { + buildEquilibriumAssets(chainAccounts.map { it.metaId to it.accountId }, chain, runtime) + } else { + val allAccountsStorageKeys = + chainAccounts.map { chainAccount -> + buildSubstrateStorageKeys( + chain, + runtime, + chainAccount.metaId, + chainAccount.accountId + ) + }.flatten() + + val keysToQuery = + allAccountsStorageKeys.mapNotNull { metadata -> + // if storage key build is failed - we put the empty assets + if (metadata.key == null) { + emptyAssets.add( + AssetLocal( + accountId = metadata.accountId, + id = metadata.asset.id, + chainId = metadata.asset.chainId, + metaId = metadata.metaAccountId, + tokenPriceId = metadata.asset.priceId, + enabled = false, + freeInPlanks = BigInteger.valueOf(-1) + ) + ) + } + metadata.key + }.toList() + + val storageKeyToResult = remoteStorageSource.queryKeys( + keysToQuery, + chain.id, + null + ) + + allAccountsStorageKeys.map { metadata -> + val hexRaw = + storageKeyToResult.getOrDefault( + metadata.key, + null + ) + + val assetBalance = + runtime?.let { + handleBalanceResponse( + it, + metadata.asset, + hexRaw + ).getOrNull().toAssetBalance() + } ?: AssetBalance() + + AssetLocal( + id = metadata.asset.id, + chainId = chain.id, + accountId = metadata.accountId, + metaId = metadata.metaAccountId, + tokenPriceId = metadata.asset.priceId, + freeInPlanks = assetBalance.freeInPlanks, + reservedInPlanks = assetBalance.reservedInPlanks, + miscFrozenInPlanks = assetBalance.miscFrozenInPlanks, + feeFrozenInPlanks = assetBalance.feeFrozenInPlanks, + bondedInPlanks = assetBalance.bondedInPlanks, + redeemableInPlanks = assetBalance.redeemableInPlanks, + unbondingInPlanks = assetBalance.unbondingInPlanks, + enabled = assetBalance.freeInPlanks.positiveOrNull() != null || metadata.asset.isUtility + ) + } + emptyAssets + } + } + val localAssets = assetsDeferred.await() + assetDao.insertAssets(localAssets) + } + } + } + + this + }.coroutineContext.job.join() + coroutineScope { + chainAccounts.forEach { + metaAccountDao.markChainAccountInitialized(it.metaId, it.chainId) + } + } + } + .launchIn(scope) + } + + private suspend fun hideEmptyAssetsIfThereAreAtLeastOnePositiveBalanceByMetaAccounts(metaAccounts: List) { + hideEmptyAssetsIfThereAreAtLeastOnePositiveBalanceByMetaIds(metaAccounts.map { it.id }) + } + + private suspend fun hideEmptyAssetsIfThereAreAtLeastOnePositiveBalanceByMetaIds(metaAccountsIds: List) { + coroutineScope { + metaAccountsIds.forEach { + withContext(Dispatchers.IO) { + assetDao.hideEmptyAssetsIfThereAreAtLeastOnePositiveBalance( + it + ) + } + } + } + } + + private suspend fun buildEquilibriumAssetsByMetaAccounts( + metaAccounts: List, + chain: Chain, + runtime: RuntimeSnapshot? + ): List { + return buildEquilibriumAssets(metaAccounts.map { it.id to it.accountId(chain) }, chain, runtime) + } + + private suspend fun buildEquilibriumAssets( + accountInfo: List>, + chain: Chain, + runtime: RuntimeSnapshot? + ): List { + val emptyAssets: MutableList = mutableListOf() + + val allAccountsStorageKeys = accountInfo.mapNotNull { (metaId, accountId) -> + accountId ?: return@mapNotNull null + buildEquilibriumStorageKeys(chain, runtime, metaId, accountId) + }.associateBy { it.key } + + val keysToQuery = + allAccountsStorageKeys.mapNotNull { (storageKey, metadata) -> + // if storage key build is failed - we put the empty assets + if (storageKey == null) { + // filling all the equilibrium assets + val empty = chain.assets.map { + AssetLocal( + accountId = metadata.accountId, + id = it.id, + chainId = it.chainId, + metaId = metadata.metaAccountId, + tokenPriceId = it.priceId, + enabled = false, + freeInPlanks = BigInteger.valueOf(-1) + ) + } + emptyAssets.addAll(empty) + } + storageKey + }.toList() + + val storageKeyToResult = remoteStorageSource.queryKeys(keysToQuery, chain.id, null) + + return storageKeyToResult.mapNotNull { (storageKey, hexRaw) -> + val metadata = allAccountsStorageKeys[storageKey] ?: return@mapNotNull null + + val balanceData = runtime?.let { bindEquilibriumAccountData(hexRaw, it) } + val equilibriumAssetsBalanceMap = balanceData?.data?.balances.orEmpty() + + chain.assets.map { asset -> + val balance = + asset.currencyId?.toBigInteger()?.let { + equilibriumAssetsBalanceMap.getOrDefault(it, null) + .orZero() + }.orZero() + + AssetLocal( + accountId = metadata.accountId, + id = asset.id, + chainId = asset.chainId, + metaId = metadata.metaAccountId, + tokenPriceId = asset.priceId, + enabled = balance.positiveOrNull() != null, + freeInPlanks = balance + ) + } + }.flatten() + emptyAssets + } +} \ No newline at end of file diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/account/details/AccountDetailsInteractor.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/account/details/AccountDetailsInteractor.kt index a92dc3531a..cf90f5662b 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/account/details/AccountDetailsInteractor.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/domain/account/details/AccountDetailsInteractor.kt @@ -18,8 +18,10 @@ import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.defaultChainSort import jp.co.soramitsu.shared_utils.scale.EncodableStruct +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map class AccountDetailsInteractor( @@ -56,6 +58,25 @@ class AccountDetailsInteractor( } } + @OptIn(ExperimentalCoroutinesApi::class) + fun hasChainsWithNoAccount() = accountRepository.selectedMetaAccountFlow() + .flatMapLatest { metaAccount -> + combine( + chainRegistry.currentChains.map { it.sortedWith(chainSort()) }, + assetNotNeedAccountUseCase.getAssetsMarkedNotNeedFlow(metaAccount.id) + ) { chains, assetsMarkedNotNeed -> + chains.any { chain -> + chain.assets.any { chainAsset -> + val markedNotNeed = assetsMarkedNotNeed.contains( + AssetKey(metaAccount.id, chain.id, emptyAccountIdValue, chainAsset.id) + ) + val hasAccount = !chain.isEthereumBased || metaAccount.ethereumPublicKey != null || metaAccount.hasChainAccount(chain.id) + hasAccount.not() && markedNotNeed.not() + } + } + } + } + private fun createAccountInChain(metaAccount: MetaAccount, chain: Chain, markedNotNeed: Boolean): AccountInChain { val address = metaAccount.address(chain) val accountId = metaAccount.accountId(chain) diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsContent.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsContent.kt index 6f06e6d36d..2023d8189a 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsContent.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsContent.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import jp.co.soramitsu.account.impl.presentation.importing.remote_backup.views.CompactWalletItemViewState import jp.co.soramitsu.common.R import jp.co.soramitsu.common.compose.component.B0 import jp.co.soramitsu.common.compose.component.CapsTitle2 @@ -81,9 +80,7 @@ internal fun AccountDetailsContent( WalletItem( modifier = Modifier .padding(horizontal = 16.dp), - state = CompactWalletItemViewState( - title = state.walletItem.title - ), + state = state.walletItem, onSelected = {} ) } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsViewModel.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsViewModel.kt index a1afe67f64..c2f277d8bf 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsViewModel.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/account/details/AccountDetailsViewModel.kt @@ -22,8 +22,6 @@ import jp.co.soramitsu.common.address.createAddressIcon import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.compose.component.ChangeBalanceViewState import jp.co.soramitsu.common.compose.component.WalletItemViewState -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder -import jp.co.soramitsu.common.domain.SelectedFiat import jp.co.soramitsu.common.list.headers.TextHeader import jp.co.soramitsu.common.list.toListWithHeaders import jp.co.soramitsu.common.resources.ResourceManager @@ -36,19 +34,17 @@ import jp.co.soramitsu.core.utils.utilityAsset import jp.co.soramitsu.feature_account_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch private const val UPDATE_NAME_INTERVAL_SECONDS = 1L @@ -69,7 +65,7 @@ class AccountDetailsViewModel @Inject constructor( private val walletId = savedStateHandle.get(ACCOUNT_ID_KEY)!! private val wallet = flowOf { interactor.getMetaAccount(walletId) - } + }.share() private val walletItem = wallet .map { wallet -> @@ -106,9 +102,9 @@ class AccountDetailsViewModel @Inject constructor( val openPlayMarket: LiveData> = _openPlayMarket private val enteredQueryFlow = MutableStateFlow("") - val accountNameFlow = MutableStateFlow("") + private val accountNameFlow = MutableStateFlow("") - val chainAccountProjections = combine( + private val chainAccountProjections = combine( interactor.getChainProjectionsFlow(walletId), enteredQueryFlow ) { groupedList, query -> @@ -123,18 +119,7 @@ class AccountDetailsViewModel @Inject constructor( .inBackground() .share() - val state = combine( - walletItem, - chainAccountProjections, - enteredQueryFlow - ) { walletItem, chainProjections, query -> - AccountDetailsState( - walletItem = walletItem, - chainProjections = chainProjections, - searchQuery = query - ) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, AccountDetailsState.Empty) + val state = MutableStateFlow(AccountDetailsState.Empty) init { launch { @@ -142,6 +127,21 @@ class AccountDetailsViewModel @Inject constructor( } syncNameChangesWithDb() + subscribeScreenState() + } + + private fun subscribeScreenState() { + walletItem.onEach { + state.value = state.value.copy(walletItem = it) + }.launchIn(this) + + chainAccountProjections.onEach { + state.value = state.value.copy(chainProjections = it) + }.launchIn(this) + + enteredQueryFlow.onEach { + state.value = state.value.copy(searchQuery = it) + }.launchIn(this) } override fun onBackClick() { @@ -249,7 +249,7 @@ class AccountDetailsViewModel @Inject constructor( if (item.hasAccount) { val chain = chainRegistry.getChain(item.chainId) val supportedExplorers = - chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, item.address) + chain.explorers.getSupportedAddressExplorers(item.address) externalAccountActions.showExternalActions(ExternalAccountActions.Payload(item.address, item.chainId, item.chainName, supportedExplorers, !chain.isEthereumChain diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountFragment.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountFragment.kt index 31c4ae718c..6c2c884b1f 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountFragment.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountFragment.kt @@ -129,10 +129,6 @@ class ImportAccountFragment : BaseFragment() { setupAdvancedBlock(blockchainType, sourceType, isChainAccount) } }.observe { } - - viewModel.showInvalidSubstrateDerivationPathError.observeEvent { - showError(resources.getString(R.string.common_invalid_hard_soft_numeric_password_message)) - } } private fun buildSourceTypesViews(blockchainType: ImportAccountType) = viewModel.sourceTypes.map { diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountViewModel.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountViewModel.kt index 2400692638..1c334f03bb 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountViewModel.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/ImportAccountViewModel.kt @@ -28,6 +28,7 @@ import jp.co.soramitsu.account.impl.presentation.importing.source.model.ImportSo import jp.co.soramitsu.account.impl.presentation.importing.source.model.JsonImportSource import jp.co.soramitsu.account.impl.presentation.importing.source.model.MnemonicImportSource import jp.co.soramitsu.account.impl.presentation.importing.source.model.RawSeedImportSource +import jp.co.soramitsu.account.impl.presentation.mnemonic.backup.exceptions.NotValidDerivationPath import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.resources.ClipboardManager import jp.co.soramitsu.common.resources.ResourceManager @@ -79,9 +80,6 @@ class ImportAccountViewModel @Inject constructor( val substrateDerivationPathLiveData = MutableLiveData() val ethereumDerivationPathLiveData = MutableLiveData() - private val _showInvalidSubstrateDerivationPathError = MutableLiveData>() - val showInvalidSubstrateDerivationPathError: LiveData> = _showInvalidSubstrateDerivationPathError - private val substrateDerivationPathRegex = Regex("(//?[^/]+)*(///[^/]+)?") val sourceTypes = provideSourceType() @@ -126,7 +124,7 @@ class ImportAccountViewModel @Inject constructor( _blockchainTypeLiveData.value = importAccountType } initialBlockchainType.value != null -> _blockchainTypeLiveData.value = initialBlockchainType.value?.let { int -> - ImportAccountType.values().getOrNull(int) + ImportAccountType.entries.getOrNull(int) }!! } } @@ -148,7 +146,7 @@ class ImportAccountViewModel @Inject constructor( fun nextClicked() { val isSubstrateDerivationPathValid = substrateDerivationPathLiveData.value?.matches(substrateDerivationPathRegex) if (isSubstrateDerivationPathValid == false) { - _showInvalidSubstrateDerivationPathError.value = Event(Unit) + showError(NotValidDerivationPath(resourceManager)) return } val source = _selectedSourceTypeLiveData.value diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/remote_backup/ImportRemoteWalletDialog.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/remote_backup/ImportRemoteWalletDialog.kt index 92aeaf2666..a0dbde9a37 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/remote_backup/ImportRemoteWalletDialog.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/remote_backup/ImportRemoteWalletDialog.kt @@ -1,23 +1,43 @@ package jp.co.soramitsu.account.impl.presentation.importing.remote_backup +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.View import android.widget.FrameLayout +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.os.bundleOf import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetBehavior import dagger.hilt.android.AndroidEntryPoint import jp.co.soramitsu.common.base.BaseComposeBottomSheetDialogFragment +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @AndroidEntryPoint class ImportRemoteWalletDialog : BaseComposeBottomSheetDialogFragment() { override val viewModel: ImportRemoteWalletViewModel by viewModels() + private val launcher: ActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + when (result.resultCode) { + Activity.RESULT_OK -> viewModel.loadRemoteWallets() + Activity.RESULT_CANCELED -> { /* no action */ } + else -> { + val googleSignInStatus = result.data?.extras?.get("googleSignInStatus") + viewModel.onGoogleLoginError(googleSignInStatus.toString()) + } + } + } + @Composable override fun Content(padding: PaddingValues) { val state by viewModel.state.collectAsState() @@ -40,5 +60,9 @@ class ImportRemoteWalletDialog : BaseComposeBottomSheetDialogFragment>() + private val defaultTextInputViewState = TextInputViewState( text = "", hint = resourceManager.getString(R.string.import_remote_wallet_hint_enter_password), @@ -156,7 +160,13 @@ class ImportRemoteWalletViewModel @Inject constructor( override fun loadRemoteWallets() { viewModelScope.launch { - val backupAccounts = backupService.getBackupAccounts().map(::getWrapped) + val backupAccounts = try { + backupService.getBackupAccounts().map(::getWrapped) + } catch (e: AuthConsentException) { + requestGoogleAuth.emit(Event(e.intent)) + return@launch + } + val webBackupAccounts = backupService.getWebBackupAccounts() .distinctBy { it.address } .map { getWrapped(it, origin = BackupOrigin.WEB) } @@ -168,6 +178,10 @@ class ImportRemoteWalletViewModel @Inject constructor( } } + fun onGoogleLoginError(message: String) { + showError("GoogleLoginError\n$message") + } + private fun getWrapped(backupMeta: BackupAccountMeta, origin: BackupOrigin = BackupOrigin.APP): WrappedBackupAccountMeta { return WrappedBackupAccountMeta(backupMeta, origin) } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/source/view/JsonPasteOptionsSheet.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/source/view/JsonPasteOptionsSheet.kt index 526acfafde..b57efac648 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/source/view/JsonPasteOptionsSheet.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/importing/source/view/JsonPasteOptionsSheet.kt @@ -21,7 +21,7 @@ class JsonPasteOptionsSheet( onPaste() } - item(icon = R.drawable.ic_file_upload, titleRes = R.string.recover_json_hint) { + item(icon = R.drawable.ic_file_upload, titleRes = R.string.common_choose_file) { onOpenFile() } } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicFragment.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicFragment.kt index eb544cf33a..b1625d280e 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicFragment.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicFragment.kt @@ -108,10 +108,6 @@ class BackupMnemonicFragment : BaseFragment(R.layout.fr showMnemonicInfoDialog() } - viewModel.showInvalidSubstrateDerivationPathError.observeEvent { - showError(resources.getString(R.string.common_invalid_hard_soft_numeric_password_message)) - } - viewModel.chainAccountImportType.observe(binding.advancedBlockView::configureForMnemonic) } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicViewModel.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicViewModel.kt index 5f56f93c4f..a18dafb0f3 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicViewModel.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/backup/BackupMnemonicViewModel.kt @@ -94,9 +94,6 @@ class BackupMnemonicViewModel @Inject constructor( private val _showInfoEvent = MutableLiveData>() val showInfoEvent: LiveData> = _showInfoEvent - private val _showInvalidSubstrateDerivationPathError = MutableLiveData>() - val showInvalidSubstrateDerivationPathError: LiveData> = _showInvalidSubstrateDerivationPathError - fun homeButtonClicked() { router.backToCreateAccountScreen() } @@ -164,7 +161,6 @@ class BackupMnemonicViewModel @Inject constructor( val isSubstrateDerivationPathValid = substrateDerivationPath.matches(substrateDerivationPathRegex) if (isSubstrateDerivationPathValid.not()) { - _showInvalidSubstrateDerivationPathError.value = Event(Unit) showError(NotValidDerivationPath(resourceManager)) return } @@ -212,7 +208,6 @@ class BackupMnemonicViewModel @Inject constructor( ) { val isSubstrateDerivationPathValid = substrateDerivationPath.matches(substrateDerivationPathRegex) if (isSubstrateDerivationPathValid.not()) { - _showInvalidSubstrateDerivationPathError.value = Event(Unit) showError(NotValidDerivationPath(resourceManager)) return } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/confirm/ConfirmMnemonicViewModel.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/confirm/ConfirmMnemonicViewModel.kt index 37656c857f..52df894912 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/confirm/ConfirmMnemonicViewModel.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/mnemonic/confirm/ConfirmMnemonicViewModel.kt @@ -11,6 +11,8 @@ import jp.co.soramitsu.account.api.domain.interfaces.AccountInteractor import jp.co.soramitsu.account.impl.presentation.AccountRouter import jp.co.soramitsu.account.impl.presentation.mnemonic.confirm.ConfirmMnemonicFragment.Companion.KEY_PAYLOAD import jp.co.soramitsu.common.base.BaseViewModel +import jp.co.soramitsu.common.compose.component.ChainSelectorViewStateWithFilters +import jp.co.soramitsu.common.model.AssetBooleanState import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.combine @@ -19,6 +21,9 @@ import jp.co.soramitsu.common.utils.requireException import jp.co.soramitsu.common.utils.sendEvent import jp.co.soramitsu.common.vibration.DeviceVibrator import jp.co.soramitsu.feature_account_impl.R +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @HiltViewModel diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt index 3bbb84c5d1..875259f5d4 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt @@ -50,10 +50,9 @@ class ProfileFragment : BaseFragment() { profileExperimentalFeatures.setOnClickListener { viewModel.onExperimentalClicked() } polkaswapDisclaimerTv.setOnClickListener { viewModel.polkaswapDisclaimerClicked() } profileSoraCard.setOnClickListener { viewModel.onSoraCardClicked() } - hideZeroBalancesContainer.setOnClickListener { viewModel.onHideZeroBalancesClick() } profileWalletConnect.setOnClickListener { viewModel.onWalletConnectClick() } - viewModel.hasMissingAccountsFlow.observe { + viewModel.hasChainsWithNoAccountFlow.observe { missingAccountsIcon.isVisible = it } } @@ -83,10 +82,6 @@ class ProfileFragment : BaseFragment() { viewModel.showFiatChooser.observeEvent(::showFiatChooser) viewModel.selectedFiatLiveData.observe(binding.selectedCurrencyTv::setText) - - viewModel.hideZeroBalancesState.observe { - binding.hideZeroBalancesSwitch.isChecked = it - } } private fun showFiatChooser(payload: DynamicListBottomSheet.Payload) { diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt index 3504b97953..541a07fba9 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt @@ -11,6 +11,7 @@ import jp.co.soramitsu.account.api.domain.interfaces.AccountInteractor import jp.co.soramitsu.account.api.domain.interfaces.TotalBalanceUseCase import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions +import jp.co.soramitsu.account.impl.domain.account.details.AccountDetailsInteractor import jp.co.soramitsu.account.impl.presentation.AccountRouter import jp.co.soramitsu.account.impl.presentation.language.mapper.mapLanguageToLanguageModel import jp.co.soramitsu.common.address.AddressIconGenerator @@ -22,14 +23,11 @@ import jp.co.soramitsu.common.data.network.coingecko.FiatCurrency import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies import jp.co.soramitsu.common.domain.SelectedFiat import jp.co.soramitsu.common.resources.ResourceManager -import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.formatFiat import jp.co.soramitsu.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet -import jp.co.soramitsu.feature_account_impl.R import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull @@ -43,7 +41,7 @@ private const val AVATAR_SIZE_DP = 32 @HiltViewModel class ProfileViewModel @Inject constructor( private val interactor: AccountInteractor, - private val walletInteractor: WalletInteractor, + private val accountDetailsInteractor: AccountDetailsInteractor, private val soraCardInteractor: SoraCardInteractor, private val router: AccountRouter, private val addressIconGenerator: AddressIconGenerator, @@ -76,9 +74,8 @@ class ProfileViewModel @Inject constructor( val selectedFiatLiveData: LiveData = selectedFiat.flow().asLiveData().map { it.uppercase() } - val hasMissingAccountsFlow = walletInteractor.assetsFlow().map { - it.any { it.hasAccount.not() } - }.stateIn(this, SharingStarted.Eagerly, false) + val hasChainsWithNoAccountFlow = accountDetailsInteractor.hasChainsWithNoAccount() + .stateIn(this, SharingStarted.Eagerly, false) // private val soraCardState = soraCardInteractor.subscribeSoraCardInfo().map { // val kycStatus = it?.kycStatus?.let(::mapKycStatus) @@ -86,8 +83,6 @@ class ProfileViewModel @Inject constructor( // } private val soraCardState = flowOf(SoraCardItemViewState()) - val hideZeroBalancesState: Flow = walletInteractor.observeHideZeroBalanceEnabledForCurrentWallet() - fun aboutClicked() { router.openAboutScreen() } @@ -155,12 +150,6 @@ class ProfileViewModel @Inject constructor( private fun onSoraCardStatusClicked() { } - fun onHideZeroBalancesClick() { - viewModelScope.launch { - walletInteractor.toggleHideZeroBalancesForCurrentWallet() - } - } - fun onWalletConnectClick() { router.openConnectionsScreen() } diff --git a/feature-account-impl/src/main/res/layout/fragment_profile.xml b/feature-account-impl/src/main/res/layout/fragment_profile.xml index 32fa7f1d86..7a5a1befd1 100644 --- a/feature-account-impl/src/main/res/layout/fragment_profile.xml +++ b/feature-account-impl/src/main/res/layout/fragment_profile.xml @@ -255,42 +255,6 @@ - - - - - - - - - - > { + ): Flow> = withContext(coroutineContext) { val chain = chainsRepository.getChain(token.chainId) val connection = getWeb3Connection(chain.id) @@ -57,8 +63,7 @@ class NFTTransferInteractorImpl( """.trimIndent() ) } - - return connection.subscribeNewHeads().transform { newHead -> + return@withContext connection.subscribeBaseFeePerGas().filter { it != null }.transform { baseFeePerGas -> val nftTransfer = NFTTransferAdapter( web3j = connection.nonNullWeb3j, sender = senderResult.getOrThrow(), @@ -67,24 +72,25 @@ class NFTTransferInteractorImpl( canReceiverAcceptToken = canReceiverAcceptToken ) - val networkFee = connection.EstimateEthTransactionNetworkFee( + val networkFee = connection.estimateEthTransactionNetworkFee( call = nftTransfer, - baseFeePerGas = Numeric.decodeQuantity(newHead.params.result?.baseFeePerGas) + baseFeePerGas = baseFeePerGas ?: return@transform ) - emit(Result.success(networkFee)) - }.catch { emit(Result.failure(it)) } + }.catch { + emit(Result.failure(it)) + }.flowOn(Dispatchers.Default) } override suspend fun send( token: NFT, receiver: String, canReceiverAcceptToken: Boolean - ): Result { + ): Result = withContext(coroutineContext) { val chain = chainsRepository.getChain(token.chainId) val connection = getWeb3Connection(chain.id) - return runCatching { + runCatching { val ethereumSecrets = accountRepository.getMetaAccountSecrets( metaId = accountRepository.getSelectedMetaAccount().id @@ -124,11 +130,11 @@ class NFTTransferInteractorImpl( } } - override suspend fun balance(token: NFT): Result { + override suspend fun balance(token: NFT): Result = withContext(coroutineContext) { val chain = chainsRepository.getChain(token.chainId) val connection = getWeb3Connection(chain.id) - return runCatching { + runCatching { val sender = accountRepository.getSelectedMetaAccount().address(chain) ?: error( """ Currently selected account is unavailable now. diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/CreateRawEthTransaction.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/CreateRawEthTransaction.kt index 1ebacc3d70..0d8056a9ab 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/CreateRawEthTransaction.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/CreateRawEthTransaction.kt @@ -14,7 +14,7 @@ suspend fun EthereumChainConnection.CreateRawEthTransaction(call: EthCall): RawT EIP1559CallImpl.createAsync( ethConnection = this, call = call, - estimateGas = EstimateEthTransactionGas( + estimateGas = estimateEthTransactionGas( call = call ) ) diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionGas.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionGas.kt index f9dc28290e..a8b26df6d7 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionGas.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionGas.kt @@ -9,11 +9,8 @@ import org.web3j.protocol.core.methods.request.Transaction import org.web3j.utils.Numeric import java.math.BigInteger -@Suppress("FunctionName") -suspend fun EthereumChainConnection.EstimateEthTransactionGas(call: EthCall): BigInteger { - val response = nonNullWeb3j.ethEstimateGas( - call.convertToWeb3Transaction() - ).sendAsync().await() +suspend fun EthereumChainConnection.estimateEthTransactionGas(call: EthCall): BigInteger { + val response = nonNullWeb3j.ethEstimateGas(call.convertToWeb3Transaction()).sendAsync().await() return response.map { Numeric.decodeQuantity(it) } } diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionNetworkFee.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/estimateEthTransactionNetworkFee.kt similarity index 58% rename from feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionNetworkFee.kt rename to feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/estimateEthTransactionNetworkFee.kt index 0b3037ae35..6bd9c29b5a 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/EstimateEthTransactionNetworkFee.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/usecase/eth/estimateEthTransactionNetworkFee.kt @@ -9,26 +9,24 @@ import jp.co.soramitsu.runtime.multiNetwork.connection.EthereumChainConnection import java.math.BigDecimal import java.math.BigInteger -@Suppress("FunctionName", "UseIfInsteadOfWhen") -suspend fun EthereumChainConnection.EstimateEthTransactionNetworkFee( +suspend fun EthereumChainConnection.estimateEthTransactionNetworkFee( call: EthCall, baseFeePerGas: BigInteger ): BigDecimal { - val eip1559Transfer = when (call) { - is EthCall.SmartContractCall -> - EIP1559CallImpl.createAsync( - ethConnection = this, - call = call, - baseFeePerGas = baseFeePerGas, - estimateGas = EstimateEthTransactionGas( - call = call - ) - ) + val eip1559Transfer = if (call is EthCall.SmartContractCall) { + val estimateGas = estimateEthTransactionGas(call = call) - else -> error( + EIP1559CallImpl.createAsync( + ethConnection = this, + call = call, + baseFeePerGas = baseFeePerGas, + estimateGas = estimateGas + ) + } else { + error( """ - Unknown transfer type. - """.trimIndent() + Unknown transfer type. + """.trimIndent() ) } diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/utils/Web3AdapterExt.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/utils/Web3AdapterExt.kt index 6eb11a515a..0c99c279b1 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/utils/Web3AdapterExt.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/domain/utils/Web3AdapterExt.kt @@ -1,16 +1,12 @@ package jp.co.soramitsu.nft.impl.domain.utils import jp.co.soramitsu.runtime.multiNetwork.connection.EthereumChainConnection -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.future.await -import kotlinx.coroutines.reactive.asFlow import org.web3j.protocol.Web3j import org.web3j.protocol.Web3jService import org.web3j.protocol.core.DefaultBlockParameterName import org.web3j.protocol.core.Request import org.web3j.protocol.core.Response -import org.web3j.protocol.core.methods.response.EthSubscribe -import org.web3j.protocol.websocket.events.Notification import org.web3j.utils.Numeric import java.math.BigInteger @@ -49,8 +45,7 @@ inline fun Response.map(crossinline transform: (T) -> K): K { } suspend fun Web3j.getNonce(address: String): BigInteger { - val response = ethGetTransactionCount(address, DefaultBlockParameterName.PENDING) - .sendAsync().await() + val response = ethGetTransactionCount(address, DefaultBlockParameterName.PENDING).sendAsync().await() return response.map { Numeric.decodeQuantity(it) } } @@ -80,42 +75,3 @@ suspend fun Web3j.getBaseFee(): BigInteger { return response.map { Numeric.decodeQuantity(it.baseFeePerGas) } } - -fun EthereumChainConnection.subscribeNewHeads(): Flow { - return nonNullWeb3jService.subscribe( - Request( - // method - "eth_subscribe", - // params - listOf("newHeads"), - // web3jSocket - service, - // type - EthSubscribe::class.java - ), - "eth_unsubscribe", - NewHeadsNotificationExtended::class.java - ).asFlow() -} - -class NewHeadsNotificationExtended : - Notification() - -class NewHeadExtended { - var difficulty: String? = null - var extraData: String? = null - var gasLimit: String? = null - var gasUsed: String? = null - var hash: String? = null - var logsBloom: String? = null - var miner: String? = null - var nonce: String? = null - var number: String? = null - var parentHash: String? = null - var receiptRoot: String? = null - var sha3Uncles: String? = null - var stateRoot: String? = null - var timestamp: String? = null - var transactionRoot: String? = null - var baseFeePerGas: String? = null -} diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/NFTFlowFragment.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/NFTFlowFragment.kt index bd58d4c648..77a5e3c1fc 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/NFTFlowFragment.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/NFTFlowFragment.kt @@ -190,7 +190,9 @@ class NFTFlowFragment : BaseComposeBottomSheetDialogFragment() title = loadingState.data.first.retrieveString(), navigationIconResId = loadingState.data.second, onNavigationClick = remember { - { viewModel.onNavigationClick() } + { + viewModel.onNavigationClick() + } } ) diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/ChooseNFTRecipientPresenter.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/ChooseNFTRecipientPresenter.kt index e9f6707e41..f44c3f3970 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/ChooseNFTRecipientPresenter.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/ChooseNFTRecipientPresenter.kt @@ -23,6 +23,7 @@ import jp.co.soramitsu.nft.domain.NFTTransferInteractor import jp.co.soramitsu.nft.domain.models.NFT import jp.co.soramitsu.nft.impl.domain.usecase.transfer.ValidateNFTTransferUseCase import jp.co.soramitsu.nft.impl.navigation.InternalNFTRouter +import jp.co.soramitsu.nft.impl.navigation.NavAction import jp.co.soramitsu.nft.impl.presentation.CoroutinesStore import jp.co.soramitsu.nft.impl.presentation.chooserecipient.contract.ChooseNFTRecipientCallback import jp.co.soramitsu.nft.impl.presentation.chooserecipient.contract.ChooseNFTRecipientScreenState @@ -40,7 +41,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filterIsInstance @@ -70,8 +70,9 @@ class ChooseNFTRecipientPresenter @Inject constructor( private val currentAccountAddressUseCase: CurrentAccountAddressUseCase, private val internalNFTRouter: InternalNFTRouter ) : ChooseNFTRecipientCallback { + private val navGraphRoutesFlow = internalNFTRouter.createNavGraphRoutesFlow().shareIn(coroutinesStore.ioScope, SharingStarted.Eagerly, 1) - private val tokenFlow = internalNFTRouter.createNavGraphRoutesFlow() + private val tokenFlow = navGraphRoutesFlow .filterIsInstance() .map { destinationArgs -> destinationArgs.token } .shareIn(coroutinesStore.uiScope, SharingStarted.Eagerly, 1) @@ -85,6 +86,21 @@ class ChooseNFTRecipientPresenter @Inject constructor( private val currentToken: NFT? get() = tokenFlow.replayCache.lastOrNull() + init { + subscribeClearInputOnBackClick() + } + + private fun subscribeClearInputOnBackClick() { + internalNFTRouter.createNavGraphActionsFlow() + .onEach { + val currentRoute = navGraphRoutesFlow.replayCache.lastOrNull() + if (it is NavAction.BackPressed && currentRoute is NFTNavGraphRoute.ChooseNFTRecipientScreen) { + clearInputAddress() + } + } + .launchIn(coroutinesStore.uiScope) + } + fun handleQRCodeResult(qrCodeContent: String) { val result = walletInteractor.tryReadAddressFromSoraFormat(qrCodeContent) ?: qrCodeContent @@ -145,7 +161,7 @@ class ChooseNFTRecipientPresenter @Inject constructor( input = addressInput, image = addressIcon, editable = false, - showClear = false + showClear = true ), buttonState = ButtonViewState( text = resourceManager.getString(R.string.common_preview), @@ -237,8 +253,12 @@ class ChooseNFTRecipientPresenter @Inject constructor( } override fun onAddressInputClear() { - selectedWalletIdFlow.value = null + clearInputAddress() + } + + private fun clearInputAddress() { addressInputFlow.value = "" + selectedWalletIdFlow.value = null } override fun onNextClick() { diff --git a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/contract/ChooseNFTRecipientContract.kt b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/contract/ChooseNFTRecipientContract.kt index 560e39f457..5ec637af0e 100644 --- a/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/contract/ChooseNFTRecipientContract.kt +++ b/feature-nft-impl/src/main/java/jp/co/soramitsu/nft/impl/presentation/chooserecipient/contract/ChooseNFTRecipientContract.kt @@ -23,7 +23,7 @@ data class ChooseNFTRecipientScreenState( "", R.drawable.ic_address_placeholder, editable = false, - showClear = false + showClear = true ), buttonState = ButtonViewState("", false), feeInfoState = FeeInfoViewState.default, diff --git a/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/data/OnboardingFlow.kt b/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/data/OnboardingFlow.kt index 5eed58ffd0..5137e4f05a 100644 --- a/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/data/OnboardingFlow.kt +++ b/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/data/OnboardingFlow.kt @@ -1,17 +1,30 @@ package jp.co.soramitsu.onboarding.api.data data class OnboardingConfig( - val en_EN: Variants + val configs: List ) { - class Variants( - val new: List, - val regular: List + data class OnboardingConfigItem( + val minVersion: String, + val background: String, + val enEn: Variants ) { - class ScreenInfo( - val title: String, - val description: String, - val image: String + class Variants( + val new: List, + val regular: List ) { + class ScreenInfo( + val title: TitleInfo, + val description: String, + val image: String + ) { + class TitleInfo( + val text: String, + val color: String + ) { + companion object; + } + companion object; + } companion object; } companion object; diff --git a/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/domain/OnboardingInteractor.kt b/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/domain/OnboardingInteractor.kt index 6d623242f6..e7f3b39514 100644 --- a/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/domain/OnboardingInteractor.kt +++ b/feature-onboarding-api/src/main/java/jp/co/soramitsu/onboarding/api/domain/OnboardingInteractor.kt @@ -5,5 +5,9 @@ import jp.co.soramitsu.onboarding.api.data.OnboardingConfig interface OnboardingInteractor { suspend fun getConfig(): Result + suspend fun getAppVersionSupportedConfig(): Result + fun getWelcomeSlidesShownVersion(): String? + fun saveWelcomeSlidesShownVersion(version: String) + fun shouldShowWelcomeSlides(version: String): Boolean } diff --git a/feature-onboarding-impl/build.gradle b/feature-onboarding-impl/build.gradle index 2cf5813719..b4d6ce7984 100644 --- a/feature-onboarding-impl/build.gradle +++ b/feature-onboarding-impl/build.gradle @@ -18,11 +18,11 @@ android { buildTypes { debug { - buildConfigField "String", "ONBOARDING_CONFIG", "\"https://raw.githubusercontent.com/soramitsu/shared-features-utils/develop-free/appConfigs/onboarding/mobile.json\"" + buildConfigField "String", "ONBOARDING_CONFIG", "\"https://raw.githubusercontent.com/soramitsu/shared-features-utils/develop-free/appConfigs/onboarding/mobile%20v2.json\"" } release { - buildConfigField "String", "ONBOARDING_CONFIG", "\"https://raw.githubusercontent.com/soramitsu/shared-features-utils/develop-free/appConfigs/onboarding/mobile.json\"" + buildConfigField "String", "ONBOARDING_CONFIG", "\"https://raw.githubusercontent.com/soramitsu/shared-features-utils/develop-free/appConfigs/onboarding/mobile%20v2.json\"" } } diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/OnboardingRouter.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/OnboardingRouter.kt index e07647a1b6..dad71a4e06 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/OnboardingRouter.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/OnboardingRouter.kt @@ -35,4 +35,6 @@ interface OnboardingRouter { fun openSelectImportModeForResult(): Flow fun openCreatePincode() + + fun openInitialCheckPincode() } diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/data/DeserializationExt.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/data/DeserializationExt.kt index dceabf0002..9cd8dcda79 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/data/DeserializationExt.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/data/DeserializationExt.kt @@ -8,37 +8,64 @@ val OnboardingConfig.Companion.deserializer: JsonDeserializer val jsonObj = json.asJsonObject return@JsonDeserializer OnboardingConfig( - en_EN = context.deserialize( - jsonObj.get("en-EN"), OnboardingConfig.Variants::class.java + configs = jsonObj.get("Android")?.asJsonArray?.mapNotNull { jsonElem -> + context?.deserialize( + jsonElem, OnboardingConfig.OnboardingConfigItem::class.java + ) + } ?: emptyList(), + ) + } + +val OnboardingConfig.OnboardingConfigItem.Companion.deserializer: JsonDeserializer + get() = JsonDeserializer { json, typeOfT, context -> + val jsonObj = json.asJsonObject + + return@JsonDeserializer OnboardingConfig.OnboardingConfigItem( + minVersion = jsonObj.get("minVersion").asString, + background = jsonObj.get("background").asString, + enEn = context.deserialize( + jsonObj.get("en-EN"), OnboardingConfig.OnboardingConfigItem.Variants::class.java ) ) } -val OnboardingConfig.Variants.Companion.deserializer: JsonDeserializer +val OnboardingConfig.OnboardingConfigItem.Variants.Companion.deserializer: JsonDeserializer get() = JsonDeserializer { json, typeOfT, context -> val jsonObj = json.asJsonObject - return@JsonDeserializer OnboardingConfig.Variants( + return@JsonDeserializer OnboardingConfig.OnboardingConfigItem.Variants( new = jsonObj.get("new")?.asJsonArray?.mapNotNull { jsonElem -> - context?.deserialize( - jsonElem, OnboardingConfig.Variants.ScreenInfo::class.java + context?.deserialize( + jsonElem, OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo::class.java ) } ?: emptyList(), regular = jsonObj.get("regular")?.asJsonArray?.mapNotNull { jsonElem -> - context?.deserialize( - jsonElem, OnboardingConfig.Variants.ScreenInfo::class.java + context?.deserialize( + jsonElem, OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo::class.java ) } ?: emptyList() ) } -val OnboardingConfig.Variants.ScreenInfo.Companion.deserializer: JsonDeserializer +val OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.Companion.deserializer: JsonDeserializer get() = JsonDeserializer { json, typeOfT, context -> val jsonObj = json.asJsonObject - return@JsonDeserializer OnboardingConfig.Variants.ScreenInfo( - title = jsonObj.get("title").asString, + return@JsonDeserializer OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo( + title = context.deserialize( + jsonObj.get("title"), OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo::class.java + ), description = jsonObj.get("description").asString, image = jsonObj.get("image").asString ) + } + +val OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo.Companion.deserializer: JsonDeserializer + get() = JsonDeserializer { json, typeOfT, context -> + val jsonObj = json.asJsonObject + + return@JsonDeserializer OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo( + text = jsonObj.get("text").asString, + color = jsonObj.get("color").asString + ) } \ No newline at end of file diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/di/OnboardingFeatureModule.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/di/OnboardingFeatureModule.kt index 5e9ecc04d2..b57c0faf55 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/di/OnboardingFeatureModule.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/di/OnboardingFeatureModule.kt @@ -29,8 +29,10 @@ class OnboardingFeatureModule { OnboardingConfigApi::class.java, typeAdapters = mapOf( OnboardingConfig::class.java to OnboardingConfig.deserializer, - OnboardingConfig.Variants::class.java to OnboardingConfig.Variants.deserializer, - OnboardingConfig.Variants.ScreenInfo::class.java to OnboardingConfig.Variants.ScreenInfo.deserializer, + OnboardingConfig.OnboardingConfigItem::class.java to OnboardingConfig.OnboardingConfigItem.deserializer, + OnboardingConfig.OnboardingConfigItem.Variants::class.java to OnboardingConfig.OnboardingConfigItem.Variants.deserializer, + OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo::class.java to OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.deserializer, + OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo::class.java to OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo.deserializer, ) ) @@ -45,10 +47,12 @@ class OnboardingFeatureModule { @Provides fun provideOnboardingInteractor( - onboardingRepository: OnboardingRepository + onboardingRepository: OnboardingRepository, + preferences: Preferences ): OnboardingInteractor { return OnboardingInteractorImpl( - onboardingRepository + onboardingRepository, + preferences ) } diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/domain/OnboardingInteractorImpl.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/domain/OnboardingInteractorImpl.kt index 6926af1b64..9665a2d72f 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/domain/OnboardingInteractorImpl.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/domain/OnboardingInteractorImpl.kt @@ -1,15 +1,46 @@ package jp.co.soramitsu.onboarding.impl.domain +import jp.co.soramitsu.common.data.storage.Preferences +import jp.co.soramitsu.common.domain.AppVersion import jp.co.soramitsu.onboarding.api.data.OnboardingConfig import jp.co.soramitsu.onboarding.api.data.OnboardingRepository import jp.co.soramitsu.onboarding.api.domain.OnboardingInteractor class OnboardingInteractorImpl( - private val onboardingRepository: OnboardingRepository + private val onboardingRepository: OnboardingRepository, + private val preferences: Preferences ): OnboardingInteractor { + companion object { + private const val PREFS_SHOWN_WELCOME_VERSION = "prefs_shown_welcome_version" + } + override suspend fun getConfig(): Result { return onboardingRepository.getConfig() } + override suspend fun getAppVersionSupportedConfig(): Result { + val appVersion = AppVersion.current() + + return onboardingRepository.getConfig().map { + it.configs.filter { + val configVersion = AppVersion.fromString(it.minVersion) + configVersion.major == appVersion.major && configVersion.minor == appVersion.minor + }.maxByOrNull { + AppVersion.fromString(it.minVersion) + } + } + } + + override fun getWelcomeSlidesShownVersion(): String? { + return preferences.getString(PREFS_SHOWN_WELCOME_VERSION) + } + + override fun saveWelcomeSlidesShownVersion(version: String) { + preferences.putString(PREFS_SHOWN_WELCOME_VERSION, version) + } + + override fun shouldShowWelcomeSlides(version: String): Boolean { + return version != getWelcomeSlidesShownVersion() + } } diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingScreen.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingScreen.kt index 6b13496018..cfafc00f7c 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingScreen.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingScreen.kt @@ -45,6 +45,7 @@ import jp.co.soramitsu.common.compose.component.MenuIconItem import jp.co.soramitsu.common.compose.component.Toolbar import jp.co.soramitsu.common.compose.component.ToolbarViewState import jp.co.soramitsu.common.compose.theme.FearlessAppTheme +import jp.co.soramitsu.common.compose.theme.colorFromHex import jp.co.soramitsu.common.compose.theme.white import jp.co.soramitsu.common.compose.theme.white20 import jp.co.soramitsu.common.compose.theme.white60 @@ -57,29 +58,29 @@ import kotlinx.coroutines.launch @Immutable @JvmInline value class OnboardingFlow( - private val flow: List -): List by flow + private val flow: List +): List by flow @Stable interface OnboardingScreenCallback { fun onClose() - fun onNext() - fun onSkip() - } @Suppress("FunctionName") fun NavGraphBuilder.OnboardingScreen( + backgroundImageFlow: StateFlow, onboardingStateFlow: StateFlow, callback: OnboardingScreenCallback ) { composable(WelcomeEvent.Onboarding.PagerScreen.route) { val onboardingFlow by onboardingStateFlow.collectAsState() + val backgroundImageUrl by backgroundImageFlow.collectAsState() OnboardingScreenContent( + background = backgroundImageUrl, onboardingFlow = onboardingFlow, callback = callback ) @@ -89,6 +90,7 @@ fun NavGraphBuilder.OnboardingScreen( @OptIn(ExperimentalFoundationApi::class) @Composable private fun OnboardingScreenContent( + background: String?, onboardingFlow: OnboardingFlow?, callback: OnboardingScreenCallback ) { @@ -118,11 +120,17 @@ private fun OnboardingScreenContent( val coroutineScope = rememberCoroutineScope() + val backgroundPainter = if (background.isNullOrBlank()) { + painterResource(R.drawable.drawable_background_image) + } else { + rememberAsyncImagePainter(model = background) + } + Column( modifier = Modifier .fillMaxSize() .paint( - painter = painterResource(R.drawable.drawable_background_image), + painter = backgroundPainter, contentScale = ContentScale.FillWidth ), verticalArrangement = Arrangement.spacedBy(16.dp), @@ -155,8 +163,8 @@ private fun OnboardingScreenContent( horizontalAlignment = Alignment.CenterHorizontally ) { H1( - text = onboardingFlow[it].title, - color = white, + text = onboardingFlow[it].title.text, + color = runCatching { onboardingFlow[it].title.color.colorFromHex() }.getOrNull() ?: white, textAlign = TextAlign.Center ) @@ -205,7 +213,7 @@ private fun OnboardingScreenContent( val page = currentPageAsState.value if (page == onboardingFlow.lastIndex) - callback.onNext() + callback.onClose() else coroutineScope.launch { pagerState.animateScrollToPage(page + 1) } @@ -232,30 +240,28 @@ private fun OnboardingScreenContent( private fun OnboardingScreenPreview() { FearlessAppTheme { OnboardingScreenContent( - OnboardingFlow( + background = "", + onboardingFlow = OnboardingFlow( listOf( - OnboardingConfig.Variants.ScreenInfo( - title = "Brand new network management", + OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo( + title = OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo("Brand new network management", "#ee0077"), description = "Navigate between All, Popular and your Favourite networks modes", image = "${R.drawable.drawable_background_image}" ), - OnboardingConfig.Variants.ScreenInfo( - title = "Title1", + OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo( + title = OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo("Title1", ""), description = "Description1", image = "image1" ), - OnboardingConfig.Variants.ScreenInfo( - title = "Title2", + OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo( + title = OnboardingConfig.OnboardingConfigItem.Variants.ScreenInfo.TitleInfo("Title2", ""), description = "Description2", image = "image2" ) ) ), - object : OnboardingScreenCallback { + callback = object : OnboardingScreenCallback { override fun onClose() = Unit - - override fun onNext() = Unit - override fun onSkip() = Unit } ) diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingSplashScreen.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingSplashScreen.kt index 2dbaa1e6cd..213aabfeab 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingSplashScreen.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/OnboardingSplashScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint @@ -25,6 +26,7 @@ import jp.co.soramitsu.common.compose.component.AccentButton import jp.co.soramitsu.common.compose.component.MarginVertical import jp.co.soramitsu.common.compose.theme.FearlessAppTheme import jp.co.soramitsu.feature_onboarding_impl.R +import kotlinx.coroutines.flow.StateFlow @Stable fun interface OnboardingSplashScreenClickListener { @@ -33,15 +35,17 @@ fun interface OnboardingSplashScreenClickListener { @Suppress("FunctionName") fun NavGraphBuilder.OnboardingSplashScreen( + isAccountSelectedFlow: StateFlow, listener: OnboardingSplashScreenClickListener ) { composable(WelcomeEvent.Onboarding.SplashScreen.route) { - OnboardingSplashScreenContent(listener) + OnboardingSplashScreenContent(isAccountSelectedFlow.collectAsState().value, listener) } } @Composable private fun OnboardingSplashScreenContent( + isAccountSelected: Boolean, listener: OnboardingSplashScreenClickListener ) { Column( @@ -56,7 +60,9 @@ private fun OnboardingSplashScreenContent( ) { Column( - modifier = Modifier.weight(1f).width(IntrinsicSize.Max), + modifier = Modifier + .weight(1f) + .width(IntrinsicSize.Max), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -75,14 +81,16 @@ private fun OnboardingSplashScreenContent( } - AccentButton( - text = stringResource(id = R.string.common_start), - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 16.dp), - onClick = listener::onStart - ) + if (isAccountSelected.not()) { + AccentButton( + text = stringResource(id = R.string.common_start), + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 16.dp), + onClick = listener::onStart + ) + } MarginVertical(margin = 16.dp) } @@ -92,6 +100,9 @@ private fun OnboardingSplashScreenContent( @Preview private fun OnboardingSplashScreenPreview() { FearlessAppTheme { - OnboardingSplashScreenContent { Unit } + OnboardingSplashScreenContent( + isAccountSelected = true, + listener = {} + ) } } \ No newline at end of file diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeFragment.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeFragment.kt index 9f819e577d..6ec443e5b9 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeFragment.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeFragment.kt @@ -74,8 +74,6 @@ class WelcomeFragment : BaseComposeFragment() { super.onViewCreated(view, savedInstanceState) observeBrowserEvents(viewModel) - - } private fun handleAuthorizeGoogleEvent() { @@ -118,10 +116,12 @@ class WelcomeFragment : BaseComposeFragment() { ) { OnboardingSplashScreen( + isAccountSelectedFlow = viewModel.isAccountSelectedFlow, listener = viewModel ) OnboardingScreen( + backgroundImageFlow = viewModel.onboardingBackground, onboardingStateFlow = viewModel.onboardingFlowState, callback = viewModel ) diff --git a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeViewModel.kt b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeViewModel.kt index 8d578d6a1e..5ff8d4c6eb 100644 --- a/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeViewModel.kt +++ b/feature-onboarding-impl/src/main/java/jp/co/soramitsu/onboarding/impl/welcome/WelcomeViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import jp.co.soramitsu.account.api.domain.PendulumPreInstalledAccountsScenario +import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.account.api.domain.model.ImportMode import jp.co.soramitsu.backup.BackupService import jp.co.soramitsu.common.base.BaseViewModel @@ -22,6 +23,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -38,12 +40,19 @@ class WelcomeViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val backupService: BackupService, private val pendulumPreInstalledAccountsScenario: PendulumPreInstalledAccountsScenario, - private val onboardingInteractor: OnboardingInteractor + private val onboardingInteractor: OnboardingInteractor, + private val accountRepository: AccountRepository ) : BaseViewModel(), Browserable, WelcomeScreenInterface, OnboardingScreenCallback, OnboardingSplashScreenClickListener { private val payload = savedStateHandle.get(KEY_PAYLOAD)!! + private val _isAccountSelectedFlow = MutableStateFlow(true) + val isAccountSelectedFlow: StateFlow = _isAccountSelectedFlow + private val _onboardingBackgroundState = MutableStateFlow(null) + val onboardingBackground = _onboardingBackgroundState + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + private val _onboardingFlowState = MutableStateFlow?>(null) val onboardingFlowState = _onboardingFlowState.map { it?.getOrNull() } .stateIn(viewModelScope, SharingStarted.Eagerly, null) @@ -62,6 +71,7 @@ class WelcomeViewModel @Inject constructor( val events = _events.receiveAsFlow() override val openBrowserEvent = MutableLiveData>() + private var currentOnboardingConfigVersion: String? = null init { payload.createChainAccount?.run { @@ -72,14 +82,37 @@ class WelcomeViewModel @Inject constructor( } viewModelScope.launch { - onboardingInteractor.getConfig() + val isAccountSelected = accountRepository.isAccountSelected() + _isAccountSelectedFlow.value = isAccountSelected + + val useConfig = onboardingInteractor.getAppVersionSupportedConfig() .onFailure { Log.e("OnboardingScreen", "onboardingInteractor.getConfig() failed: $it") showError(it) + }.getOrNull() + + val shouldShowSlides = useConfig != null + && (onboardingInteractor.shouldShowWelcomeSlides(useConfig.minVersion) || isAccountSelected.not()) + + currentOnboardingConfigVersion = useConfig?.minVersion + + when { + isAccountSelected && shouldShowSlides -> { + _onboardingFlowState.value = Result.success(OnboardingFlow(useConfig!!.enEn.regular)) + _onboardingBackgroundState.value = useConfig.background + _events.trySend(WelcomeEvent.Onboarding.PagerScreen) + } + isAccountSelected -> { + moveNextToPincode() } - .map { OnboardingFlow(it.en_EN.new) }.let { - _onboardingFlowState.value = it + shouldShowSlides -> { + _onboardingFlowState.value = Result.success(OnboardingFlow(useConfig!!.enEn.new)) + _onboardingBackgroundState.value = useConfig.background } + else -> { + _onboardingFlowState.value = Result.failure(IllegalStateException("Onboarding config is empty")) + } + } } } @@ -172,14 +205,28 @@ class WelcomeViewModel @Inject constructor( } override fun onClose() { - _events.trySend(WelcomeEvent.Onboarding.WelcomeScreen) + viewModelScope.launch { + if (accountRepository.isAccountSelected()) { + moveNextToPincode() + } else { + _events.trySend(WelcomeEvent.Onboarding.WelcomeScreen) + } + } + currentOnboardingConfigVersion?.let { + onboardingInteractor.saveWelcomeSlidesShownVersion(it) + currentOnboardingConfigVersion = null + } } - override fun onNext() { - _events.trySend(WelcomeEvent.Onboarding.WelcomeScreen) + override fun onSkip() { + onClose() } - override fun onSkip() { - _events.trySend(WelcomeEvent.Onboarding.WelcomeScreen) + private suspend fun moveNextToPincode() { + if (accountRepository.isCodeSet()) { + router.openInitialCheckPincode() + } else { + router.openCreatePincode() + } } } diff --git a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/data/PolkaswapRepositoryImpl.kt b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/data/PolkaswapRepositoryImpl.kt index f5b4e2617b..48c9e70271 100644 --- a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/data/PolkaswapRepositoryImpl.kt +++ b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/data/PolkaswapRepositoryImpl.kt @@ -11,9 +11,6 @@ import jp.co.soramitsu.common.utils.poolXYK import jp.co.soramitsu.common.utils.u32ArgumentFromStorageKey import jp.co.soramitsu.core.extrinsic.ExtrinsicService import jp.co.soramitsu.core.rpc.RpcCalls -import jp.co.soramitsu.core.rpc.calls.liquidityProxyIsPathAvailable -import jp.co.soramitsu.core.rpc.calls.liquidityProxyListEnabledSourcesForPath -import jp.co.soramitsu.core.rpc.calls.liquidityProxyQuote import jp.co.soramitsu.core.runtime.models.responses.QuoteResponse import jp.co.soramitsu.polkaswap.api.data.PolkaswapRepository import jp.co.soramitsu.polkaswap.api.models.Market @@ -32,6 +29,11 @@ import jp.co.soramitsu.shared_utils.runtime.definitions.types.composite.Struct import jp.co.soramitsu.shared_utils.runtime.metadata.storage import jp.co.soramitsu.shared_utils.runtime.metadata.storageKey import jp.co.soramitsu.shared_utils.wsrpc.exception.RpcException +import jp.co.soramitsu.shared_utils.wsrpc.executeAsync +import jp.co.soramitsu.shared_utils.wsrpc.mappers.nonNull +import jp.co.soramitsu.shared_utils.wsrpc.mappers.pojo +import jp.co.soramitsu.shared_utils.wsrpc.mappers.pojoList +import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.RuntimeRequest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -109,7 +111,16 @@ class PolkaswapRepositoryImpl @Inject constructor( tokenToId: String, dexId: Int ): Boolean { - return rpcCalls.liquidityProxyIsPathAvailable(chainId, tokenFromId, tokenToId, dexId) + val request = RuntimeRequest( + method = "liquidityProxy_isPathAvailable", + params = listOf( + dexId, + tokenFromId, + tokenToId + ) + ) + + return chainRegistry.awaitConnection(chainId).socketService.executeAsync(request, mapper = pojo().nonNull()) } override suspend fun getSwapQuote( @@ -122,16 +133,20 @@ class PolkaswapRepositoryImpl @Inject constructor( dexId: Int ): QuoteResponse? { return try { - rpcCalls.liquidityProxyQuote( - chainId, - tokenFromId, - tokenToId, - amount, - desired.backString, - curMarkets.backStrings(), - curMarkets.toFilters(), - dexId + val request = RuntimeRequest( + method = "liquidityProxy_quote", + params = listOf( + dexId, + tokenFromId, + tokenToId, + amount.toString(), + desired.backString, + curMarkets.backStrings(), + curMarkets.toFilters() + ) ) + + chainRegistry.awaitConnection(chainId).socketService.executeAsync(request, mapper = pojo()).result } catch (e: Exception) { null } @@ -180,7 +195,11 @@ class PolkaswapRepositoryImpl @Inject constructor( private suspend fun getEnabledMarkets(chainId: ChainId, dexId: Int, tokenId1: String, tokenId2: String): List { return try { - rpcCalls.liquidityProxyListEnabledSourcesForPath(chainId, dexId, tokenId1, tokenId2).toMarkets() + val request = RuntimeRequest( + "liquidityProxy_listEnabledSourcesForPath", + listOf(dexId, tokenId1, tokenId2) + ) + return chainRegistry.awaitConnection(chainId).socketService.executeAsync(request, mapper = pojoList().nonNull()).toMarkets() } catch (e: RpcException) { listOf() } diff --git a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/di/PolkaswapFeatureBindModule.kt b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/di/PolkaswapFeatureBindModule.kt index e22816d132..af6ad86882 100644 --- a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/di/PolkaswapFeatureBindModule.kt +++ b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/di/PolkaswapFeatureBindModule.kt @@ -9,6 +9,7 @@ import javax.inject.Named import javax.inject.Singleton import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.common.data.network.config.RemoteConfigFetcher +import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.core.extrinsic.ExtrinsicService import jp.co.soramitsu.polkaswap.api.data.PolkaswapRepository import jp.co.soramitsu.polkaswap.api.domain.PolkaswapInteractor @@ -17,22 +18,12 @@ import jp.co.soramitsu.polkaswap.impl.domain.PolkaswapInteractorImpl import jp.co.soramitsu.runtime.di.REMOTE_STORAGE_SOURCE import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.core.rpc.RpcCalls +import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository import jp.co.soramitsu.runtime.storage.source.StorageDataSource +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository @InstallIn(SingletonComponent::class) @Module -interface PolkaswapFeatureBindModule { - - @Binds - @Singleton - fun bindsPolkaswapInteractor(polkaswapInteractor: PolkaswapInteractorImpl): PolkaswapInteractor - - @Binds - fun bindsPolkaswapRepository(polkaswapRepository: PolkaswapRepositoryImpl): PolkaswapRepository -} - -@InstallIn(SingletonComponent::class) -@Module(includes = [PolkaswapFeatureBindModule::class]) class PolkaswapFeatureModule { @Provides @@ -43,7 +34,27 @@ class PolkaswapFeatureModule { chainRegistry: ChainRegistry, rpcCalls: RpcCalls, accountRepository: AccountRepository - ): PolkaswapRepositoryImpl { + ): PolkaswapRepository { return PolkaswapRepositoryImpl(remoteConfigFetcher, remoteSource, extrinsicService, chainRegistry, rpcCalls, accountRepository) } + + @Provides + @Singleton + fun providePolkaswapInteractor( + chainRegistry: ChainRegistry, + walletRepository: WalletRepository, + accountRepository: AccountRepository, + polkaswapRepository: PolkaswapRepository, + sharedPreferences: Preferences, + chainsRepository: ChainsRepository, + ): PolkaswapInteractor { + return PolkaswapInteractorImpl( + chainRegistry, + walletRepository, + accountRepository, + polkaswapRepository, + sharedPreferences, + chainsRepository + ) + } } diff --git a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/domain/PolkaswapInteractorImpl.kt b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/domain/PolkaswapInteractorImpl.kt index d9b5834d63..6cf904310e 100644 --- a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/domain/PolkaswapInteractorImpl.kt +++ b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/domain/PolkaswapInteractorImpl.kt @@ -1,5 +1,6 @@ package jp.co.soramitsu.polkaswap.impl.domain +import android.util.Log import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -9,7 +10,6 @@ import jp.co.soramitsu.account.api.domain.model.accountId import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.common.presentation.LoadingState import jp.co.soramitsu.common.utils.isZero -import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.core.runtime.models.responses.QuoteResponse import jp.co.soramitsu.core.utils.utilityAsset import jp.co.soramitsu.polkaswap.api.data.PolkaswapRepository @@ -30,14 +30,19 @@ import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository import jp.co.soramitsu.wallet.impl.domain.model.Asset import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks import jp.co.soramitsu.wallet.impl.domain.model.planksFromAmount +import kotlin.coroutines.CoroutineContext import kotlin.math.max +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.withContext class PolkaswapInteractorImpl @Inject constructor( private val chainRegistry: ChainRegistry, @@ -45,7 +50,8 @@ class PolkaswapInteractorImpl @Inject constructor( private val accountRepository: AccountRepository, private val polkaswapRepository: PolkaswapRepository, private val sharedPreferences: Preferences, - private val chainsRepository: ChainsRepository + private val chainsRepository: ChainsRepository, + private val coroutineContext: CoroutineContext = Dispatchers.Default + CoroutineExceptionHandler { _, throwable -> Log.e("PolkaswapInteractor", "$throwable ${throwable.message}") } ) : PolkaswapInteractor { override var polkaswapChainId = soraMainChainId @@ -65,16 +71,16 @@ class PolkaswapInteractorImpl @Inject constructor( chainId?.takeIf { it in listOf(soraMainChainId, soraTestChainId) }?.let { polkaswapChainId = chainId } } - override suspend fun getFeeAsset(): Asset? { + override suspend fun getFeeAsset(): Asset? = withContext(coroutineContext) { val chain = chainsRepository.getChain(polkaswapChainId) - return chain.utilityAsset?.id?.let { getAsset(it) } + chain.utilityAsset?.id?.let { getAsset(it) } } - override suspend fun getAsset(assetId: String): Asset? { + override suspend fun getAsset(assetId: String): Asset? = withContext(coroutineContext) { val metaAccount = accountRepository.getSelectedMetaAccount() val (chain, chainAsset) = chainsRepository.chainWithAsset(polkaswapChainId, assetId) - return walletRepository.getAsset(metaAccount.id, metaAccount.accountId(chain)!!, chainAsset, chain.minSupportedVersion) + walletRepository.getAsset(metaAccount.id, metaAccount.accountId(chain)!!, chainAsset, chain.minSupportedVersion) } @OptIn(ExperimentalCoroutinesApi::class) @@ -90,11 +96,11 @@ class PolkaswapInteractorImpl @Inject constructor( chainAsset, chain.minSupportedVersion ) - } + }.flowOn(Dispatchers.Default) } - override suspend fun getAvailableDexes(): List { - return polkaswapRepository.getAvailableDexes(polkaswapChainId) + override suspend fun getAvailableDexes(): List = withContext(coroutineContext) { + polkaswapRepository.getAvailableDexes(polkaswapChainId) } @OptIn(FlowPreview::class) @@ -113,7 +119,7 @@ class PolkaswapInteractorImpl @Inject constructor( flows.add(polkaswapRepository.observePoolTBCReserves(polkaswapChainId, fromTokenId)) flows.add(polkaswapRepository.observePoolTBCReserves(polkaswapChainId, toTokenId)) } - return flows.merge().debounce(500) + return flows.merge().debounce(500).flowOn(Dispatchers.Default) } override suspend fun calcDetails( @@ -124,7 +130,7 @@ class PolkaswapInteractorImpl @Inject constructor( desired: WithDesired, slippageTolerance: Double, market: Market - ): Result { + ): Result = withContext(coroutineContext) { val polkaswapUtilityAssetId = chainsRepository.getChain(polkaswapChainId).utilityAsset?.id val feeAsset = requireNotNull(polkaswapUtilityAssetId?.let { getAsset(it) }) @@ -136,7 +142,7 @@ class PolkaswapInteractorImpl @Inject constructor( val tokenToId = requireNotNull(tokenTo.token.configuration.currencyId) val previousBestDex = bestDexIdFlow.value - if (market !in availableMarkets.values.flatten().toSet()) return Result.success(null) + if (market !in availableMarkets.values.flatten().toSet()) return@withContext Result.success(null) bestDexIdFlow.emit(LoadingState.Loading()) val (bestDex, swapQuote) = getBestSwapQuote( dexes = availableDexPaths, @@ -147,11 +153,11 @@ class PolkaswapInteractorImpl @Inject constructor( curMarkets = curMarkets )?.let { it.first to it.second.toModel(feeAsset.token.configuration) } ?: run { bestDexIdFlow.emit(previousBestDex) - return Result.failure(InsufficientLiquidityException()) + return@withContext Result.failure(InsufficientLiquidityException()) } bestDexIdFlow.emit(LoadingState.Loaded(bestDex)) - if (swapQuote.amount.isZero()) return Result.success(null) + if (swapQuote.amount.isZero()) return@withContext Result.success(null) val minMax = (swapQuote.amount * BigDecimal.valueOf(slippageTolerance / 100)).let { @@ -164,7 +170,7 @@ class PolkaswapInteractorImpl @Inject constructor( val scale = max(swapQuote.amount.scale(), amount.scale()) - if (swapQuote.amount.isZero()) return Result.success(null) + if (swapQuote.amount.isZero()) return@withContext Result.success(null) val per1 = amount.divide(swapQuote.amount, scale, RoundingMode.HALF_EVEN) val per2 = swapQuote.amount.divide(amount, scale, RoundingMode.HALF_EVEN) @@ -187,7 +193,7 @@ class PolkaswapInteractorImpl @Inject constructor( bestDexId = bestDex, route = route ) - return Result.success(details) + return@withContext Result.success(details) } private suspend fun getBestSwapQuote( @@ -197,7 +203,7 @@ class PolkaswapInteractorImpl @Inject constructor( amount: BigInteger, desired: WithDesired, curMarkets: List - ): Pair? { + ): Pair? = withContext(coroutineContext) { val quotes = dexes.mapNotNull { dexId -> val quote = polkaswapRepository.getSwapQuote( chainId = polkaswapChainId, @@ -211,7 +217,7 @@ class PolkaswapInteractorImpl @Inject constructor( dexId to quote } - return if (quotes.isEmpty()) { + return@withContext if (quotes.isEmpty()) { null } else { when (desired) { @@ -228,9 +234,9 @@ class PolkaswapInteractorImpl @Inject constructor( amountInPlanks: BigInteger, market: Market, desired: WithDesired - ): BigInteger { + ): BigInteger = withContext(coroutineContext) { val curMarkets = if (market == Market.SMART) emptyList() else listOf(market) - return polkaswapRepository.estimateSwapFee( + return@withContext polkaswapRepository.estimateSwapFee( polkaswapChainId, bestDex, tokenFromId, @@ -243,7 +249,7 @@ class PolkaswapInteractorImpl @Inject constructor( ) } - override suspend fun fetchAvailableSources(tokenInput: Asset, tokenOutput: Asset, availableDexes: List): Set { + override suspend fun fetchAvailableSources(tokenInput: Asset, tokenOutput: Asset, availableDexes: List): Set = withContext(coroutineContext) { val tokenFromId = requireNotNull(tokenInput.token.configuration.currencyId) val tokenToId = requireNotNull(tokenOutput.token.configuration.currencyId) @@ -252,11 +258,11 @@ class PolkaswapInteractorImpl @Inject constructor( availableMarkets.clear() availableMarkets.putAll(sources) - return sources.values.flatten().toSet() + return@withContext sources.values.flatten().toSet() } - override suspend fun getAvailableDexesForPair(tokenFromId: String, tokenToId: String, dexes: List): List { - return dexes.map { + override suspend fun getAvailableDexesForPair(tokenFromId: String, tokenToId: String, dexes: List): List = withContext(coroutineContext) { + return@withContext dexes.map { val isAvailable = polkaswapRepository.isPairAvailable(polkaswapChainId, tokenFromId, tokenToId, it.toInt()) it.toInt() to isAvailable @@ -272,13 +278,13 @@ class PolkaswapInteractorImpl @Inject constructor( filter: String, markets: List, desired: WithDesired - ): Result { - return polkaswapRepository.swap(polkaswapChainId, dexId, inputAssetId, outputAssetId, amount, limit, filter, markets, desired) + ): Result = withContext(coroutineContext) { + polkaswapRepository.swap(polkaswapChainId, dexId, inputAssetId, outputAssetId, amount, limit, filter, markets, desired) } - override suspend fun calcFakeFee(): BigDecimal { - val feeAsset = getFeeAsset() ?: return BigDecimal.ZERO - val feeAssetId = feeAsset.token.configuration.currencyId ?: return BigDecimal.ZERO + override suspend fun calcFakeFee(): BigDecimal = withContext(coroutineContext) { + val feeAsset = getFeeAsset() ?: return@withContext BigDecimal.ZERO + val feeAssetId = feeAsset.token.configuration.currencyId ?: return@withContext BigDecimal.ZERO val markets = emptyList() val fee = polkaswapRepository.estimateSwapFee( @@ -292,6 +298,6 @@ class PolkaswapInteractorImpl @Inject constructor( markets.backStrings(), WithDesired.INPUT ) - return feeAsset.token.configuration.amountFromPlanks(fee) + return@withContext feeAsset.token.configuration.amountFromPlanks(fee) } } diff --git a/feature-splash/build.gradle b/feature-splash/build.gradle index a1ea3845bd..ddfbcaf7bc 100644 --- a/feature-splash/build.gradle +++ b/feature-splash/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation projects.common implementation projects.featureAccountApi + implementation projects.featureOnboardingApi implementation libs.kotlin.stdlib.jdk7 diff --git a/feature-splash/src/main/java/jp/co/soramitsu/splash/SplashRouter.kt b/feature-splash/src/main/java/jp/co/soramitsu/splash/SplashRouter.kt index d3a64fedbc..1af70e8095 100644 --- a/feature-splash/src/main/java/jp/co/soramitsu/splash/SplashRouter.kt +++ b/feature-splash/src/main/java/jp/co/soramitsu/splash/SplashRouter.kt @@ -1,18 +1,8 @@ package jp.co.soramitsu.splash import jp.co.soramitsu.common.navigation.SecureRouter -import jp.co.soramitsu.common.presentation.StoryGroupModel -import kotlinx.coroutines.flow.Flow interface SplashRouter : SecureRouter { - val educationalStoriesCompleted: Flow - - fun openAddFirstAccount() - - fun openCreatePincode() - - fun openInitialCheckPincode() - - fun openEducationalStories(stories: StoryGroupModel) + fun openOnboarding() } diff --git a/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashFragment.kt b/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashFragment.kt index 75f8ba95b7..edeada00be 100644 --- a/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashFragment.kt +++ b/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashFragment.kt @@ -26,7 +26,6 @@ class SplashFragment : BaseFragment() { } override fun subscribe(viewModel: SplashViewModel) { -// viewModel.checkStories() viewModel.openInitialDestination() } } diff --git a/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashViewModel.kt b/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashViewModel.kt index 9d588b7eba..8f5d581158 100644 --- a/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashViewModel.kt +++ b/feature-splash/src/main/java/jp/co/soramitsu/splash/presentation/SplashViewModel.kt @@ -4,71 +4,21 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import jp.co.soramitsu.account.api.domain.PendulumPreInstalledAccountsScenario -import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.domain.GetEducationalStoriesUseCase -import jp.co.soramitsu.common.domain.ShouldShowEducationalStoriesUseCase -import jp.co.soramitsu.common.domain.model.StoryGroup -import jp.co.soramitsu.common.presentation.StoryElement -import jp.co.soramitsu.common.presentation.StoryGroupModel import jp.co.soramitsu.splash.SplashRouter import kotlinx.coroutines.launch @HiltViewModel class SplashViewModel @Inject constructor( private val router: SplashRouter, - private val repository: AccountRepository, - shouldShowEducationalStoriesUseCase: ShouldShowEducationalStoriesUseCase, - private val getEducationalStories: GetEducationalStoriesUseCase, private val pendulumPreInstalledAccountsScenario: PendulumPreInstalledAccountsScenario ) : BaseViewModel() { - private var shouldShowEducationalStories by shouldShowEducationalStoriesUseCase - - fun checkStories() { - launch { - when { - repository.isAccountSelected() -> openInitialDestination() - shouldShowEducationalStories -> { - listenForStories() - val stories = getEducationalStories().transform() - router.openEducationalStories(stories) - shouldShowEducationalStories = false - } - else -> openInitialDestination() - } - } - } - - private fun StoryGroup.Onboarding.transform() = - StoryGroupModel( - this.elements.map { - StoryElement.Onboarding(it.titleRes, it.bodyRes, it.image, it.buttonCaptionRes) - } - ) - fun openInitialDestination() { viewModelScope.launch { pendulumPreInstalledAccountsScenario.fetchFeatureToggle() - if (repository.isAccountSelected()) { - if (repository.isCodeSet()) { - router.openInitialCheckPincode() - } else { - router.openCreatePincode() - } - } else { - router.openAddFirstAccount() - } - } - } - private fun listenForStories() { - viewModelScope.launch { - router.educationalStoriesCompleted.collect { - if (it) { - openInitialDestination() - } - } + router.openOnboarding() } } } diff --git a/feature-staking-api/src/main/java/jp/co/soramitsu/staking/api/data/StakingSharedState.kt b/feature-staking-api/src/main/java/jp/co/soramitsu/staking/api/data/StakingSharedState.kt index 0bc4fce1ee..f5075a7aba 100644 --- a/feature-staking-api/src/main/java/jp/co/soramitsu/staking/api/data/StakingSharedState.kt +++ b/feature-staking-api/src/main/java/jp/co/soramitsu/staking/api/data/StakingSharedState.kt @@ -11,13 +11,13 @@ import jp.co.soramitsu.runtime.multiNetwork.chain.model.reefChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraMainChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraTestChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.ternoaChainId -import jp.co.soramitsu.runtime.multiNetwork.chainWithAsset import jp.co.soramitsu.runtime.state.SingleAssetSharedState import jp.co.soramitsu.wallet.impl.domain.TokenUseCase import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository import jp.co.soramitsu.wallet.impl.domain.model.Asset import jp.co.soramitsu.wallet.impl.domain.model.Token import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -65,6 +65,7 @@ class StakingSharedState( private const val DELIMITER = ":" } + @OptIn(ExperimentalCoroutinesApi::class) val selectionItem: Flow = accountRepository.selectedMetaAccountFlow() .flatMapLatest { preferences.stringFlow( @@ -74,11 +75,12 @@ class StakingSharedState( encode(defaultAsset) } - ).distinctUntilChanged() + ) } .map { encoded -> encoded?.let { decode(it) } } + .distinctUntilChanged() .filterNotNull() .shareIn(scope, SharingStarted.Eagerly, replay = 1) @@ -97,6 +99,7 @@ class StakingSharedState( SingleAssetSharedState.AssetWithChain(chain, asset) } + @OptIn(ExperimentalCoroutinesApi::class) fun currentAssetFlow() = combine( assetWithChain, accountRepository.selectedMetaAccountFlow() diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/bindings/Identity.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/bindings/Identity.kt index 52f007f5bb..611202766e 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/bindings/Identity.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/bindings/Identity.kt @@ -81,7 +81,7 @@ fun bindSuperOf( @HelperBinding fun bindIdentityData(identityInfo: Struct.Instance, field: String): String? { - val value = identityInfo.get(field) ?: incompatible() + val value = identityInfo.get(field) ?: return null return bindData(value).asString() } diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/StakingLedgerUpdater.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/StakingLedgerUpdater.kt index 4bf2bc2ab5..6c61c2f2c9 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/StakingLedgerUpdater.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/StakingLedgerUpdater.kt @@ -13,7 +13,6 @@ import jp.co.soramitsu.core.updater.Updater import jp.co.soramitsu.coredb.dao.AccountStakingDao import jp.co.soramitsu.coredb.model.AccountStakingLocal import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.getRuntime import jp.co.soramitsu.runtime.network.updaters.insert import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.shared_utils.runtime.AccountId @@ -64,7 +63,7 @@ class StakingLedgerUpdater( val currentAccountId = scope.getAccount().accountId(chain)!! // TODO ethereum val key = runtime.metadata.staking().storage("Bonded").storageKey(runtime, currentAccountId) - runtime.metadata.staking().calls?.get("setController")?.arguments + updatesMixin.startUpdateAsset(scope.getAccount().id, chain.id, currentAccountId, chainAsset.id) return storageSubscriptionBuilder.subscribe(key) diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/controller/AccountControllerBalanceUpdater.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/controller/AccountControllerBalanceUpdater.kt index e838b98a3a..3d60f2126e 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/controller/AccountControllerBalanceUpdater.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/controller/AccountControllerBalanceUpdater.kt @@ -7,7 +7,6 @@ import jp.co.soramitsu.common.utils.system import jp.co.soramitsu.core.updater.SubscriptionBuilder import jp.co.soramitsu.core.updater.Updater import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.getRuntime import jp.co.soramitsu.shared_utils.runtime.metadata.storage import jp.co.soramitsu.shared_utils.runtime.metadata.storageKey import jp.co.soramitsu.staking.api.data.StakingSharedState diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt index c75dd221d4..1fa6a0e293 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/network/blockhain/updaters/historical/HistoricalUpdateMediator.kt @@ -8,7 +8,6 @@ import jp.co.soramitsu.core.updater.SubscriptionBuilder import jp.co.soramitsu.core.updater.Updater import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.runtime.multiNetwork.getRuntime import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.staking.api.data.StakingSharedState import jp.co.soramitsu.staking.impl.data.network.blockhain.updaters.fetchValuesToCache diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/IdentityRepositoryImpl.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/IdentityRepositoryImpl.kt index 77d05df8f8..06db76dd43 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/IdentityRepositoryImpl.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/IdentityRepositoryImpl.kt @@ -7,7 +7,6 @@ import jp.co.soramitsu.runtime.ext.accountFromMapKey import jp.co.soramitsu.runtime.ext.hexAccountIdOf import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain -import jp.co.soramitsu.runtime.multiNetwork.getSocket import jp.co.soramitsu.shared_utils.extensions.toHexString import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.shared_utils.runtime.definitions.types.Type @@ -34,8 +33,13 @@ class IdentityRepositoryImpl( chain: Chain, accountIdsHex: List ): AccountIdMap = withContext(Dispatchers.Default) { - val socketService = chainRegistry.getSocket(chain.id) - val runtime = chainRegistry.getRuntime(chain.id) + val (socketService, runtime) = if( chain.identityChain != null ) { + chainRegistry.awaitConnection(chain.identityChain!!).socketService to + chainRegistry.getRuntime(chain.identityChain!!) + } else { + chainRegistry.awaitConnection(chain.id).socketService to + chainRegistry.getRuntime(chain.id) + } val identityModule = runtime.metadata.module("Identity") diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/PayoutRepository.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/PayoutRepository.kt index 411231e421..8c563cff49 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/PayoutRepository.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/PayoutRepository.kt @@ -11,7 +11,6 @@ import jp.co.soramitsu.runtime.ext.accountIdOf import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.runtime.multiNetwork.getService import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.shared_utils.extensions.toHexString import jp.co.soramitsu.shared_utils.runtime.AccountId diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/StakingConstantsRepository.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/StakingConstantsRepository.kt index 9986a24c04..20fdc7ca24 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/StakingConstantsRepository.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/data/repository/StakingConstantsRepository.kt @@ -21,7 +21,7 @@ class StakingConstantsRepository( return try { val runtime = chainRegistry.getRuntime(chainId) - if(runtime.metadata.stakingOrNull()?.constantOrNull("MaxNominatorRewardedPerValidator") != null){ + if (runtime.metadata.stakingOrNull()?.constantOrNull("MaxNominatorRewardedPerValidator") != null) { return getNumberConstant(chainId, "MaxNominatorRewardedPerValidator").toInt() } else { // todo need research diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/di/validations/MakePayoutValidationsModule.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/di/validations/MakePayoutValidationsModule.kt index 0f807d7c06..63dca1967a 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/di/validations/MakePayoutValidationsModule.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/di/validations/MakePayoutValidationsModule.kt @@ -34,7 +34,7 @@ class MakePayoutValidationsModule { accountRepository, walletRepository, originAddressExtractor = { it.originAddress }, - chainAssetExtractor = { it.chainAsset }, + chainAssetExtractor = { it.token.configuration }, chainProducer = { stakingSharedState.chain() } ), errorProducer = { PayoutValidationFailure.CannotPayFee } diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractor.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractor.kt index 7c4de6100e..e5ba140b21 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractor.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractor.kt @@ -22,7 +22,6 @@ import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId -import jp.co.soramitsu.runtime.multiNetwork.getRuntimeOrNull import jp.co.soramitsu.shared_utils.runtime.AccountId import jp.co.soramitsu.shared_utils.runtime.metadata.RuntimeMetadata import jp.co.soramitsu.shared_utils.runtime.metadata.moduleOrNull diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractorExt.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractorExt.kt index bb1d88b2c5..b0de13f6dd 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractorExt.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/StakingInteractorExt.kt @@ -1,6 +1,5 @@ package jp.co.soramitsu.staking.impl.domain -import jp.co.soramitsu.shared_utils.extensions.toHexString import jp.co.soramitsu.shared_utils.runtime.AccountId import jp.co.soramitsu.staking.api.domain.model.IndividualExposure import kotlinx.coroutines.flow.first @@ -29,7 +28,11 @@ fun LegacyExposure.willAccountBeRewarded( accountId: AccountId, rewardedNominatorsPerValidator: Int? ): Boolean { - if(rewardedNominatorsPerValidator == null) return true + if(rewardedNominatorsPerValidator == null) { + return others.any { + it.who.contentEquals(accountId) + } + } val indexInRewardedList = others.sortedByDescending(IndividualExposure::value).indexOfFirst { it.who.contentEquals(accountId) } @@ -43,22 +46,4 @@ fun LegacyExposure.willAccountBeRewarded( return numberInRewardedList <= rewardedNominatorsPerValidator } -fun minimumStake( - exposures: Collection, - minimumNominatorBond: BigInteger -): BigInteger { - val stakeByNominator = exposures - .map(LegacyExposure::others) - .flatten() - .fold(mutableMapOf()) { acc, individualExposure -> - val currentExposure = acc.getOrDefault(individualExposure.who.toHexString(), BigInteger.ZERO) - - acc[individualExposure.who.toHexString()] = currentExposure + individualExposure.value - - acc - } - - return stakeByNominator.values.minOrZero().coerceAtLeast(minimumNominatorBond) -} - -private fun Iterable.minOrZero(): BigInteger = this.minOrNull() ?: BigInteger.ZERO +fun Iterable.minOrZero(): BigInteger = this.minOrNull() ?: BigInteger.ZERO diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/Alert.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/Alert.kt index 7fe0a2a0d0..7b3f6b9ff4 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/Alert.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/Alert.kt @@ -11,12 +11,12 @@ sealed class Alert { class BondMoreTokens(val minimalStake: BigDecimal, val token: Token) : Alert() - object ChangeValidators : Alert() - object AllValidatorsAreOversubscribed : Alert() + data object ChangeValidators : Alert() + data object AllValidatorsAreOversubscribed : Alert() - object WaitingForNextEra : Alert() + data object WaitingForNextEra : Alert() - object SetValidators : Alert() + data object SetValidators : Alert() class ChangeCollators(val collatorIdHex: String, val amountToStakeMore: String) : Alert() class CollatorLeaving(val delegation: CollatorDelegation, val collatorName: String) : Alert() diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/AlertsInteractor.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/AlertsInteractor.kt index 6e4489700e..44b0bb8a23 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/AlertsInteractor.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/alerts/AlertsInteractor.kt @@ -8,7 +8,6 @@ import jp.co.soramitsu.staking.api.domain.model.StakingState import jp.co.soramitsu.staking.impl.data.repository.StakingConstantsRepository import jp.co.soramitsu.staking.impl.domain.common.isWaiting import jp.co.soramitsu.staking.impl.domain.isNominationActive -import jp.co.soramitsu.staking.impl.domain.minimumStake import jp.co.soramitsu.staking.impl.scenarios.relaychain.StakingRelayChainScenarioRepository import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository import jp.co.soramitsu.wallet.impl.domain.model.Asset @@ -19,8 +18,10 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import java.math.BigDecimal import java.math.BigInteger +import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.shared_utils.extensions.toHexString import jp.co.soramitsu.staking.api.domain.model.LegacyExposure +import kotlinx.coroutines.ExperimentalCoroutinesApi import jp.co.soramitsu.core.models.Asset as CoreAsset private const val NOMINATIONS_ACTIVE_MEMO = "NOMINATIONS_ACTIVE_MEMO" @@ -34,13 +35,14 @@ class AlertsInteractor( ) { class AlertContext( - val exposures: Map?, + val exposures: Map, val stakingState: StakingState, val maxRewardedNominatorsPerValidator: Int?, val minimumNominatorBond: BigInteger, val activeEra: BigInteger, val asset: Asset, - val maxNominators: Int? + val maxNominators: Int?, + val isLegacyErasStakersSchema: Boolean ) { val memo = mutableMapOf() @@ -54,9 +56,6 @@ class AlertsInteractor( } private fun AlertContext.isStakingActive(stashId: AccountId) = useMemo(NOMINATIONS_ACTIVE_MEMO) { - if(exposures == null) { - return true - } isNominationActive(stashId, exposures.values, maxRewardedNominatorsPerValidator) } @@ -66,9 +65,18 @@ class AlertsInteractor( } } - private fun produceChangeValidatorsAlert(context: AlertContext): Alert? { + private suspend fun produceChangeValidatorsAlert(context: AlertContext): Alert? { return requireState(context.stakingState) { nominatorState: StakingState.Stash.Nominator -> - val allValidatorsAreOversubscribed = if (context.exposures == null || context.maxNominators == null) { + if (context.isLegacyErasStakersSchema.not()) { + val myValidators = stakingRepository.getRemoteAccountNominations( + context.stakingState.chain.id, + nominatorState.accountId + )?.targets.orEmpty() + + return@requireState if (context.exposures.any { it.key.fromHex() in myValidators }) Alert.ChangeValidators else null + } + + val allValidatorsAreOversubscribed = if (context.maxNominators == null) { false } else { nominatorState.nominations.targets.mapNotNull { context.exposures[it.toHexString()] } @@ -94,44 +102,52 @@ class AlertsInteractor( } } - private suspend fun produceMinStakeAlert(context: AlertContext) = requireState(context.stakingState) { state: StakingState.Stash -> - with(context) { - val minimalStakeInPlanks = (if(exposures != null) minimumStake(exposures.values, minimumNominatorBond) else stakingRepository.minimumActiveStake(stakingState.chain.id)).orZero() + private suspend fun produceMinStakeAlert(context: AlertContext) = + requireState(context.stakingState) { state: StakingState.Stash -> + with(context) { + val minActiveStake = stakingRepository.minimumActiveStake(stakingState.chain.id) + ?: context.exposures.values.minOf { exposure -> exposure.others.minOf { it.value } } - if ( + val minimalStakeInPlanks = + minActiveStake.coerceAtLeast(stakingRepository.minimumNominatorBond(context.asset.token.configuration)) + + if ( // do not show alert for validators - state !is StakingState.Stash.Validator && - asset.bondedInPlanks.orZero() < minimalStakeInPlanks && - // prevent alert for situation where all tokens are being unbounded - asset.bondedInPlanks.orZero() > BigInteger.ZERO - ) { - val minimalStake = asset.token.amountFromPlanks(minimalStakeInPlanks) - - Alert.BondMoreTokens(minimalStake, asset.token) - } else { - null + state !is StakingState.Stash.Validator && + asset.bondedInPlanks.orZero() < minimalStakeInPlanks && + // prevent alert for situation where all tokens are being unbounded + asset.bondedInPlanks.orZero() > BigInteger.ZERO + ) { + val minimalStake = asset.token.amountFromPlanks(minimalStakeInPlanks) + + Alert.BondMoreTokens(minimalStake, asset.token) + } else { + null + } } } - } private fun produceWaitingNextEraAlert(context: AlertContext) = requireState(context.stakingState) { nominatorState: StakingState.Stash.Nominator -> Alert.WaitingForNextEra.takeIf { - val isStakingActive = context.isStakingActive(nominatorState.stashId) + return@takeIf if(context.isLegacyErasStakersSchema) { + // staking is inactive and there is pending change + context.isStakingActive(nominatorState.stashId).not() && nominatorState.nominations.isWaiting(context.activeEra) + } else { - // staking is inactive and there is pending change - isStakingActive.not() && nominatorState.nominations.isWaiting(context.activeEra) + nominatorState.nominations.isWaiting(context.activeEra) + } } } private val alertProducers = listOf( - ::produceChangeValidatorsAlert, ::produceRedeemableAlert, ::produceWaitingNextEraAlert, ::produceSetValidatorsAlert ) - private val suspendableAlertProducers = listOf(::produceMinStakeAlert) + private val suspendableAlertProducers = listOf(::produceChangeValidatorsAlert,::produceMinStakeAlert) + @OptIn(ExperimentalCoroutinesApi::class) fun getAlertsFlow(stakingState: StakingState): Flow> = sharedState.assetWithChain.flatMapLatest { (chain, chainAsset) -> if (chainAsset.staking != CoreAsset.StakingType.RELAYCHAIN) { return@flatMapLatest flowOf(emptyList()) @@ -148,7 +164,7 @@ class AlertsInteractor( walletRepository.assetFlow(meta.id, stakingState.accountId, chainAsset, chain.minSupportedVersion), stakingRepository.observeActiveEraIndex(chain.id) ) { exposures, asset, activeEra -> - + val isLegacyErasStakersSchema = stakingRepository.isLegacyErasStakersSchema(chain.id) val context = AlertContext( exposures = exposures, stakingState = stakingState, @@ -156,9 +172,16 @@ class AlertsInteractor( minimumNominatorBond = minimumNominatorBond, asset = asset, activeEra = activeEra, - maxNominators = maxNominators + maxNominators = maxNominators, + isLegacyErasStakersSchema = isLegacyErasStakersSchema ) - alertProducers.mapNotNull { it.invoke(context) } + suspendableAlertProducers.mapNotNull { it.invoke(context) } + val alerts = (alertProducers.mapNotNull { it.invoke(context) } + suspendableAlertProducers.mapNotNull { it.invoke(context) }).toMutableList() + + if(alerts.contains(Alert.WaitingForNextEra) && alerts.any { it is Alert.BondMoreTokens }) { + alerts.remove(Alert.WaitingForNextEra) + } + + alerts } alertsFlow diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/recommendations/settings/RecommendationSettingsProvider.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/recommendations/settings/RecommendationSettingsProvider.kt index 54d6b630c2..e2307cbac3 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/recommendations/settings/RecommendationSettingsProvider.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/recommendations/settings/RecommendationSettingsProvider.kt @@ -68,7 +68,7 @@ abstract class RecommendationSettingsProvider { override fun defaultSettings(): RecommendationSettings { return RecommendationSettings( alwaysEnabledFilters = listOf(BlockProducerFilters.ValidatorFilter.HasBlocked), - customEnabledFilters = listOf(BlockProducerFilters.ValidatorFilter.HundredPercentCommissionFilter, BlockProducerFilters.ValidatorFilter.NotOverSubscribedFilter(maximumRewardedNominators), BlockProducerFilters.ValidatorFilter.ElectedFilter), + customEnabledFilters = listOf(BlockProducerFilters.ValidatorFilter.HundredPercentCommissionFilter, BlockProducerFilters.ValidatorFilter.NotOverSubscribedFilter(maximumRewardedNominators), BlockProducerFilters.ValidatorFilter.ElectedFilter, BlockProducerFilters.ValidatorFilter.HasIdentity), sorting = BlockProducersSorting.ValidatorSorting.APYSorting, postProcessors = allPostProcessors, limit = maximumValidatorsPerNominator diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/ReefRewardCalculator.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/ReefRewardCalculator.kt index 87f76fbf15..75e143947f 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/ReefRewardCalculator.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/ReefRewardCalculator.kt @@ -9,11 +9,9 @@ import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.shared_utils.extensions.toHexString -import jp.co.soramitsu.shared_utils.ss58.SS58Encoder.toAccountId import jp.co.soramitsu.staking.impl.data.network.blockhain.bindings.EraRewardPoints import jp.co.soramitsu.staking.impl.data.repository.HistoricalMapping import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks -import kotlin.math.pow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -35,7 +33,7 @@ class ReefRewardCalculator( calculationTargets.associateWith { apyByValidator[it] }.filterValues { it != null } .cast>() - private val maxAPY = apyByCalculationTargets.values.maxOrNull() ?: 0.0 + private val maxAPY = apyByCalculationTargets.values.filter { it.isNaN().not() }.maxOrNull() ?: 0.0 private val expectedAPY = calculateExpectedAPY() private fun calculateExpectedAPY(): Double { @@ -51,11 +49,16 @@ class ReefRewardCalculator( private fun calculateValidatorAPY(validator: RewardCalculationTarget): Double { return runCatching { - val averageValidatorRewardPoints = - historicalRewardDistribution.values.asSequence().map { it.individual } - .flatten() - .filter { it.accountId.contentEquals(validator.accountIdHex.fromHex()) } - .map { it.rewardPoints.toDouble() }.average() + val validatorHistoricalRewardDistribution = historicalRewardDistribution.values.asSequence().map { it.individual } + .flatten() + .filter { it.accountId.contentEquals(validator.accountIdHex.fromHex()) } + .toList() + + val averageValidatorRewardPoints = if(validatorHistoricalRewardDistribution.isEmpty()){ + 0.0 + } else { + validatorHistoricalRewardDistribution.map { it.rewardPoints.toDouble() }.average() + } val rewardDistributionForLastEra = historicalRewardDistribution.maxBy { it.key }.value diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/SoraRewardCalculator.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/SoraRewardCalculator.kt index 55914a56be..07c67d7273 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/SoraRewardCalculator.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/rewards/SoraRewardCalculator.kt @@ -6,6 +6,7 @@ import jp.co.soramitsu.common.utils.fractionToPercentage import jp.co.soramitsu.common.utils.median import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.shared_utils.extensions.toHexString import jp.co.soramitsu.staking.impl.data.network.blockhain.bindings.EraRewardPoints import jp.co.soramitsu.staking.impl.data.repository.HistoricalMapping @@ -52,10 +53,16 @@ class SoraRewardCalculator( } private fun calculateValidatorAPY(validator: RewardCalculationTarget): Double { - val averageValidatorRewardPoints = - historicalRewardDistribution.values.asSequence().map { it.individual }.flatten() - .filter { it.accountId.toHexString(false) == validator.accountIdHex } - .map { it.rewardPoints.toDouble() }.average() + val validatorHistoricalRewardDistribution = historicalRewardDistribution.values.asSequence().map { it.individual } + .flatten() + .filter { it.accountId.contentEquals(validator.accountIdHex.fromHex()) } + .toList() + + val averageValidatorRewardPoints = if(validatorHistoricalRewardDistribution.isEmpty()){ + 0.0 + } else { + validatorHistoricalRewardDistribution.map { it.rewardPoints.toDouble() }.average() + } val validatorOwnStake = asset.amountFromPlanks(validator.totalStake).toDouble() diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/MakePayoutPayload.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/MakePayoutPayload.kt index f1927f6ad7..aacfef2328 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/MakePayoutPayload.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/MakePayoutPayload.kt @@ -1,15 +1,26 @@ package jp.co.soramitsu.staking.impl.domain.validations.payout -import jp.co.soramitsu.core.models.Asset import java.math.BigDecimal import java.math.BigInteger +import jp.co.soramitsu.wallet.impl.domain.model.Token -class MakePayoutPayload( +open class MakePayoutPayload( val originAddress: String, val fee: BigDecimal, val totalReward: BigDecimal, - val chainAsset: Asset, + val token: Token, val payoutStakersCalls: List ) { data class PayoutStakersPayload(val era: BigInteger, val validatorAddress: String) } + +class SoraPayoutsPayload( + originAddress: String, + fee: BigDecimal, + totalReward: BigDecimal, + utilityToken: Token, + val rewardToken: Token, + payoutStakersCalls: List +): MakePayoutPayload(originAddress, fee, totalReward, utilityToken, payoutStakersCalls) { + +} \ No newline at end of file diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/ProfitablePayoutValidation.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/ProfitablePayoutValidation.kt index 0ccce60c5c..1d940b7148 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/ProfitablePayoutValidation.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/payout/ProfitablePayoutValidation.kt @@ -7,10 +7,29 @@ import jp.co.soramitsu.common.validation.ValidationStatus class ProfitablePayoutValidation : Validation { override suspend fun validate(value: MakePayoutPayload): ValidationStatus { - return if (value.fee < value.totalReward) { - ValidationStatus.Valid() + return if (value is SoraPayoutsPayload) { + val feeFiat = value.token.fiatAmount(value.fee) + val totalRewardFiat = value.rewardToken.fiatAmount(value.totalReward) + if (feeFiat == null || totalRewardFiat == null) { + return ValidationStatus.Valid() + } + if (feeFiat < totalRewardFiat) { + ValidationStatus.Valid() + } else { + ValidationStatus.NotValid( + DefaultFailureLevel.WARNING, + reason = PayoutValidationFailure.UnprofitablePayout + ) + } } else { - ValidationStatus.NotValid(DefaultFailureLevel.WARNING, reason = PayoutValidationFailure.UnprofitablePayout) + if (value.fee < value.totalReward) { + ValidationStatus.Valid() + } else { + ValidationStatus.NotValid( + DefaultFailureLevel.WARNING, + reason = PayoutValidationFailure.UnprofitablePayout + ) + } } } } diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/setup/MinimumAmountValidation.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/setup/MinimumAmountValidation.kt index f8b90f68a6..6223f6df0f 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/setup/MinimumAmountValidation.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validations/setup/MinimumAmountValidation.kt @@ -1,10 +1,16 @@ package jp.co.soramitsu.staking.impl.domain.validations.setup +import java.math.BigDecimal import jp.co.soramitsu.common.validation.DefaultFailureLevel import jp.co.soramitsu.common.validation.Validation import jp.co.soramitsu.common.validation.ValidationStatus +import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId +import jp.co.soramitsu.staking.impl.domain.model.NetworkInfo +import jp.co.soramitsu.staking.impl.presentation.staking.main.scenarios.StakingRelaychainScenarioViewModel import jp.co.soramitsu.staking.impl.scenarios.StakingScenarioInteractor import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map class MinimumAmountValidation( private val stakingScenarioInteractor: StakingScenarioInteractor @@ -13,8 +19,15 @@ class MinimumAmountValidation( override suspend fun validate(value: SetupStakingPayload): ValidationStatus { val assetConfiguration = value.asset.token.configuration + val networkInfo = stakingScenarioInteractor.observeNetworkInfoState().map { it as? NetworkInfo.RelayChain }.firstOrNull() val minimumBondInPlanks = stakingScenarioInteractor.getMinimumStake(assetConfiguration) - val minimumBond = assetConfiguration.amountFromPlanks(minimumBondInPlanks) + val minStakeMultiplier: Double = if (networkInfo?.shouldUseMinimumStakeMultiplier == true) { + StakingRelaychainScenarioViewModel.STAKE_EXTRA_MULTIPLIER // 15% increase + } else { + 1.0 + } + + val minimumBond = assetConfiguration.amountFromPlanks(minimumBondInPlanks) * BigDecimal(minStakeMultiplier) // either first time bond or already existing bonded balance val amountToCheckAgainstMinimum = value.bondAmount ?: value.asset.bonded diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validators/current/CurrentValidatorsInteractor.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validators/current/CurrentValidatorsInteractor.kt index 16e6c3475b..d8db130aae 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validators/current/CurrentValidatorsInteractor.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/domain/validators/current/CurrentValidatorsInteractor.kt @@ -64,7 +64,7 @@ class CurrentValidatorsInteractor( val userIndividualExposure = activeNominations[validator.accountIdHex] val status = when { - userIndividualExposure != null -> { + userIndividualExposure != null && validator.electedInfo != null -> { // safe to !! here since non null nomination means that validator is elected val userNominationIndex = validator.electedInfo!!.nominatorStakes .sortedByDescending(IndividualExposure::value) diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/StakingConfirmViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/StakingConfirmViewModel.kt index 7da47fe227..380140e4fb 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/StakingConfirmViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/StakingConfirmViewModel.kt @@ -7,10 +7,12 @@ import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase import jp.co.soramitsu.wallet.api.presentation.BaseConfirmViewModel +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import jp.co.soramitsu.wallet.impl.domain.model.Asset abstract class StakingConfirmViewModel( - private val existentialDepositUseCase: ExistentialDepositUseCase, + walletInteractor: WalletInteractor, + existentialDepositUseCase: ExistentialDepositUseCase, private val router: StakingRouter, resourceManager: ResourceManager, asset: Asset, @@ -26,6 +28,7 @@ abstract class StakingConfirmViewModel( private val onOperationSuccess: () -> Unit, private val customSuccessMessage: String? = null ) : BaseConfirmViewModel( + walletInteractor, existentialDepositUseCase, resourceManager, asset, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/common/StakingAssetSelector.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/common/StakingAssetSelector.kt index 4807b847a3..d44aca530f 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/common/StakingAssetSelector.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/common/StakingAssetSelector.kt @@ -23,11 +23,8 @@ class StakingAssetSelector( val showAssetChooser = MutableLiveData>>() - val selectedItem = stakingSharedState.selectionItem - .shareIn(this, SharingStarted.Eagerly, replay = 1) - val selectedAssetModelFlow: SharedFlow = combine( - selectedItem, + stakingSharedState.selectionItem, stakingSharedState.currentAssetFlow() ) { selectedItem, asset -> val assetBalance = if (selectedItem.type == StakingType.POOL) { diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/ConfirmStakingViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/ConfirmStakingViewModel.kt index 00ac59f1fa..a5995b5a78 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/ConfirmStakingViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/ConfirmStakingViewModel.kt @@ -13,7 +13,6 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.AddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Retriable import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ClipboardManager @@ -25,7 +24,7 @@ import jp.co.soramitsu.common.validation.progressConsumer import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.ext.addressOf import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.api.domain.model.RewardDestination import jp.co.soramitsu.staking.api.domain.model.StakingState import jp.co.soramitsu.staking.api.domain.model.Validator @@ -201,7 +200,7 @@ class ConfirmStakingViewModel @Inject constructor( interactor.getSelectedAccountProjection()?.let { account -> val chainId = controllerAssetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, account.address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(account.address) val externalActionsPayload = ExternalAccountActions.Payload( value = account.address, chainId = chainId, @@ -218,7 +217,7 @@ class ConfirmStakingViewModel @Inject constructor( val payoutDestination = rewardDestinationLiveData.value as? RewardDestinationModel.Payout ?: return@launch val chainId = controllerAssetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, payoutDestination.destination.address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(payoutDestination.destination.address) val externalActionsPayload = ExternalAccountActions.Payload( value = payoutDestination.destination.address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/pool/create/ConfirmCreatePoolViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/pool/create/ConfirmCreatePoolViewModel.kt index 614db5a76c..afdfb6e4a7 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/pool/create/ConfirmCreatePoolViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/confirm/pool/create/ConfirmCreatePoolViewModel.kt @@ -15,6 +15,7 @@ import jp.co.soramitsu.staking.impl.presentation.common.SelectValidatorFlowState import jp.co.soramitsu.staking.impl.presentation.common.StakingPoolSharedStateProvider import jp.co.soramitsu.staking.impl.scenarios.StakingPoolInteractor import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.stateIn @HiltViewModel class ConfirmCreatePoolViewModel @Inject constructor( + walletInteractor: WalletInteractor, existentialDepositUseCase: ExistentialDepositUseCase, poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, @@ -29,6 +31,7 @@ class ConfirmCreatePoolViewModel @Inject constructor( private val router: StakingRouter, private val poolInteractor: StakingPoolInteractor ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt index ab07743f4b..0eb2395fc3 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/confirm/ConfirmPayoutViewModel.kt @@ -13,7 +13,6 @@ import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.base.TitleAndMessage -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.formatCryptoDetail @@ -25,7 +24,7 @@ import jp.co.soramitsu.common.validation.ValidationSystem import jp.co.soramitsu.common.validation.progressConsumer import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.shared_utils.ss58.SS58Encoder.addressByte import jp.co.soramitsu.shared_utils.ss58.SS58Encoder.toAddress import jp.co.soramitsu.staking.api.data.SyntheticStakingType @@ -38,6 +37,7 @@ import jp.co.soramitsu.staking.impl.domain.payout.PayoutInteractor import jp.co.soramitsu.staking.impl.domain.rewards.SoraStakingRewardsScenario import jp.co.soramitsu.staking.impl.domain.validations.payout.MakePayoutPayload import jp.co.soramitsu.staking.impl.domain.validations.payout.PayoutValidationFailure +import jp.co.soramitsu.staking.impl.domain.validations.payout.SoraPayoutsPayload import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.staking.impl.presentation.payouts.confirm.model.ConfirmPayoutPayload import jp.co.soramitsu.staking.impl.scenarios.relaychain.StakingRelayChainScenarioInteractor @@ -72,7 +72,7 @@ class ConfirmPayoutViewModel @Inject constructor( private val payload = savedStateHandle.get(ConfirmPayoutFragment.KEY_PAYOUTS)!! private val tokenFlow = interactor.currentAssetFlow().map { - if(it.token.configuration.syntheticStakingType() == SyntheticStakingType.SORA){ + if(it.token.configuration.syntheticStakingType() == SyntheticStakingType.SORA) { soraRewardScenario.getRewardAsset() } else { it.token @@ -146,13 +146,20 @@ class ConfirmPayoutViewModel @Inject constructor( private fun sendTransactionIfValid() = feeLoaderMixin.requireFee(this) { fee -> launch { - val tokenType = interactor.currentAssetFlow().first().token.configuration + val asset = interactor.currentAssetFlow().first() + val tokenType = asset.token.configuration val accountAddress = stakingStateFlow.first().accountAddress val amount = tokenType.amountFromPlanks(payload.totalRewardInPlanks) val payoutStakersPayloads = payouts.map { MakePayoutPayload.PayoutStakersPayload(it.era, it.validatorAddress) } - val makePayoutPayload = MakePayoutPayload(accountAddress, fee, amount, tokenType, payoutStakersPayloads) + val makePayoutPayload = if(tokenType.syntheticStakingType() == SyntheticStakingType.SORA ) { + val rewardToken = soraRewardScenario.getRewardAsset() + SoraPayoutsPayload(accountAddress, fee, amount, asset.token, rewardToken, payoutStakersPayloads) + } else { + MakePayoutPayload(accountAddress, fee, amount, asset.token, payoutStakersPayloads) + } + validationExecutor.requireValid( validationSystem = validationSystem, @@ -204,7 +211,7 @@ class ConfirmPayoutViewModel @Inject constructor( val address = addressProducer() ?: return@launch val chainId = tokenFlow.first().configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(address) val externalActionsPayload = ExternalAccountActions.Payload( value = address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/detail/PayoutDetailsViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/detail/PayoutDetailsViewModel.kt index dc79ce1e21..eee4bccea9 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/detail/PayoutDetailsViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/payouts/detail/PayoutDetailsViewModel.kt @@ -7,14 +7,13 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.formatCryptoDetail import jp.co.soramitsu.common.utils.formatFiat import jp.co.soramitsu.common.utils.inBackground import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.api.data.SyntheticStakingType import jp.co.soramitsu.staking.api.data.syntheticStakingType import jp.co.soramitsu.staking.impl.domain.StakingInteractor @@ -66,10 +65,7 @@ class PayoutDetailsViewModel @Inject constructor( fun validatorExternalActionClicked() = launch { val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers( - BlockExplorerUrlBuilder.Type.ACCOUNT, - payout.validatorInfo.address - ) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(payout.validatorInfo.address) val externalActionsPayload = ExternalAccountActions.Payload( value = payout.validatorInfo.address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/pools/edit/EditPoolConfirmViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/pools/edit/EditPoolConfirmViewModel.kt index 36e3d4e601..cac031a73c 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/pools/edit/EditPoolConfirmViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/pools/edit/EditPoolConfirmViewModel.kt @@ -17,15 +17,18 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor @HiltViewModel class EditPoolConfirmViewModel @Inject constructor( + walletInteractor: WalletInteractor, existentialDepositUseCase: ExistentialDepositUseCase, private val resourceManager: ResourceManager, private val poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, private val router: StakingRouter ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt index 4291f43531..8990c21256 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmBondMoreViewModel.kt @@ -10,7 +10,6 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.flowOf @@ -22,7 +21,7 @@ import jp.co.soramitsu.common.validation.ValidationExecutor import jp.co.soramitsu.common.validation.progressConsumer import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.impl.domain.StakingInteractor import jp.co.soramitsu.staking.impl.domain.staking.bond.BondMoreInteractor import jp.co.soramitsu.staking.impl.domain.validations.bond.BondMoreValidationPayload @@ -108,7 +107,7 @@ class ConfirmBondMoreViewModel @Inject constructor( fun originAccountClicked() = launch { val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, payload.stashAddress) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(payload.stashAddress) val externalActionsPayload = ExternalAccountActions.Payload( value = payload.stashAddress, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmPoolBondMoreViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmPoolBondMoreViewModel.kt index 48ece91cff..7ddb80cc21 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmPoolBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/confirm/ConfirmPoolBondMoreViewModel.kt @@ -9,17 +9,20 @@ import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.staking.impl.presentation.common.StakingPoolSharedStateProvider import jp.co.soramitsu.staking.impl.scenarios.StakingPoolInteractor import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @HiltViewModel class ConfirmPoolBondMoreViewModel @Inject constructor( + walletInteractor: WalletInteractor, existentialDepositUseCase: ExistentialDepositUseCase, poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, resourceManager: ResourceManager, private val router: StakingRouter ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt index 207c8c6e3d..dd35ae9343 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/bond/select/SelectBondMoreViewModel.kt @@ -41,8 +41,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch private const val DEFAULT_AMOUNT = 1 @@ -92,8 +92,7 @@ class SelectBondMoreViewModel @Inject constructor( .asLiveData() val enteredAmountFlow = MutableStateFlow(DEFAULT_AMOUNT.toString()) - - private val parsedAmountFlow = enteredAmountFlow.mapNotNull { it.toBigDecimalOrNull() } + private val decimalAmountFlow = MutableStateFlow(DEFAULT_AMOUNT.toBigDecimal()) val accountLiveData = stakingScenarioInteractor.getSelectedAccountAddress() .inBackground() @@ -103,7 +102,7 @@ class SelectBondMoreViewModel @Inject constructor( .inBackground() .asLiveData() - val enteredFiatAmountFlow = assetFlow.combine(parsedAmountFlow) { asset, amount -> + val enteredFiatAmountFlow = assetFlow.combine(decimalAmountFlow) { asset, amount -> asset.token.fiatAmount(amount)?.formatFiat(asset.token.fiatSymbol) } .inBackground() @@ -111,6 +110,12 @@ class SelectBondMoreViewModel @Inject constructor( init { listenFee() + enteredAmountFlow.onEach { stringValue -> + decimalAmountFlow.update { + // todo don't do like this, we must create a reversed formatter from String to BigDecimal + stringValue.replace(",", "").toBigDecimalOrNull() ?: it + } + }.launchIn(viewModelScope) } fun nextClicked() { @@ -127,7 +132,7 @@ class SelectBondMoreViewModel @Inject constructor( @OptIn(FlowPreview::class) private fun listenFee() { - parsedAmountFlow + decimalAmountFlow .debounce(DEBOUNCE_DURATION_MILLIS.toDuration(DurationUnit.MILLISECONDS)) .onEach { loadFee(it) } .launchIn(viewModelScope) @@ -157,7 +162,7 @@ class SelectBondMoreViewModel @Inject constructor( val payload = BondMoreValidationPayload( stashAddress = stashAddress(), fee = fee, - amount = parsedAmountFlow.first(), + amount = decimalAmountFlow.value, chainAsset = assetFlow.first().token.configuration ) @@ -244,6 +249,7 @@ class SelectBondMoreViewModel @Inject constructor( } enteredAmountFlow.emit(value.formatCrypto()) + decimalAmountFlow.update { value } } } } diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/claim/ConfirmPoolClaimViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/claim/ConfirmPoolClaimViewModel.kt index a28066c00b..7ab83942d7 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/claim/ConfirmPoolClaimViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/claim/ConfirmPoolClaimViewModel.kt @@ -12,15 +12,18 @@ import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.staking.impl.presentation.common.StakingPoolSharedStateProvider import jp.co.soramitsu.staking.impl.scenarios.StakingPoolInteractor import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor @HiltViewModel class ConfirmPoolClaimViewModel @Inject constructor( + walletInteractor: WalletInteractor, private val existentialDepositUseCase: ExistentialDepositUseCase, poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, private val resourceManager: ResourceManager, private val router: StakingRouter ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt index 17b9e92ef8..38b06c47d5 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/confirm/ConfirmSetControllerViewModel.kt @@ -9,7 +9,6 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.flowOf @@ -17,7 +16,7 @@ import jp.co.soramitsu.common.utils.inBackground import jp.co.soramitsu.common.validation.ValidationExecutor import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.impl.domain.StakingInteractor import jp.co.soramitsu.staking.impl.domain.staking.controller.ControllerInteractor import jp.co.soramitsu.staking.impl.domain.validations.controller.SetControllerValidationPayload @@ -77,7 +76,7 @@ class ConfirmSetControllerViewModel @Inject constructor( viewModelScope.launch { val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, payload.stashAddress) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(payload.stashAddress) val externalActionsPayload = ExternalAccountActions.Payload( value = payload.stashAddress, chainId = chainId, @@ -93,7 +92,7 @@ class ConfirmSetControllerViewModel @Inject constructor( viewModelScope.launch { val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, payload.controllerAddress) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(payload.controllerAddress) val externalActionsPayload = ExternalAccountActions.Payload( value = payload.controllerAddress, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/set/SetControllerViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/set/SetControllerViewModel.kt index c0e76c8bba..0d9d5cc0a1 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/set/SetControllerViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/controller/set/SetControllerViewModel.kt @@ -13,7 +13,6 @@ import jp.co.soramitsu.common.address.AddressModel import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.data.network.AppLinksProvider -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.RetryPayload import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager @@ -25,7 +24,7 @@ import jp.co.soramitsu.common.utils.updateFrom import jp.co.soramitsu.common.validation.ValidationExecutor import jp.co.soramitsu.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet.Payload import jp.co.soramitsu.feature_wallet_api.R -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.api.domain.model.StakingAccount import jp.co.soramitsu.staking.api.domain.model.StakingState import jp.co.soramitsu.staking.impl.domain.StakingInteractor @@ -122,7 +121,7 @@ class SetControllerViewModel @Inject constructor( } else { stakingInteractor.getChain(chainId) } - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, stashAddress) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(stashAddress) val externalActionsPayload = ExternalAccountActions.Payload( value = stashAddress, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/main/scenarios/StakingRelaychainScenarioViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/main/scenarios/StakingRelaychainScenarioViewModel.kt index b744478e44..0a643b6ffe 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/main/scenarios/StakingRelaychainScenarioViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/main/scenarios/StakingRelaychainScenarioViewModel.kt @@ -58,6 +58,10 @@ class StakingRelaychainScenarioViewModel( stakingSharedState: StakingSharedState ) : StakingScenarioViewModel { + companion object { + const val STAKE_EXTRA_MULTIPLIER = 1.15 // allow to be not at the bottom of reward list and not be excluded soon + } + override val enteredAmountFlow: MutableStateFlow = MutableStateFlow(BigDecimal.ZERO) private val welcomeStakingValidationSystem = ValidationSystem( @@ -73,46 +77,41 @@ class StakingRelaychainScenarioViewModel( ) ) - private val viewStatesCash: MutableMap = mutableMapOf() - override val stakingStateFlow: Flow = scenarioInteractor.stakingStateFlow().shareIn(baseViewModel.stakingStateScope, SharingStarted.Eagerly, 1) override val stakingViewStateFlowOld: Flow = stakingStateFlow.distinctUntilChanged().map { stakingState -> - val key = "${stakingState.accountId.toHexString()}:${stakingState.chain.id}" - viewStatesCash.getOrPut(key) { - when (stakingState) { - is StakingState.Stash.Nominator -> stakingViewStateFactory.createNominatorViewState( - stakingState, - stakingInteractor.currentAssetFlow(), - baseViewModel.stakingStateScope, - baseViewModel::showError - ) + when (stakingState) { + is StakingState.Stash.Nominator -> stakingViewStateFactory.createNominatorViewState( + stakingState, + stakingInteractor.currentAssetFlow(), + baseViewModel.stakingStateScope, + baseViewModel::showError + ) - is StakingState.Stash.None -> stakingViewStateFactory.createStashNoneState( - stakingInteractor.currentAssetFlow(), - stakingState, - baseViewModel.stakingStateScope, - baseViewModel::showError - ) + is StakingState.Stash.None -> stakingViewStateFactory.createStashNoneState( + stakingInteractor.currentAssetFlow(), + stakingState, + baseViewModel.stakingStateScope, + baseViewModel::showError + ) - is StakingState.NonStash -> stakingViewStateFactory.createRelayChainWelcomeViewState( - stakingInteractor.currentAssetFlow(), - baseViewModel.stakingStateScope, - welcomeStakingValidationSystem = welcomeStakingValidationSystem, - baseViewModel::showError - ) + is StakingState.NonStash -> stakingViewStateFactory.createRelayChainWelcomeViewState( + stakingInteractor.currentAssetFlow(), + baseViewModel.stakingStateScope, + welcomeStakingValidationSystem = welcomeStakingValidationSystem, + baseViewModel::showError + ) - is StakingState.Stash.Validator -> stakingViewStateFactory.createValidatorViewState( - stakingState, - stakingInteractor.currentAssetFlow(), - baseViewModel.stakingStateScope, - baseViewModel::showError - ) + is StakingState.Stash.Validator -> stakingViewStateFactory.createValidatorViewState( + stakingState, + stakingInteractor.currentAssetFlow(), + baseViewModel.stakingStateScope, + baseViewModel::showError + ) - else -> error("Wrong state") - } + else -> error("Wrong state") } }.shareIn(baseViewModel.stakingStateScope, SharingStarted.Eagerly, 1) @@ -164,7 +163,7 @@ class StakingRelaychainScenarioViewModel( ) { networkInfo, asset -> val minStakeMultiplier: Double = if (networkInfo.shouldUseMinimumStakeMultiplier) { - 1.15 // 15% increase + STAKE_EXTRA_MULTIPLIER // 15% increase } else { 1.0 } diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt index 864cfd4fbd..4c65563a4f 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rebond/confirm/ConfirmRebondViewModel.kt @@ -11,7 +11,6 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.formatCryptoFull @@ -21,7 +20,7 @@ import jp.co.soramitsu.common.validation.ValidationExecutor import jp.co.soramitsu.common.validation.progressConsumer import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.impl.domain.StakingInteractor import jp.co.soramitsu.staking.impl.domain.staking.rebond.RebondInteractor import jp.co.soramitsu.staking.impl.domain.validations.rebond.RebondValidationPayload @@ -116,10 +115,7 @@ class ConfirmRebondViewModel @Inject constructor( val originAddressModel = originAddressModelLiveData.value ?: return@launch val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers( - BlockExplorerUrlBuilder.Type.ACCOUNT, - originAddressModel.address - ) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(originAddressModel.address) val externalActionsPayload = ExternalAccountActions.Payload( value = originAddressModel.address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/ConfirmPoolRedeemViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/ConfirmPoolRedeemViewModel.kt index 52604f0688..274edfe33a 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/ConfirmPoolRedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/ConfirmPoolRedeemViewModel.kt @@ -12,15 +12,18 @@ import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.staking.impl.presentation.common.StakingPoolSharedStateProvider import jp.co.soramitsu.staking.impl.scenarios.StakingPoolInteractor import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor @HiltViewModel class ConfirmPoolRedeemViewModel @Inject constructor( + walletInteractor: WalletInteractor, private val existentialDepositUseCase: ExistentialDepositUseCase, poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, private val resourceManager: ResourceManager, private val router: StakingRouter ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/RedeemViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/RedeemViewModel.kt index 7b699a6aa3..ec851a13be 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/RedeemViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/redeem/RedeemViewModel.kt @@ -13,7 +13,6 @@ import jp.co.soramitsu.common.R import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.formatCryptoFull @@ -24,7 +23,7 @@ import jp.co.soramitsu.common.utils.requireValue import jp.co.soramitsu.common.validation.ValidationExecutor import jp.co.soramitsu.common.validation.progressConsumer import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.staking.api.domain.model.StakingState import jp.co.soramitsu.staking.impl.domain.StakingInteractor @@ -155,7 +154,7 @@ class RedeemViewModel @Inject constructor( val address = originAddressModelLiveData.value?.address ?: return@launch val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(address) val externalActionsPayload = ExternalAccountActions.Payload( value = address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt index c66242b632..1e18d274a6 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/rewardDestination/confirm/ConfirmRewardDestinationViewModel.kt @@ -8,7 +8,6 @@ import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.AddressModel import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.inBackground @@ -35,13 +34,13 @@ import jp.co.soramitsu.staking.impl.scenarios.relaychain.StakingRelayChainScenar import jp.co.soramitsu.wallet.api.data.mappers.mapFeeToFeeModel import jp.co.soramitsu.wallet.api.presentation.mixin.fee.FeeStatus import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers @HiltViewModel class ConfirmRewardDestinationViewModel @Inject constructor( @@ -111,7 +110,7 @@ class ConfirmRewardDestinationViewModel @Inject constructor( private fun showAddressExternalActions(address: String) = launch { val chainId = controllerAssetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(address) val externalActionsPayload = ExternalAccountActions.Payload( value = address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmPoolUnbondViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmPoolUnbondViewModel.kt index a937adfffc..1c2baff3c4 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmPoolUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmPoolUnbondViewModel.kt @@ -12,15 +12,18 @@ import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.staking.impl.presentation.common.StakingPoolSharedStateProvider import jp.co.soramitsu.staking.impl.scenarios.StakingPoolInteractor import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor @HiltViewModel class ConfirmPoolUnbondViewModel @Inject constructor( + walletInteractor: WalletInteractor, private val existentialDepositUseCase: ExistentialDepositUseCase, poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, private val resourceManager: ResourceManager, private val router: StakingRouter ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt index c894b74bf4..441368466e 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/confirm/ConfirmUnbondViewModel.kt @@ -8,7 +8,6 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createAddressModel import jp.co.soramitsu.common.base.BaseViewModel -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.mixin.api.Validatable import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.formatFiat @@ -19,7 +18,6 @@ import jp.co.soramitsu.common.validation.ValidationExecutor import jp.co.soramitsu.common.validation.progressConsumer import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers import jp.co.soramitsu.staking.impl.domain.StakingInteractor import jp.co.soramitsu.staking.impl.domain.staking.unbond.UnbondInteractor import jp.co.soramitsu.staking.impl.domain.validations.unbond.UnbondValidationPayload @@ -36,6 +34,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers @HiltViewModel class ConfirmUnbondViewModel @Inject constructor( @@ -108,7 +107,7 @@ class ConfirmUnbondViewModel @Inject constructor( val originAddressModel = originAddressModelLiveData.value ?: return@launch val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, originAddressModel.address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(originAddressModel.address) val externalActionsPayload = ExternalAccountActions.Payload( value = originAddressModel.address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/compose/ConfirmSelectValidatorsViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/compose/ConfirmSelectValidatorsViewModel.kt index 65ebc25a24..86512fff86 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/compose/ConfirmSelectValidatorsViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/compose/ConfirmSelectValidatorsViewModel.kt @@ -11,6 +11,7 @@ import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.staking.impl.presentation.common.StakingPoolSharedStateProvider import jp.co.soramitsu.staking.impl.scenarios.StakingPoolInteractor import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -18,12 +19,14 @@ import kotlinx.coroutines.flow.stateIn @HiltViewModel class ConfirmSelectValidatorsViewModel @Inject constructor( + walletInteractor: WalletInteractor, existentialDepositUseCase: ExistentialDepositUseCase, poolSharedStateProvider: StakingPoolSharedStateProvider, private val stakingPoolInteractor: StakingPoolInteractor, resourceManager: ResourceManager, private val router: StakingRouter ) : StakingConfirmViewModel( + walletInteractor = walletInteractor, existentialDepositUseCase = existentialDepositUseCase, chain = poolSharedStateProvider.requireMainState.requireChain, router = router, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/CollatorDetailsViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/CollatorDetailsViewModel.kt index 6af39bf7b5..bc741e02bb 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/CollatorDetailsViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/CollatorDetailsViewModel.kt @@ -11,7 +11,6 @@ import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.createEthereumAddressModel import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.data.network.AppLinksProvider -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.formatAsPercentage @@ -20,7 +19,7 @@ import jp.co.soramitsu.common.utils.formatFiat import jp.co.soramitsu.common.utils.inBackground import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.shared_utils.extensions.fromHex import jp.co.soramitsu.staking.api.domain.model.CandidateInfoStatus import jp.co.soramitsu.staking.impl.domain.StakingInteractor @@ -174,7 +173,7 @@ class CollatorDetailsViewModel @Inject constructor( val address = collatorDetails.value?.address ?: return@launch val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(address) val externalActionsPayload = ExternalAccountActions.Payload( value = address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/ValidatorDetailsViewModel.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/ValidatorDetailsViewModel.kt index 334e5cc61a..5d3f410cd4 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/ValidatorDetailsViewModel.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/validators/details/ValidatorDetailsViewModel.kt @@ -10,7 +10,6 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.data.network.AppLinksProvider -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.flowOf @@ -20,7 +19,7 @@ import jp.co.soramitsu.common.utils.inBackground import jp.co.soramitsu.common.utils.sumByBigInteger import jp.co.soramitsu.feature_staking_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.staking.impl.domain.StakingInteractor import jp.co.soramitsu.staking.impl.domain.getSelectedChain import jp.co.soramitsu.staking.impl.presentation.StakingRouter @@ -142,7 +141,7 @@ class ValidatorDetailsViewModel @Inject constructor( val address = validatorDetails.value?.address ?: return@launch val chainId = assetFlow.first().token.configuration.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(address) val externalActionsPayload = ExternalAccountActions.Payload( value = address, chainId = chainId, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/StakingPoolInteractor.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/StakingPoolInteractor.kt index dbd0bf2910..fa6185c8eb 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/StakingPoolInteractor.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/StakingPoolInteractor.kt @@ -61,7 +61,7 @@ class StakingPoolInteractor( private val currentValidatorsInteractor: CurrentValidatorsInteractor ) { - @OptIn(FlowPreview::class) + @OptIn(ExperimentalCoroutinesApi::class) fun stakingStateFlow(): Flow { val currentChainFlow = stakingInteractor.selectedChainFlow().filter { it.supportStakingPool } val selectedAccountFlow = accountRepository.selectedMetaAccountFlow() diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/parachain/StakingParachainScenarioInteractor.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/parachain/StakingParachainScenarioInteractor.kt index 1dbddcf6aa..8bbd8b8cef 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/parachain/StakingParachainScenarioInteractor.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/parachain/StakingParachainScenarioInteractor.kt @@ -93,6 +93,7 @@ import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks import jp.co.soramitsu.wallet.impl.domain.model.planksFromAmount import jp.co.soramitsu.wallet.impl.domain.validation.EnoughToPayFeesValidation import jp.co.soramitsu.wallet.impl.domain.validation.assetBalanceProducer +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine @@ -144,8 +145,12 @@ class StakingParachainScenarioInteractor( "91bc6e169807aaa54802737e1c504b2577d4fafedd5a02c10293b1cd60e39527" to 2 // moonbase ) + @OptIn(ExperimentalCoroutinesApi::class) override fun stakingStateFlow(): Flow { - return stakingInteractor.selectedChainFlow().flatMapLatest { chain -> + return combine( + stakingInteractor.selectedChainFlow(), + accountRepository.selectedMetaAccountFlow() + ) { chain, metaAccount -> val availableStakingSelection = stakingSharedState.availableToSelect() val isSelectedChainAvailable = availableStakingSelection.any { it.chainId == chain.id } @@ -155,14 +160,17 @@ class StakingParachainScenarioInteractor( val chainId = with(availableStakingSelection) { firstOrNull { it.chainId == polkadotChainId } ?: first() }.chainId - availableStakingSelection.firstOrNull { it.chainId == chainId }?.let { newSelection -> - stakingSharedState.update(newSelection) - } + availableStakingSelection.firstOrNull { it.chainId == chainId } + ?.let { newSelection -> + stakingSharedState.update(newSelection) + } val availableChain = stakingInteractor.getChain(chainId) availableChain } - val accountId = accountRepository.getSelectedMetaAccount().accountId(useChain) ?: error("cannot find accountId") - stakingParachainScenarioRepository.stakingStateFlow(useChain, accountId) + val accountId = metaAccount.accountId(useChain) ?: error("cannot find accountId") + useChain to accountId + }.flatMapLatest { (chain, accountId) -> + stakingParachainScenarioRepository.stakingStateFlow(chain, accountId) } } diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioInteractor.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioInteractor.kt index 919029460e..1955070b48 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioInteractor.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioInteractor.kt @@ -1,6 +1,5 @@ package jp.co.soramitsu.staking.impl.scenarios.relaychain -import android.util.Log import java.math.BigDecimal import java.math.BigInteger import java.util.Optional @@ -47,7 +46,6 @@ import jp.co.soramitsu.staking.impl.domain.EraTimeCalculatorFactory import jp.co.soramitsu.staking.impl.domain.StakingInteractor import jp.co.soramitsu.staking.impl.domain.common.isWaiting import jp.co.soramitsu.staking.impl.domain.isNominationActive -import jp.co.soramitsu.staking.impl.domain.minimumStake import jp.co.soramitsu.staking.impl.domain.model.NetworkInfo import jp.co.soramitsu.staking.impl.domain.model.NominatorStatus import jp.co.soramitsu.staking.impl.domain.model.PendingPayout @@ -96,8 +94,8 @@ import jp.co.soramitsu.wallet.impl.domain.model.Asset import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks import jp.co.soramitsu.wallet.impl.domain.validation.EnoughToPayFeesValidation import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll @@ -111,7 +109,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext import jp.co.soramitsu.core.models.Asset as CoreAsset @@ -133,6 +130,7 @@ class StakingRelayChainScenarioInteractor( private val walletConstants: WalletConstants ) : StakingScenarioInteractor { + @OptIn(ExperimentalCoroutinesApi::class) override suspend fun observeNetworkInfoState(): Flow { return stakingSharedState.assetWithChain.filter { it.asset.staking == StakingType.RELAYCHAIN } .distinctUntilChanged() @@ -200,6 +198,7 @@ class StakingRelayChainScenarioInteractor( } } + @OptIn(ExperimentalCoroutinesApi::class) fun stakingStateFlow(chainId: ChainId): Flow { return jp.co.soramitsu.common.utils.flowOf { val chain = stakingInteractor.getChain(chainId) @@ -235,7 +234,7 @@ class StakingRelayChainScenarioInteractor( return observeStakeSummary(nominatorState) { val eraStakers = it.eraStakers.values val chainId = nominatorState.chain.id - + val utilityAsset = nominatorState.chain.utilityAsset when { isNominationActive( nominatorState.stashId, @@ -243,30 +242,34 @@ class StakingRelayChainScenarioInteractor( it.rewardedNominatorsPerValidator ) -> NominatorStatus.Active + utilityAsset != null && it.asset.bondedInPlanks.orZero() < minimumStake( + nominatorState.chain.id, + eraStakers, + stakingRelayChainScenarioRepository.minimumNominatorBond(utilityAsset) + ) -> { + NominatorStatus.Inactive(NominatorStatus.Inactive.Reason.MIN_STAKE) + } + nominatorState.nominations.isWaiting(it.activeEraIndex) -> NominatorStatus.Waiting( timeLeft = getCalculator(chainId).calculate(nominatorState.nominations.submittedInEra + ERA_OFFSET) .toLong() ) - else -> { - val utilityAsset = nominatorState.chain.utilityAsset - val inactiveReason = when { - utilityAsset != null && it.asset.bondedInPlanks.orZero() < minimumStake( - eraStakers, - stakingRelayChainScenarioRepository.minimumNominatorBond(utilityAsset) - ) -> { - NominatorStatus.Inactive.Reason.MIN_STAKE - } - - else -> NominatorStatus.Inactive.Reason.NO_ACTIVE_VALIDATOR - } - - NominatorStatus.Inactive(inactiveReason) - } + else -> NominatorStatus.Inactive(NominatorStatus.Inactive.Reason.NO_ACTIVE_VALIDATOR) } } } + suspend fun minimumStake( + chainId: ChainId, + exposures: Collection, + minimumNominatorBond: BigInteger + ): BigInteger { + val minActiveStake = stakingRelayChainScenarioRepository.minimumActiveStake(chainId) + ?: kotlin.runCatching { exposures.minOf { exposure -> exposure.others.minOf { it.value } } }.getOrNull() ?: BigInteger.ZERO + return minActiveStake.coerceAtLeast(minimumNominatorBond) + } + private fun observeStakeSummary( state: StakingState.Stash, statusResolver: suspend (StatusResolutionContext) -> S @@ -289,7 +292,6 @@ class StakingRelayChainScenarioInteractor( asset, rewardedNominatorsPerValidator ) - val status = statusResolver(statusResolutionContext) StakeSummary( status = status, diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioRepository.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioRepository.kt index caae0aecfa..69045bcbe2 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioRepository.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/scenarios/relaychain/StakingRelayChainScenarioRepository.kt @@ -1,6 +1,5 @@ package jp.co.soramitsu.staking.impl.scenarios.relaychain -import android.util.Log import java.math.BigInteger import jp.co.soramitsu.common.data.network.runtime.binding.BinderWithType import jp.co.soramitsu.common.data.network.runtime.binding.NonNullBinderWithType @@ -81,6 +80,7 @@ import kotlin.math.max import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow @@ -88,11 +88,9 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext @@ -206,6 +204,7 @@ class StakingRelayChainScenarioRepository( ) } + @OptIn(ExperimentalCoroutinesApi::class) fun legacyElectedExposuresInActiveEra(chainId: ChainId): Flow> = observeActiveEraIndex(chainId) .filter { it.isNotZero() } @@ -445,7 +444,7 @@ class StakingRelayChainScenarioRepository( val runtime = runtimeFor(chainId) return runtime.metadata.staking().storageOrNull(storageName)?.let { storageEntry -> - localStorage.query( + remoteStorage.query( keyBuilder = { storageEntry.storageKey() }, binding = { scale, _ -> scale?.let { @@ -461,6 +460,7 @@ class StakingRelayChainScenarioRepository( } } + @OptIn(ExperimentalCoroutinesApi::class) fun stakingStateFlow( chain: Chain, chainAsset: Asset, @@ -612,7 +612,7 @@ class StakingRelayChainScenarioRepository( return getLegacyElectedValidatorsExposure(chainId, era) } - private suspend fun isLegacyErasStakersSchema(chainId: ChainId): Boolean { + suspend fun isLegacyErasStakersSchema(chainId: ChainId): Boolean { val runtime = chainRegistry.getRuntime(chainId) return runtime.metadata.stakingOrNull()?.storageOrNull("ErasStakersPaged") == null } diff --git a/feature-staking-impl/src/main/res/layout/fragment_bond_more.xml b/feature-staking-impl/src/main/res/layout/fragment_bond_more.xml index 9fcba2e342..8123a03d81 100644 --- a/feature-staking-impl/src/main/res/layout/fragment_bond_more.xml +++ b/feature-staking-impl/src/main/res/layout/fragment_bond_more.xml @@ -6,7 +6,8 @@ android:layout_height="match_parent" android:background="@drawable/drawable_background_image" android:backgroundTint="#63000000" - android:backgroundTintMode="src_atop"> + android:backgroundTintMode="src_atop" + android:fitsSystemWindows="true"> + android:backgroundTintMode="src_atop" + android:fitsSystemWindows="true"> , val explorer: Pair? ) { companion object { const val CODE_HASH_CLICK = 2 - val default = SuccessViewState("", emptyList(), null) + val default = SuccessViewState("", "", emptyList(), null) } } @@ -95,7 +96,7 @@ fun SuccessContent( ) MarginVertical(margin = 16.dp) H2( - text = stringResource(id = R.string.all_done), + text = state.title, modifier = Modifier.align(Alignment.CenterHorizontally) ) MarginVertical(margin = 8.dp) @@ -152,7 +153,8 @@ fun SuccessContent( @Composable private fun SuccessPreview() { val state = SuccessViewState( - message = "You can now back to your app and do that you're usually do", + title = "Title HERE", + message = "Your transaction has been successfully sent to blockchain", tableItems = listOf( TitleValueViewState( title = "Hash", diff --git a/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessFragment.kt b/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessFragment.kt index c1ae8792f0..4cd85d75d0 100644 --- a/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessFragment.kt +++ b/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessFragment.kt @@ -31,17 +31,18 @@ class SuccessFragment : BaseComposeBottomSheetDialogFragment() const val KEY_OPERATION_HASH = "KEY_OPERATION_HASH" const val KEY_CHAIN_ID = "KEY_CHAIN_ID" const val KEY_CUSTOM_MESSAGE = "KEY_CUSTOM_MESSAGE" - const val KEY_HAS_SUCCESS_RESULT = "KEY_HAS_SUCCESS_RESULT" + const val KEY_CUSTOM_TITLE = "KEY_CUSTOM_TITLE" fun getBundle( operationHash: String?, chainId: ChainId?, customMessage: String?, - hasSuccessResult: Boolean = true + customTitle: String? = null ) = bundleOf( KEY_OPERATION_HASH to operationHash, KEY_CHAIN_ID to chainId, - KEY_CUSTOM_MESSAGE to customMessage + KEY_CUSTOM_MESSAGE to customMessage, + KEY_CUSTOM_TITLE to customTitle ) } diff --git a/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessViewModel.kt b/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessViewModel.kt index 7f6f01598f..42931011f5 100644 --- a/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessViewModel.kt +++ b/feature-success-impl/src/main/kotlin/jp/co/soramitsu/success/presentation/SuccessViewModel.kt @@ -44,7 +44,7 @@ class SuccessViewModel @Inject constructor( val operationHash = savedStateHandle.get(SuccessFragment.KEY_OPERATION_HASH) val chainId = savedStateHandle.get(SuccessFragment.KEY_CHAIN_ID) private val customMessage: String? = savedStateHandle[SuccessFragment.KEY_CUSTOM_MESSAGE] - private val hasSuccessResult: Boolean = savedStateHandle[SuccessFragment.KEY_HAS_SUCCESS_RESULT] ?: true + private val customTitle: String? = savedStateHandle[SuccessFragment.KEY_CUSTOM_TITLE] private val _showHashActions = MutableLiveData>() val showHashActions: LiveData> = _showHashActions @@ -82,7 +82,8 @@ class SuccessViewModel @Inject constructor( val state: StateFlow = explorerPairFlow.map { explorer -> SuccessViewState( - message = customMessage ?: resourceManager.getString(R.string.return_to_app_message), + title = customTitle ?: resourceManager.getString(R.string.common_transaction_sent), + message = customMessage ?: resourceManager.getString(R.string.success_message_transaction_sent), tableItems = getInfoTableItems(), explorer = explorer ) diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCache.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCache.kt index 888abc2acb..a40a01db96 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCache.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCache.kt @@ -4,6 +4,7 @@ import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.common.domain.SelectedFiat import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.mixin.api.UpdatesProviderUi +import jp.co.soramitsu.common.utils.isZero import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.coredb.dao.AssetDao import jp.co.soramitsu.coredb.dao.AssetReadOnlyCache @@ -13,6 +14,7 @@ import jp.co.soramitsu.coredb.model.AssetLocal import jp.co.soramitsu.coredb.model.AssetUpdateItem import jp.co.soramitsu.coredb.model.TokenPriceLocal import jp.co.soramitsu.shared_utils.runtime.AccountId +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -23,7 +25,8 @@ class AssetCache( private val accountRepository: AccountRepository, private val assetDao: AssetDao, private val updatesMixin: UpdatesMixin, - private val selectedFiat: SelectedFiat + private val selectedFiat: SelectedFiat, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : AssetReadOnlyCache by assetDao, UpdatesProviderUi by updatesMixin { @@ -34,7 +37,7 @@ class AssetCache( accountId: AccountId, chainAsset: Asset, builder: (local: AssetLocal) -> AssetLocal - ) = withContext(Dispatchers.IO) { + ) = withContext(dispatcher) { val chainId = chainAsset.chainId val assetId = chainAsset.id val shouldUseChainlinkForRates = selectedFiat.isUsd() && chainAsset.priceProvider?.id != null @@ -49,21 +52,15 @@ class AssetCache( val cachedAsset = assetDao.getAsset(metaId, accountId, chainId, assetId)?.asset when { - cachedAsset == null -> assetDao.insertAsset( - builder.invoke( - AssetLocal.createEmpty( - accountId, - assetId, - chainId, - metaId, - priceId - ) - ) - ) + cachedAsset == null -> { + val emptyAsset = AssetLocal.createEmpty(accountId, assetId, chainId, metaId, priceId) + val newAsset = builder.invoke(emptyAsset) + assetDao.insertAsset(newAsset.copy(enabled = newAsset.freeInPlanks == null || newAsset.freeInPlanks.isZero())) + } cachedAsset.accountId.contentEquals(emptyAccountIdValue) -> { assetDao.deleteAsset(metaId, emptyAccountIdValue, chainId, assetId) - assetDao.insertAsset(builder.invoke(cachedAsset.copy(accountId = accountId, tokenPriceId = priceId))) + assetDao.insertAsset(builder.invoke(cachedAsset.copy(accountId = accountId, tokenPriceId = priceId, enabled = cachedAsset.freeInPlanks == null || cachedAsset.freeInPlanks.isZero()))) } else -> { @@ -91,7 +88,7 @@ class AssetCache( accountId: AccountId, chainAsset: Asset, builder: (local: AssetLocal) -> AssetLocal - ) = withContext(Dispatchers.IO) { + ) = withContext(dispatcher) { val applicableMetaAccount = accountRepository.findMetaAccount(accountId) applicableMetaAccount?.let { @@ -101,14 +98,14 @@ class AssetCache( suspend fun updateTokensPrice( update: List - ) = withContext(Dispatchers.IO) { + ) = withContext(dispatcher) { tokenPriceDao.insertTokensPrice(update) } suspend fun updateTokenPrice( priceId: String, builder: (local: TokenPriceLocal) -> TokenPriceLocal - ) = withContext(Dispatchers.IO) { + ) = withContext(dispatcher) { val tokenPriceLocal = tokenPriceDao.getTokenPrice(priceId) ?: TokenPriceLocal.createEmpty(priceId) val newToken = builder.invoke(tokenPriceLocal) diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCacheExt.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCacheExt.kt index a68fa98d34..88727460df 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCacheExt.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/data/cache/AssetCacheExt.kt @@ -111,7 +111,7 @@ fun bindOrmlTokensAccountDataOrDefault(hex: String?, runtime: RuntimeSnapshot): } fun bindEquilibriumAccountData(hex: String?, runtime: RuntimeSnapshot): EqAccountInfo? { - return hex?.let { bindEquilibriumAccountInfo(it, runtime) } + return hex?.let { runCatching { bindEquilibriumAccountInfo(it, runtime) }.getOrNull() } } fun bindAssetsAccountData(hex: String?, runtime: RuntimeSnapshot): AssetsAccountInfo? { diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseConfirmViewModel.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseConfirmViewModel.kt index 44f7c761b4..859f322a51 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseConfirmViewModel.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseConfirmViewModel.kt @@ -23,6 +23,7 @@ import jp.co.soramitsu.common.validation.WaitForFeeCalculationException import jp.co.soramitsu.feature_wallet_api.R import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import jp.co.soramitsu.wallet.impl.domain.model.Asset import jp.co.soramitsu.wallet.impl.domain.model.amountFromPlanks import jp.co.soramitsu.wallet.impl.domain.model.planksFromAmount @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch abstract class BaseConfirmViewModel( + walletInteractor: WalletInteractor, private val existentialDepositUseCase: ExistentialDepositUseCase, private val resourceManager: ResourceManager, protected val asset: Asset, @@ -56,6 +58,7 @@ abstract class BaseConfirmViewModel( private val amount = amountInPlanks?.let { asset.token.amountFromPlanks(it) } private val amountFormatted = amount?.formatCryptoDetail(asset.token.configuration.symbol) private val amountFiat = amount?.applyFiatRate(asset.token.fiatRate)?.formatFiat(asset.token.fiatSymbol) + private val assetFlow = walletInteractor.assetFlow(chain.id, asset.token.configuration.id).stateIn(viewModelScope, SharingStarted.Eagerly, asset) private val toolbarViewState = ToolbarViewState( resourceManager.getString(R.string.common_confirm), @@ -148,7 +151,7 @@ abstract class BaseConfirmViewModel( val chargesAmount = amountInPlanks.orZero() + fee val existentialDeposit = existentialDepositUseCase(asset.token.configuration) - val resultBalance = asset.transferableInPlanks - chargesAmount + val resultBalance = assetFlow.value.transferableInPlanks - chargesAmount if (resultBalance < existentialDeposit || resultBalance <= BigInteger.ZERO) { return Result.failure(FeeInsufficientBalanceException(resourceManager)) } diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseEnterAmountViewModel.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseEnterAmountViewModel.kt index ce67028afe..583035f143 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseEnterAmountViewModel.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/BaseEnterAmountViewModel.kt @@ -76,7 +76,11 @@ open class BaseEnterAmountViewModel( private val amountInputViewState: Flow = enteredAmountFlow.map { amount -> val tokenBalance = availableAmountForOperation(asset).formatCrypto(asset.token.configuration.symbol) val tokenBalanceFiat = availableAmountForOperation(asset).applyFiatRate(asset.token.fiatRate)?.formatFiat(asset.token.fiatSymbol) - val balanceWithFiat = tokenBalance + tokenBalanceFiat?.let { " ($it)" } + val balanceWithFiat = if (tokenBalanceFiat == null) { + tokenBalance + } else { + "$tokenBalance ($tokenBalanceFiat)" + } val fiatAmount = amount.applyFiatRate(asset.token.fiatRate)?.formatFiat(asset.token.fiatSymbol) AmountInputViewState( diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt index 501f675e7c..c5f4120868 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt @@ -9,6 +9,8 @@ import jp.co.soramitsu.common.data.model.CursorPage import jp.co.soramitsu.common.data.network.runtime.binding.EqAccountInfo import jp.co.soramitsu.common.data.network.runtime.binding.EqOraclePricePoint import jp.co.soramitsu.common.data.secrets.v2.MetaAccountSecrets +import jp.co.soramitsu.common.domain.model.NetworkIssueType +import jp.co.soramitsu.common.model.AssetBooleanState import jp.co.soramitsu.core.models.ChainId import jp.co.soramitsu.coredb.model.AddressBookContact import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain @@ -106,6 +108,8 @@ interface WalletInteractor { suspend fun markAssetAsHidden(chainId: ChainId, chainAssetId: String) + suspend fun updateAssetsHiddenState(state: List) + suspend fun markAssetAsShown(chainId: ChainId, chainAssetId: String) fun selectedMetaAccountFlow(): Flow @@ -133,10 +137,6 @@ interface WalletInteractor { fun decreaseSoraCardHiddenSessions() fun hideSoraCard() - fun observeHideZeroBalanceEnabledForCurrentWallet(): Flow - suspend fun toggleHideZeroBalancesForCurrentWallet() - suspend fun getHideZeroBalancesForCurrentWallet(): Boolean - suspend fun checkControllerDeprecations(): List suspend fun canUseAsset(chainId: String, chainAssetId: String): Boolean @@ -156,4 +156,11 @@ interface WalletInteractor { suspend fun estimateClaimRewardsFee(chainId: ChainId): BigInteger suspend fun getVestingLockedAmount(chainId: ChainId): BigInteger? suspend fun claimRewards(chainId: ChainId): Result + + fun getAssetManagementIntroPassed(): Boolean + suspend fun saveAssetManagementIntroPassed() + fun networkIssuesFlow(): Flow> + suspend fun retryChainSync(chainId: ChainId): Result + + fun observeCurrentAccountChainsPerAsset(assetId: String): Flow> } diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt index 4beed68f62..6cef5f3930 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt @@ -7,7 +7,6 @@ import jp.co.soramitsu.common.data.network.config.AppConfigRemote import jp.co.soramitsu.common.data.network.runtime.binding.EqAccountInfo import jp.co.soramitsu.common.data.network.runtime.binding.EqOraclePricePoint import jp.co.soramitsu.core.models.IChain -import jp.co.soramitsu.coredb.model.AssetLocal import jp.co.soramitsu.coredb.model.AssetUpdateItem import jp.co.soramitsu.coredb.model.PhishingLocal import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain @@ -44,13 +43,6 @@ interface WalletRepository { minSupportedVersion: String? ): Asset? - suspend fun updateAssetHidden( - metaId: Long, - accountId: AccountId, - isHidden: Boolean, - chainAsset: CoreAsset - ) - suspend fun getTransferFee( chain: Chain, transfer: Transfer, @@ -99,8 +91,6 @@ interface WalletRepository { suspend fun getRemoteConfig(): Result - fun chainRegistrySyncUp() - suspend fun getSingleAssetPriceCoingecko(priceId: String, currency: String): BigDecimal? suspend fun getControllerAccount(chainId: ChainId, accountId: AccountId): AccountId? suspend fun getStashAccount(chainId: ChainId, accountId: AccountId): AccountId? @@ -116,4 +106,5 @@ interface WalletRepository { suspend fun getVestingLockedAmount(chainId: ChainId): BigInteger? suspend fun estimateClaimRewardsFee(chainId: ChainId): BigInteger suspend fun claimRewards(chain: IChain, accountId: AccountId): Result + suspend fun updateAssetsHidden(state: List) } diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/model/Asset.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/model/Asset.kt index a0ebbc1dab..8c10fd5e31 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/model/Asset.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/model/Asset.kt @@ -6,7 +6,9 @@ import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.common.model.AssetKey import jp.co.soramitsu.common.utils.applyFiatRate import jp.co.soramitsu.common.utils.formatFiat +import jp.co.soramitsu.common.utils.lessThan import jp.co.soramitsu.common.utils.orZero +import jp.co.soramitsu.common.utils.positiveOrNull import jp.co.soramitsu.core.utils.utilityAsset import jp.co.soramitsu.shared_utils.runtime.AccountId import jp.co.soramitsu.core.models.Asset as CoreAsset @@ -70,7 +72,7 @@ data class Asset( ) } - private val free = token.amountFromPlanks(freeInPlanks.orZero()) + private val free = token.amountFromPlanks(freeInPlanks.positiveOrNull().orZero()) val reserved = token.amountFromPlanks(reservedInPlanks.orZero()) private val miscFrozen = token.amountFromPlanks(miscFrozenInPlanks.orZero()) private val feeFrozen = token.amountFromPlanks(feeFrozenInPlanks.orZero()) @@ -82,7 +84,7 @@ data class Asset( val availableForStaking: BigDecimal = maxOf(free - frozen, BigDecimal.ZERO) val transferable = free - locked - val transferableInPlanks = freeInPlanks?.let { it - miscFrozenInPlanks.orZero().max(feeFrozenInPlanks.orZero()) }.orZero() + val transferableInPlanks = freeInPlanks.positiveOrNull()?.let { it - miscFrozenInPlanks.orZero().max(feeFrozenInPlanks.orZero()) }.orZero() val isAssetFrozen = status == STATUS_FROZEN val sendAvailable: BigDecimal = if (isAssetFrozen) BigDecimal.ZERO else transferable @@ -103,4 +105,10 @@ data class Asset( fun calculateTotalBalance( freeInPlanks: BigInteger?, reservedInPlanks: BigInteger? -) = freeInPlanks?.let { freeInPlanks + reservedInPlanks.orZero() } +): BigInteger? { + return if(freeInPlanks != null && freeInPlanks.lessThan(BigInteger.ZERO)) { + BigInteger.ZERO + } else { + freeInPlanks?.let { freeInPlanks + reservedInPlanks.orZero() } + } +} diff --git a/feature-wallet-impl/build.gradle b/feature-wallet-impl/build.gradle index ca2cd08318..f7c00c4f73 100644 --- a/feature-wallet-impl/build.gradle +++ b/feature-wallet-impl/build.gradle @@ -22,11 +22,6 @@ android { buildConfigField "String", "MOONPAY_HOST", "\"buy-staging.moonpay.com\"" buildConfigField "String", "MOONPAY_PUBLIC_KEY", "\"pk_test_DMRuyL6Nf1qc9OzjPBmCFBeCGkFwiZs0\"" - buildConfigField "String", "SORA_CONFIG_COMMON_STAGE", "\"https://config.polkaswap2.io/stage/common.json\"" - buildConfigField "String", "SORA_CONFIG_MOBILE_STAGE", "\"https://config.polkaswap2.io/stage/mobile.json\"" - buildConfigField "String", "SORA_CONFIG_COMMON_PROD", "\"https://config.polkaswap2.io/prod/common.json\"" - buildConfigField "String", "SORA_CONFIG_MOBILE_PROD", "\"https://config.polkaswap2.io/prod/mobile.json\"" - buildConfigField "String", "SCAM_DETECTION_CONFIG", "\"https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/scamDetection/Polkadot_Hot_Wallet_Attributions.csv\"" } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt index c560d874a7..95abfdc8f0 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt @@ -5,22 +5,17 @@ import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.wallet.impl.data.network.subquery.OperationsHistoryApi import jp.co.soramitsu.xnetworking.basic.networkclient.SoramitsuNetworkClient import jp.co.soramitsu.xnetworking.fearlesswallet.txhistory.client.TxHistoryClientForFearlessWalletFactory -import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigBuilder class HistorySourceProvider( private val walletOperationsApi: OperationsHistoryApi, private val chainRegistry: ChainRegistry, private val soramitsuNetworkClient: SoramitsuNetworkClient, - private val soraTxHistoryFactory: TxHistoryClientForFearlessWalletFactory, - private val soraProdRemoteConfigBuilder: SoraRemoteConfigBuilder, - private val soraStageRemoteConfigBuilder: SoraRemoteConfigBuilder + private val soraTxHistoryFactory: TxHistoryClientForFearlessWalletFactory ) { operator fun invoke(historyUrl: String, historyType: Chain.ExternalApi.Section.Type): HistorySource? { return when (historyType) { Chain.ExternalApi.Section.Type.SUBQUERY -> SubqueryHistorySource(walletOperationsApi, chainRegistry, historyUrl) - Chain.ExternalApi.Section.Type.SORA -> { - SoraHistorySource(soramitsuNetworkClient, soraTxHistoryFactory, soraProdRemoteConfigBuilder, soraStageRemoteConfigBuilder) - } + Chain.ExternalApi.Section.Type.SORA -> SoraHistorySource(soramitsuNetworkClient, soraTxHistoryFactory) Chain.ExternalApi.Section.Type.SUBSQUID -> SubsquidHistorySource(walletOperationsApi, historyUrl) Chain.ExternalApi.Section.Type.GIANTSQUID -> GiantsquidHistorySource(walletOperationsApi, historyUrl) Chain.ExternalApi.Section.Type.ETHERSCAN -> EtherscanHistorySource(walletOperationsApi, historyUrl) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt index 2987cfa2c3..995de35b4f 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt @@ -10,13 +10,10 @@ import jp.co.soramitsu.wallet.impl.domain.model.Operation import jp.co.soramitsu.xnetworking.basic.networkclient.SoramitsuNetworkClient import jp.co.soramitsu.xnetworking.basic.txhistory.TxHistoryItem import jp.co.soramitsu.xnetworking.fearlesswallet.txhistory.client.TxHistoryClientForFearlessWalletFactory -import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigBuilder class SoraHistorySource( soramitsuNetworkClient: SoramitsuNetworkClient, - soraTxHistoryFactory: TxHistoryClientForFearlessWalletFactory, - private val soraProdRemoteConfigBuilder: SoraRemoteConfigBuilder, - private val soraStageRemoteConfigBuilder: SoraRemoteConfigBuilder + soraTxHistoryFactory: TxHistoryClientForFearlessWalletFactory ) : HistorySource { private val client = soraTxHistoryFactory.createSubSquid(soramitsuNetworkClient, 100) @@ -44,16 +41,17 @@ class SoraHistorySource( }.getOrNull() val soraHistoryItems: List = soraHistory?.items.orEmpty() - val soraOperations = runCatching { - soraHistoryItems.mapNotNull { - it.toOperation( - chain, - chainAsset, - accountAddress, - filters - ) + val soraOperations = + soraHistoryItems.mapNotNull { item -> + runCatching { + item.toOperation( + chain, + chainAsset, + accountAddress, + filters + ) + }.getOrNull() } - }.getOrNull() ?: emptyList() val nextCursor = if (soraHistory?.endReached == true) null else page.inc().toString() return CursorPage(nextCursor, soraOperations) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/mappers/OperationMappers.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/mappers/OperationMappers.kt index df0c45ef97..0fbefe5770 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/mappers/OperationMappers.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/mappers/OperationMappers.kt @@ -1,5 +1,6 @@ package jp.co.soramitsu.wallet.impl.data.mappers +import java.math.BigDecimal import java.math.BigInteger import jp.co.soramitsu.account.api.presentation.account.AddressDisplayUseCase import jp.co.soramitsu.common.address.AddressIconGenerator @@ -212,7 +213,7 @@ fun TxHistoryItem.toOperation( hash = id, myAddress = data?.firstOrNull { it.paramName == "from" }?.paramValue.orEmpty(), amount = chainAsset.planksFromAmount( - data?.firstOrNull { it.paramName == "amount" }?.paramValue?.toBigDecimal() + data?.firstOrNull { it.paramName == "amount" }?.paramValue?.toBigDecimalOrNull() .orZero() ), receiver = data?.firstOrNull { it.paramName == "to" }?.paramValue.orEmpty(), @@ -231,16 +232,16 @@ fun TxHistoryItem.toOperation( val baseAsset = chain.assets.firstOrNull { it.currencyId == baseCurrencyId } ?: return null val baseAssetAmount = - data?.firstOrNull { it.paramName == "baseAssetAmount" }?.paramValue?.toBigDecimal() + data?.firstOrNull { it.paramName == "baseAssetAmount" }?.paramValue?.toBigDecimalOrNull() .orZero() val targetAsset = chain.assets.firstOrNull { it.currencyId == targetCurrencyId } val targetAssetAmount = - data?.firstOrNull { it.paramName == "targetAssetAmount" }?.paramValue?.toBigDecimal() + data?.firstOrNull { it.paramName == "targetAssetAmount" }?.paramValue?.toBigDecimalOrNull() .orZero() val liquidityProviderFee = - data?.firstOrNull { it.paramName == "liquidityProviderFee" }?.paramValue?.toBigDecimal() + data?.firstOrNull { it.paramName == "liquidityProviderFee" }?.paramValue?.toBigDecimalOrNull() .orZero() Operation( @@ -271,7 +272,7 @@ fun TxHistoryItem.toOperation( chainAsset = chainAsset, type = Operation.Type.Reward( amount = chainAsset.planksFromAmount( - data?.firstOrNull { it.paramName == "amount" }?.paramValue?.toBigDecimal() + data?.firstOrNull { it.paramName == "amount" }?.paramValue?.toBigDecimalOrNull() .orZero() ), isReward = true, @@ -580,3 +581,7 @@ fun mapOperationToParcel( } } } + +fun String.toBigDecimalOrNull(): BigDecimal? { + return runCatching { toBigDecimal() }.getOrNull() +} \ No newline at end of file diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/EthereumRemoteSource.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/EthereumRemoteSource.kt index 829416cd80..3e7650249d 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/EthereumRemoteSource.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/EthereumRemoteSource.kt @@ -1,5 +1,6 @@ package jp.co.soramitsu.wallet.impl.data.network.blockchain +import android.util.Log import java.math.BigInteger import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.account.api.domain.model.address @@ -19,8 +20,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.withContext @@ -56,10 +59,10 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio chain: Chain, accountId: AccountId ): Result { - val connection = ethereumConnectionPool.get(chain.id) + val connection = ethereumConnectionPool.await(chain.id) return kotlin.runCatching { - connection?.web3j!!.fetchEthBalance( + connection.web3j!!.fetchEthBalance( chainAsset, accountId.toHexString(true) ) @@ -72,7 +75,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio privateKey: String ): Result = withContext(Dispatchers.IO) { - val connection = ethereumConnectionPool.get(chain.id) + val connection = ethereumConnectionPool.await(chain.id) ?: return@withContext Result.failure("There is no connection created for chain ${chain.name}, ${chain.id}") val web3 = connection.web3j ?: return@withContext Result.failure("There is no connection established for chain ${chain.name}, ${chain.id}") @@ -99,7 +102,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio chain: Chain, account: MetaAccount ): Result>>> { - val connection = ethereumConnectionPool.get(chain.id) + val connection = ethereumConnectionPool.await(chain.id) val web3 = connection?.web3j ?: return Result.failure("There is no connection created for chain ${chain.name}, ${chain.id}") @@ -134,7 +137,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio asset: Asset, address: String ): BigInteger { - val connection = ethereumConnectionPool.get(asset.chainId) + val connection = ethereumConnectionPool.await(asset.chainId) val web3 = connection?.web3j ?: throw RuntimeException("There is no connection created for chain ${asset.chainId}") @@ -145,7 +148,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio chainId: ChainId, receiverAddress: String ): BigInteger? { - val connection = ethereumConnectionPool.get(chainId) + val connection = ethereumConnectionPool.await(chainId) val web3 = connection?.web3j ?: throw RuntimeException("There is no connection created for chain ${chainId}") @@ -222,7 +225,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio } fun listenGas(transfer: Transfer, chain: Chain): Flow { - val connection = requireNotNull(ethereumConnectionPool.get(chain.id)) + val connection = requireNotNull(ethereumConnectionPool.getOrNull(chain.id)) val web3j = requireNotNull(connection.web3j) val wsService = requireNotNull(connection.service) val transactionBuilder = EthereumTransactionBuilder(connection) @@ -268,7 +271,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio raw: RawTransaction, privateKey: String ): Result = withContext(Dispatchers.IO) { - val connection = ethereumConnectionPool.get(chainId) + val connection = ethereumConnectionPool.await(chainId) ?: return@withContext Result.failure("There is no connection created for chain with id = $chainId") val web3 = connection.web3j ?: return@withContext Result.failure("There is no connection established for chain with id = $chainId") @@ -287,7 +290,7 @@ class EthereumRemoteSource(private val ethereumConnectionPool: EthereumConnectio raw: RawTransaction, privateKey: String ): Result = withContext(Dispatchers.IO) { - val connection = ethereumConnectionPool.get(chainId) + val connection = ethereumConnectionPool.await(chainId) ?: return@withContext Result.failure("There is no connection created for chain with id = $chainId") val web3 = connection.web3j diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/updaters/BalancesUpdateSystem.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/updaters/BalancesUpdateSystem.kt index ec2c5354c5..2fbb431cd5 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/updaters/BalancesUpdateSystem.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/blockchain/updaters/BalancesUpdateSystem.kt @@ -2,22 +2,18 @@ package jp.co.soramitsu.wallet.impl.data.network.blockchain.updaters import android.annotation.SuppressLint import android.util.Log -import it.airgap.beaconsdk.core.internal.utils.failure -import it.airgap.beaconsdk.core.internal.utils.onEachFailure -import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.account.api.domain.model.accountId import jp.co.soramitsu.account.api.domain.model.address import jp.co.soramitsu.account.impl.data.mappers.mapMetaAccountLocalToMetaAccount -import jp.co.soramitsu.common.data.network.rpc.BulkRetriever +import jp.co.soramitsu.account.impl.domain.buildStorageKeys +import jp.co.soramitsu.account.impl.domain.handleBalanceResponse import jp.co.soramitsu.common.data.network.runtime.binding.ExtrinsicStatusEvent import jp.co.soramitsu.common.data.network.runtime.binding.SimpleBalanceData -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin import jp.co.soramitsu.common.utils.failure import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.common.utils.requireException import jp.co.soramitsu.common.utils.requireValue -import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.core.models.ChainAssetType import jp.co.soramitsu.core.updater.UpdateSystem import jp.co.soramitsu.core.updater.Updater @@ -28,12 +24,8 @@ import jp.co.soramitsu.coredb.model.OperationLocal import jp.co.soramitsu.runtime.ext.addressOf import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain -import jp.co.soramitsu.runtime.multiNetwork.getSocket -import jp.co.soramitsu.runtime.multiNetwork.getSocketOrNull -import jp.co.soramitsu.runtime.multiNetwork.toSyncIssue import jp.co.soramitsu.runtime.network.subscriptionFlowCatching import jp.co.soramitsu.shared_utils.runtime.AccountId -import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.RuntimeRequest import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.storage.storageChange import jp.co.soramitsu.wallet.api.data.cache.AssetCache @@ -43,8 +35,6 @@ import jp.co.soramitsu.wallet.impl.data.mappers.mapOperationStatusToOperationLoc import jp.co.soramitsu.wallet.impl.data.network.blockchain.EthereumRemoteSource import jp.co.soramitsu.wallet.impl.data.network.blockchain.SubstrateRemoteSource import jp.co.soramitsu.wallet.impl.data.network.blockchain.bindings.TransferExtrinsic -import jp.co.soramitsu.wallet.impl.data.network.model.constructBalanceKey -import jp.co.soramitsu.wallet.impl.data.network.model.handleBalanceResponse import jp.co.soramitsu.wallet.impl.domain.model.Operation import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -55,14 +45,12 @@ import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -72,13 +60,10 @@ private const val RUNTIME_AWAITING_TIMEOUT = 10_000L @SuppressLint("LogNotTimber") class BalancesUpdateSystem( private val chainRegistry: ChainRegistry, - private val accountRepository: AccountRepository, private val metaAccountDao: MetaAccountDao, - private val bulkRetriever: BulkRetriever, private val assetCache: AssetCache, private val substrateSource: SubstrateRemoteSource, private val operationDao: OperationDao, - private val networkStateMixin: NetworkStateMixin, private val ethereumRemoteSource: EthereumRemoteSource ) : UpdateSystem { @@ -107,11 +92,8 @@ class BalancesUpdateSystem( listenEthereumBalancesByTrigger(chain, metaAccount).launchIn(scope) } else { if (!chain.isEthereumBased || metaAccount.ethereumPublicKey != null) { - subscribeChainBalances(chain, metaAccount).onEachFailure { - logError( - chain, - it - ) + subscribeChainBalances(chain, metaAccount).onEach { result -> + result.onFailure { logError(chain, it) } }.launchIn(scope) } } @@ -130,7 +112,7 @@ class BalancesUpdateSystem( val specificChainTriggered = triggeredChainId != null val currentChainTriggered = triggeredChainId == chain.id - if (specificChainTriggered && currentChainTriggered.not()) return@map Result.failure() + if (specificChainTriggered && currentChainTriggered.not()) return@map Result.success(Unit) kotlin.runCatching { fetchEthereumBalances(chain, listOf(metaAccount)) } } @@ -146,11 +128,9 @@ class BalancesUpdateSystem( ?.observeWithTimeout(RUNTIME_AWAITING_TIMEOUT) ?.flatMapLatest { runtimeResult -> if (runtimeResult.isFailure) { - networkStateMixin.notifyChainSyncProblem(chain.toSyncIssue()) return@flatMapLatest flowOf(runtimeResult) } val runtime = runtimeResult.requireValue() - networkStateMixin.notifyChainSyncSuccess(chain.id) val storageKeys = buildStorageKeys( @@ -161,12 +141,13 @@ class BalancesUpdateSystem( .getOrNull() ?: return@flatMapLatest flowOf(Result.failure(RuntimeException("Can't build storage keys for meta account ${metaAccount.name}, chain: ${chain.name}"))) - val socketService = runCatching { chainRegistry.getSocketOrNull(chain.id) } - .onFailure { return@flatMapLatest flowOf(Result.failure(it)) } - .getOrNull() - ?: return@flatMapLatest flowOf(Result.failure(RuntimeException("Error getting socket for chain ${chain.name}"))) + val socketService = + runCatching { chainRegistry.awaitConnection(chain.id).socketService } + .onFailure { return@flatMapLatest flowOf(Result.failure(it)) } + .getOrNull() + ?: return@flatMapLatest flowOf(Result.failure(RuntimeException("Error getting socket for chain ${chain.name}"))) - val request = SubscribeBalanceRequest(storageKeys.map { it.key }) + val request = SubscribeBalanceRequest(storageKeys.mapNotNull { it.key }) combine(socketService.subscriptionFlowCatching(request)) { subscriptionsChangeResults -> subscriptionsChangeResults.forEach { subscriptionChangeResult -> @@ -187,13 +168,13 @@ class BalancesUpdateSystem( val eqHexRaw = storageKeyToHexRaw.second val balanceData = bindEquilibriumAccountData(eqHexRaw, runtime) val balances = balanceData?.data?.balances.orEmpty() - chain.assets.forEach { asset -> + chain.assets.forEach asset@{ asset -> val balance = balances.getOrDefault( asset.currencyId?.toBigInteger().orZero(), null ).orZero() val metadata = storageKeys.firstOrNull { it.key == storageKeyToHexRaw.first } - ?: return@forEach + ?: return@asset assetCache.updateAsset( metadata.metaAccountId, metadata.accountId, @@ -233,143 +214,6 @@ class BalancesUpdateSystem( return chainUpdateFlow } - private val ethBalancesFlow = combine(chainRegistry.syncedChains, - accountRepository.allMetaAccountsFlow().filterNot { it.isEmpty() }) { chains, accounts -> - val filtered = chains.filter { it.isEthereumChain } - coroutineScope { - filtered.forEach { chain -> - launch { - fetchEthereumBalances(chain, accounts) - } - } - } - } - - private val substrateBalancesFlow = combine(chainRegistry.syncedChains, - accountRepository.allMetaAccountsFlow().filterNot { it.isEmpty() }) { chains, accounts -> - coroutineScope { - val filtered = chains.filterNot { it.isEthereumChain } - filtered.forEach { chain -> - launch { - runCatching { - val runtime = - runCatching { chainRegistry.getRuntimeOrNull(chain.id) }.getOrNull() - ?: return@launch - val socketService = - runCatching { chainRegistry.getSocket(chain.id) }.getOrNull() - ?: return@launch - val storageKeys = - accounts.mapNotNull { metaAccount -> - buildStorageKeys(chain, metaAccount, runtime) - .onFailure { } - .getOrNull()?.toList() - }.flatten() - val queryResults = withContext(Dispatchers.IO) { - bulkRetriever.queryKeys( - socketService, - storageKeys.map { it.key } - ) - } - - storageKeys.map { keyWithMetadata -> - val hexRaw = - queryResults[keyWithMetadata.key] - - val balanceData = handleBalanceResponse( - runtime, - keyWithMetadata.asset, - hexRaw - ).onFailure { logError(chain, it) } - - assetCache.updateAsset( - keyWithMetadata.metaAccountId, - keyWithMetadata.accountId, - keyWithMetadata.asset, - balanceData.getOrNull() - ) - } - } - .onFailure { - logError(chain, it) - return@launch - } - } - } - } - } - - private fun singleUpdateFlow(): Flow { - return combine( - ethBalancesFlow, - substrateBalancesFlow - ) { _, _ -> - }.onStart { emit(Unit) }.flowOn(Dispatchers.Default) - } - - private fun buildStorageKeys( - chain: Chain, - metaAccount: MetaAccount, - runtime: RuntimeSnapshot - ): Result> { - val accountId = metaAccount.accountId(chain) - ?: return Result.failure(RuntimeException("Can't get account id for meta account ${metaAccount.name}, chain: ${chain.name}")) - - if (chain.utilityAsset != null && chain.utilityAsset?.typeExtra == ChainAssetType.Equilibrium) { - val equilibriumStorageKeys = listOf( - constructBalanceKey( - runtime, - requireNotNull(chain.utilityAsset), - accountId - )?.let { - StorageKeyWithMetadata( - requireNotNull(chain.utilityAsset), - metaAccount.id, - accountId, - it - ) - }) - return Result.success(equilibriumStorageKeys.filterNotNull()) - } - - val storageKeys = chain.assets.map { asset -> - constructBalanceKey(runtime, asset, accountId)?.let { - StorageKeyWithMetadata(asset, metaAccount.id, accountId, it) - } - } - return Result.success(storageKeys.filterNotNull()) - } - - data class StorageKeyWithMetadata( - val asset: Asset, - val metaAccountId: Long, - val accountId: AccountId, - val key: String - ) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as StorageKeyWithMetadata - - if (asset != other.asset) return false - if (metaAccountId != other.metaAccountId) return false - if (!accountId.contentEquals(other.accountId)) return false - - return true - } - - override fun hashCode(): Int { - var result = asset.hashCode() - result = 31 * result + metaAccountId.hashCode() - result = 31 * result + accountId.contentHashCode() - return result - } - - override fun toString(): String { - return "StorageKeyWithMetadata(asset=${asset.name}, metaAccountId=$metaAccountId, key='$key')" - } - } - private suspend fun fetchEthereumBalances( chain: Chain, accounts: List @@ -377,7 +221,7 @@ class BalancesUpdateSystem( accounts.forEach { account -> val address = account.address(chain) ?: return@forEach val accountId = account.accountId(chain) ?: return@forEach - chain.assets.forEach { asset -> + chain.assets.forEach asset@{ asset -> val balance = kotlin.runCatching { ethereumRemoteSource.fetchEthBalance(asset, address) } .onFailure { @@ -386,7 +230,7 @@ class BalancesUpdateSystem( "fetchEthBalance error ${it.message} ${it.localizedMessage} $it" ) } - .getOrNull() ?: return@forEach + .getOrNull() ?: return@asset val balanceData = SimpleBalanceData(balance) assetCache.updateAsset( metaId = account.id, @@ -398,10 +242,6 @@ class BalancesUpdateSystem( } } - override fun start(): Flow { - return combine(subscribeFlow(), singleUpdateFlow()) { sideEffect, _ -> sideEffect } - } - private fun logError(chain: Chain, error: Throwable) { Log.e( "BalancesUpdateSystem", @@ -463,6 +303,10 @@ class BalancesUpdateSystem( source = OperationLocal.Source.BLOCKCHAIN ) } + + override fun start(): Flow { + return subscribeFlow() + } } // Request with id = 0 helps to indicate balances subscriptions in logs diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/model/SubstrateBalancesUtils.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/model/SubstrateBalancesUtils.kt deleted file mode 100644 index ebc8e194f3..0000000000 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/network/model/SubstrateBalancesUtils.kt +++ /dev/null @@ -1,105 +0,0 @@ -package jp.co.soramitsu.wallet.impl.data.network.model - -import android.util.Log -import jp.co.soramitsu.common.data.network.runtime.binding.AssetBalanceData -import jp.co.soramitsu.common.data.network.runtime.binding.EmptyBalance -import jp.co.soramitsu.common.utils.Modules -import jp.co.soramitsu.common.utils.system -import jp.co.soramitsu.common.utils.tokens -import jp.co.soramitsu.core.models.Asset -import jp.co.soramitsu.core.models.ChainAssetType -import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot -import jp.co.soramitsu.shared_utils.runtime.metadata.module -import jp.co.soramitsu.shared_utils.runtime.metadata.storage -import jp.co.soramitsu.shared_utils.runtime.metadata.storageKey -import jp.co.soramitsu.wallet.api.data.cache.bindAccountInfoOrDefault -import jp.co.soramitsu.wallet.api.data.cache.bindAssetsAccountData -import jp.co.soramitsu.wallet.api.data.cache.bindEquilibriumAccountData -import jp.co.soramitsu.wallet.api.data.cache.bindOrmlTokensAccountDataOrDefault - - -fun constructBalanceKey( - runtime: RuntimeSnapshot, - asset: Asset, - accountId: ByteArray -): String? { - val keyConstructionResult = runCatching { - val currency = - asset.currency ?: return@runCatching runtime.metadata.system().storage("Account") - .storageKey(runtime, accountId) - when (asset.typeExtra) { - null, ChainAssetType.Normal, - ChainAssetType.Equilibrium, - ChainAssetType.SoraUtilityAsset -> runtime.metadata.system().storage("Account") - .storageKey(runtime, accountId) - - ChainAssetType.OrmlChain, - ChainAssetType.OrmlAsset, - ChainAssetType.VToken, - ChainAssetType.VSToken, - ChainAssetType.Stable, - ChainAssetType.ForeignAsset, - ChainAssetType.StableAssetPoolToken, - ChainAssetType.SoraAsset, - ChainAssetType.AssetId, - ChainAssetType.Token2, - ChainAssetType.Xcm, - ChainAssetType.LiquidCrowdloan -> runtime.metadata.tokens().storage("Accounts") - .storageKey(runtime, accountId, currency) - - ChainAssetType.Assets -> runtime.metadata.module(Modules.ASSETS).storage("Account") - .storageKey(runtime, currency, accountId) - - ChainAssetType.Unknown -> error("Not supported type for token ${asset.symbol} in ${asset.chainName}") - } - } - return keyConstructionResult - .onFailure { - Log.d( - "BalancesUpdateSystem", - "Failed to construct storage key for asset ${asset.symbol} (${asset.id}) $it " - ) - } - .getOrNull() -} - -fun handleBalanceResponse( - runtime: RuntimeSnapshot, - asset: Asset, - scale: String? -): Result { - return runCatching { - when (asset.typeExtra) { - null, - ChainAssetType.Normal, - ChainAssetType.SoraUtilityAsset -> { - bindAccountInfoOrDefault(scale, runtime) - } - - ChainAssetType.OrmlChain, - ChainAssetType.OrmlAsset, - ChainAssetType.ForeignAsset, - ChainAssetType.StableAssetPoolToken, - ChainAssetType.LiquidCrowdloan, - ChainAssetType.VToken, - ChainAssetType.SoraAsset, - ChainAssetType.VSToken, - ChainAssetType.AssetId, - ChainAssetType.Token2, - ChainAssetType.Xcm, - ChainAssetType.Stable -> { - bindOrmlTokensAccountDataOrDefault(scale, runtime) - } - - ChainAssetType.Equilibrium -> { - bindEquilibriumAccountData(scale, runtime) ?: EmptyBalance - } - - ChainAssetType.Assets -> { - bindAssetsAccountData(scale, runtime) ?: EmptyBalance - } - - ChainAssetType.Unknown -> EmptyBalance - } - } -} diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/HistoryRepository.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/HistoryRepository.kt index 8f31f675e2..3a5f7c87b4 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/HistoryRepository.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/HistoryRepository.kt @@ -1,5 +1,6 @@ package jp.co.soramitsu.wallet.impl.data.repository +import android.util.Log import jp.co.soramitsu.common.data.model.CursorPage import jp.co.soramitsu.common.utils.mapList import jp.co.soramitsu.core.models.Asset diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt index 523c4b3fad..049fc185ce 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt @@ -6,8 +6,6 @@ import java.math.BigInteger import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.account.api.domain.model.accountId -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState -import jp.co.soramitsu.common.compose.component.NetworkIssueType import jp.co.soramitsu.common.data.network.HttpExceptionHandler import jp.co.soramitsu.common.data.network.coingecko.CoingeckoApi import jp.co.soramitsu.common.data.network.config.AppConfigRemote @@ -18,9 +16,7 @@ import jp.co.soramitsu.common.data.network.runtime.binding.bindString import jp.co.soramitsu.common.data.network.runtime.binding.cast import jp.co.soramitsu.common.data.secrets.v2.KeyPairSchema import jp.co.soramitsu.common.data.secrets.v2.MetaAccountSecrets -import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies -import jp.co.soramitsu.common.domain.SelectedFiat import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.mixin.api.UpdatesProviderUi import jp.co.soramitsu.common.utils.Modules @@ -36,7 +32,6 @@ import jp.co.soramitsu.core.runtime.storage.returnType import jp.co.soramitsu.core.utils.utilityAsset import jp.co.soramitsu.coredb.dao.OperationDao import jp.co.soramitsu.coredb.dao.PhishingDao -import jp.co.soramitsu.coredb.dao.TokenPriceDao import jp.co.soramitsu.coredb.dao.emptyAccountIdValue import jp.co.soramitsu.coredb.model.AssetUpdateItem import jp.co.soramitsu.coredb.model.AssetWithToken @@ -69,7 +64,6 @@ import jp.co.soramitsu.wallet.impl.data.network.phishing.PhishingApi import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletConstants import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository import jp.co.soramitsu.wallet.impl.domain.model.Asset -import jp.co.soramitsu.wallet.impl.domain.model.Asset.Companion.createEmpty import jp.co.soramitsu.wallet.impl.domain.model.AssetWithStatus import jp.co.soramitsu.wallet.impl.domain.model.Fee import jp.co.soramitsu.wallet.impl.domain.model.Transfer @@ -104,11 +98,8 @@ class WalletRepositoryImpl( private val availableFiatCurrencies: GetAvailableFiatCurrencies, private val updatesMixin: UpdatesMixin, private val remoteConfigFetcher: RemoteConfigFetcher, - private val preferences: Preferences, private val accountRepository: AccountRepository, private val chainsRepository: ChainsRepository, - private val selectedFiat: SelectedFiat, - private val tokenPriceDao: TokenPriceDao, private val extrinsicService: ExtrinsicService, private val remoteStorageSource: StorageDataSource ) : WalletRepository, UpdatesProviderUi by updatesMixin { @@ -136,61 +127,10 @@ class WalletRepositoryImpl( ) } } - - val assetsByChain: List = chainsById.values - .flatMap { chain -> - chain.assets.map { - AssetWithStatus( - asset = createEmpty( - chainAsset = it, - metaId = meta.id, - accountId = meta.accountId(chain) ?: emptyAccountIdValue, - minSupportedVersion = chain.minSupportedVersion - ), - hasAccount = !chain.isEthereumBased || meta.ethereumPublicKey != null, - hasChainAccount = chain.id in chainAccounts.mapNotNull { it.chain?.id } - ) - } - } - - val assetsByUniqueAccounts = chainAccounts.mapNotNull { chainAccount -> - createEmpty(chainAccount)?.let { asset -> - AssetWithStatus( - asset = asset, - hasAccount = true, - hasChainAccount = false - ) - } - } - - val notUpdatedAssetsByUniqueAccounts = assetsByUniqueAccounts.filter { unique -> - !updatedAssets.any { - it.asset.token.configuration.chainToSymbol == unique.asset.token.configuration.chainToSymbol && - it.asset.accountId.contentEquals(unique.asset.accountId) - } - } - val notUpdatedAssets = assetsByChain.filter { - it.asset.token.configuration.chainToSymbol !in updatedAssets.map { it.asset.token.configuration.chainToSymbol } - } - - updatedAssets + notUpdatedAssetsByUniqueAccounts + notUpdatedAssets + updatedAssets } } - private fun buildNetworkIssues(items: List): Set { - return items.map { - val configuration = it.asset.token.configuration - NetworkIssueItemState( - iconUrl = configuration.iconUrl, - title = "${configuration.chainName} ${configuration.name}", - type = NetworkIssueType.Node, - chainId = configuration.chainId, - chainName = configuration.chainName, - assetId = configuration.id - ) - }.toSet() - } - override suspend fun getAssets(metaId: Long): List = withContext(Dispatchers.Default) { val chainsById = chainsRepository.getChainsById() val assetsLocal = assetCache.getAssets(metaId) @@ -220,7 +160,7 @@ class WalletRepositoryImpl( syncAllRates(chains, currencyId) } - private suspend fun syncAllRates(chains: List, currencyId: String) { + private suspend fun syncAllRates(chains: List, currencyId: String) { val priceIdsWithChainlinkId = chains.map { it.assets.mapNotNull { asset -> asset.priceId?.let { priceId -> @@ -240,7 +180,7 @@ class WalletRepositoryImpl( updatesMixin.startUpdateTokens(allPriceIds) var coingeckoPriceStats: Map> = emptyMap() - var chainlinkPrices: Map = emptyMap() + var chainlinkPrices: Map = emptyMap() coroutineScope { launch { @@ -266,7 +206,12 @@ class WalletRepositoryImpl( listOf( TokenPriceLocal(priceId, stat[currencyId], fiatCurrency?.symbol, change), chainlinkId?.let { - TokenPriceLocal(chainlinkId, chainlinkPrices[chainlinkId], fiatCurrency?.symbol, change) + TokenPriceLocal( + chainlinkId, + chainlinkPrices[chainlinkId], + fiatCurrency?.symbol, + change + ) } ) }.flatten().filterNotNull() @@ -327,27 +272,8 @@ class WalletRepositoryImpl( return assetLocal?.let { mapAssetLocalToAsset(it, chainAsset, minSupportedVersion) } } - override suspend fun updateAssetHidden( - metaId: Long, - accountId: AccountId, - isHidden: Boolean, - chainAsset: CoreAsset - ) { - val tokenPriceId = - chainAsset.priceProvider?.id?.takeIf { selectedFiat.isUsd() } ?: chainAsset.priceId - val updateItems = listOf( - AssetUpdateItem( - metaId = metaId, - chainId = chainAsset.chainId, - accountId = accountId, - id = chainAsset.id, - sortIndex = Int.MAX_VALUE, // Int.MAX_VALUE on sorting because we don't use it anymore - just random value - enabled = !isHidden, - tokenPriceId = tokenPriceId - ) - ) - - assetCache.updateAsset(updateItems) + override suspend fun updateAssetsHidden(state: List) { + assetCache.updateAsset(state) } override suspend fun observeTransferFee( @@ -412,7 +338,15 @@ class WalletRepositoryImpl( ethereumSource.performTransfer(chain, transfer, privateKey.toHexString(true)) .requireValue() // handle error } else { - substrateSource.performTransfer(accountId, chain, transfer, tip, appId, additional, batchAll) + substrateSource.performTransfer( + accountId, + chain, + transfer, + tip, + appId, + additional, + batchAll + ) } val accountAddress = chain.addressOf(accountId) @@ -615,10 +549,6 @@ class WalletRepositoryImpl( return kotlin.runCatching { remoteConfigFetcher.getAppConfig() } } - override fun chainRegistrySyncUp() { - chainRegistry.syncUp() - } - override suspend fun getControllerAccount(chainId: ChainId, accountId: AccountId): AccountId? { return substrateSource.getControllerAccount(chainId, accountId) } @@ -631,11 +561,12 @@ class WalletRepositoryImpl( accountMetaId: Long, assetId: String ): Flow> { - return chainsRepository.observeChainsPerAssetFlow(accountMetaId, assetId).map { - val chains = it.keys.map { mapChainLocalToChain(it) } + return chainsRepository.observeChainsPerAssetFlow(accountMetaId, assetId).map { chainsPerAsset -> + val chains = chainsPerAsset.keys.map { mapChainLocalToChain(it) } val chainsById = chains.associateBy { it.id } - val assets = it.values.map { mapAssetLocalToAsset(chainsById, it) } - chains.zip(assets).toMap() + val assets = chainsPerAsset.values.map { mapAssetLocalToAsset(chainsById, it) } + val chainToAssetMap = chains.zip(assets).toMap() + chainToAssetMap.filter { pair -> pair.value != null} } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt index e7a2876a73..cd1077424d 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt @@ -12,6 +12,7 @@ import javax.inject.Named import javax.inject.Singleton import jp.co.soramitsu.account.api.domain.interfaces.AccountInteractor import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository +import jp.co.soramitsu.account.impl.domain.WalletSyncService import jp.co.soramitsu.account.impl.presentation.account.mixin.api.AccountListingMixin import jp.co.soramitsu.account.impl.presentation.account.mixin.impl.AccountListingProvider import jp.co.soramitsu.common.address.AddressIconGenerator @@ -19,12 +20,11 @@ import jp.co.soramitsu.common.data.network.HttpExceptionHandler import jp.co.soramitsu.common.data.network.NetworkApiCreator import jp.co.soramitsu.common.data.network.coingecko.CoingeckoApi import jp.co.soramitsu.common.data.network.config.RemoteConfigFetcher -import jp.co.soramitsu.common.data.network.rpc.BulkRetriever import jp.co.soramitsu.common.data.storage.Preferences import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.domain.SelectedFiat import jp.co.soramitsu.common.interfaces.FileProvider -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.QrBitmapDecoder @@ -45,6 +45,7 @@ import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository import jp.co.soramitsu.runtime.multiNetwork.connection.EthereumConnectionPool import jp.co.soramitsu.runtime.multiNetwork.runtime.RuntimeFilesCache +import jp.co.soramitsu.runtime.storage.source.RemoteStorageSource import jp.co.soramitsu.runtime.storage.source.StorageDataSource import jp.co.soramitsu.wallet.api.data.cache.AssetCache import jp.co.soramitsu.wallet.api.domain.ExistentialDepositUseCase @@ -92,8 +93,6 @@ import jp.co.soramitsu.xcm.XcmService import jp.co.soramitsu.xcm.domain.XcmEntitiesFetcher import jp.co.soramitsu.xnetworking.basic.networkclient.SoramitsuNetworkClient import jp.co.soramitsu.xnetworking.fearlesswallet.txhistory.client.TxHistoryClientForFearlessWalletFactory -import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigBuilder -import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigProvider @InstallIn(SingletonComponent::class) @Module @@ -172,11 +171,8 @@ class WalletFeatureModule { availableFiatCurrencies: GetAvailableFiatCurrencies, updatesMixin: UpdatesMixin, remoteConfigFetcher: RemoteConfigFetcher, - preferences: Preferences, accountRepository: AccountRepository, chainsRepository: ChainsRepository, - selectedFiat: SelectedFiat, - tokenPriceDao: TokenPriceDao, extrinsicService: ExtrinsicService, @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource @@ -194,15 +190,30 @@ class WalletFeatureModule { availableFiatCurrencies, updatesMixin, remoteConfigFetcher, - preferences, accountRepository, chainsRepository, - selectedFiat, - tokenPriceDao, extrinsicService, remoteStorageSource ) + @Provides + @Singleton + fun provideWalletSyncService( + metaAccountDao: MetaAccountDao, + chainsRepository: ChainsRepository, + chainRegistry: ChainRegistry, + remoteStorageSource: RemoteStorageSource, + assetDao: AssetDao, + ): WalletSyncService { + return WalletSyncService( + metaAccountDao, + chainsRepository, + chainRegistry, + remoteStorageSource, + assetDao + ) + } + @Provides @Singleton fun provideHistoryRepository( @@ -222,16 +233,12 @@ class WalletFeatureModule { walletOperationsHistoryApi: OperationsHistoryApi, chainRegistry: ChainRegistry, soramitsuNetworkClient: SoramitsuNetworkClient, - txHistoryClientForFearlessWalletFactory: TxHistoryClientForFearlessWalletFactory, - @Named("prod") soraProdRemoteConfigBuilder: SoraRemoteConfigBuilder, - @Named("stage") soraStageRemoteConfigBuilder: SoraRemoteConfigBuilder + txHistoryClientForFearlessWalletFactory: TxHistoryClientForFearlessWalletFactory ) = HistorySourceProvider( walletOperationsHistoryApi, chainRegistry, soramitsuNetworkClient, - txHistoryClientForFearlessWalletFactory, - soraProdRemoteConfigBuilder, - soraStageRemoteConfigBuilder + txHistoryClientForFearlessWalletFactory ) @Provides @@ -247,7 +254,8 @@ class WalletFeatureModule { selectedFiat: SelectedFiat, updatesMixin: UpdatesMixin, xcmEntitiesFetcher: XcmEntitiesFetcher, - chainsRepository: ChainsRepository + chainsRepository: ChainsRepository, + networkStateService: NetworkStateService ): WalletInteractor = WalletInteractorImpl( walletRepository, addressBookRepository, @@ -259,7 +267,8 @@ class WalletFeatureModule { selectedFiat, updatesMixin, xcmEntitiesFetcher, - chainsRepository + chainsRepository, + networkStateService ) @Provides @@ -355,23 +364,17 @@ class WalletFeatureModule { @Named("BalancesUpdateSystem") fun provideFeatureUpdaters( chainRegistry: ChainRegistry, - accountRepository: AccountRepository, metaAccountDao: MetaAccountDao, - bulkRetriever: BulkRetriever, assetCache: AssetCache, substrateSource: SubstrateRemoteSource, operationDao: OperationDao, - networkStateMixin: NetworkStateMixin, ethereumRemoteSource: EthereumRemoteSource ): UpdateSystem = BalancesUpdateSystem( chainRegistry, - accountRepository, metaAccountDao, - bulkRetriever, assetCache, substrateSource, operationDao, - networkStateMixin, ethereumRemoteSource ) @@ -456,36 +459,6 @@ class WalletFeatureModule { @ApplicationContext context: Context ): TxHistoryClientForFearlessWalletFactory = TxHistoryClientForFearlessWalletFactory(context) - @Singleton - @Provides - @Named("prod") - fun provideProdSoraRemoteConfigBuilder( - client: SoramitsuNetworkClient, - @ApplicationContext context: Context - ): SoraRemoteConfigBuilder { - return SoraRemoteConfigProvider( - context = context, - client = client, - commonUrl = BuildConfig.SORA_CONFIG_COMMON_PROD, - mobileUrl = BuildConfig.SORA_CONFIG_MOBILE_PROD - ).provide() - } - - @Singleton - @Provides - @Named("stage") - fun provideStageSoraRemoteConfigBuilder( - client: SoramitsuNetworkClient, - @ApplicationContext context: Context - ): SoraRemoteConfigBuilder { - return SoraRemoteConfigProvider( - context = context, - client = client, - commonUrl = BuildConfig.SORA_CONFIG_COMMON_STAGE, - mobileUrl = BuildConfig.SORA_CONFIG_MOBILE_STAGE - ).provide() - } - @Provides fun provideAccountListingMixin( interactor: AccountInteractor, diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/ValidateTransferUseCaseImpl.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/ValidateTransferUseCaseImpl.kt index ebf92ddee1..66aeee23d9 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/ValidateTransferUseCaseImpl.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/ValidateTransferUseCaseImpl.kt @@ -7,6 +7,7 @@ import jp.co.soramitsu.account.api.domain.model.accountId import jp.co.soramitsu.common.utils.formatCryptoDetail import jp.co.soramitsu.common.utils.isZero import jp.co.soramitsu.common.utils.orZero +import jp.co.soramitsu.common.utils.positiveOrNull import jp.co.soramitsu.common.utils.sumByBigDecimal import jp.co.soramitsu.core.models.ChainAssetType import jp.co.soramitsu.core.models.ChainId @@ -184,7 +185,7 @@ class ValidateTransferUseCaseImpl( val utilityAssetBalance = utilityAsset?.transferableInPlanks.orZero() val destinationChainUtilityAsset = destinationChain.utilityAsset val totalDestinationUtilityAssetBalanceInPlanks = kotlin.runCatching { destinationChainUtilityAsset?.let { walletRepository.getTotalBalance(it, destinationChain, destinationAccountId) } }.getOrNull().orZero() - val resultedBalance = (originAsset.freeInPlanks ?: originTransferable) - (amountInPlanks + originFee + tip) + val resultedBalance = (originAsset.freeInPlanks.positiveOrNull() ?: originTransferable) - (amountInPlanks + originFee + tip) mapOf( TransferValidationResult.InsufficientBalance to (amountInPlanks + originFee + tip > originAvailable), @@ -195,7 +196,7 @@ class ValidateTransferUseCaseImpl( } originAssetConfig.isUtility -> { - val resultedBalance = (originAsset.freeInPlanks ?: originTransferable) - (amountInPlanks + originFee + tip) + val resultedBalance = (originAsset.freeInPlanks.positiveOrNull() ?: originTransferable) - (amountInPlanks + originFee + tip) mapOf( TransferValidationResult.InsufficientBalance to (amountInPlanks + originFee + tip > originAvailable), @@ -343,7 +344,7 @@ class ValidateTransferUseCaseImpl( } originAssetConfig.isUtility -> { - val resultedBalance = (originAsset.freeInPlanks ?: transferable) - (amountInPlanks + originFee + tip) + val resultedBalance = (originAsset.freeInPlanks.positiveOrNull() ?: transferable) - (amountInPlanks + originFee + tip) val assetEdFormatted = originExistentialDeposit.formatCryptoDetailFromPlanks(originAsset.token.configuration) mapOf( getTransferValidationResultExistentialDeposit(isCrossChainTransfer, assetEdFormatted) to (resultedBalance < originExistentialDeposit), diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt index 1224cca6ff..831e09f748 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt @@ -20,6 +20,9 @@ import jp.co.soramitsu.common.domain.SelectedFiat import jp.co.soramitsu.common.interfaces.FileProvider import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.mixin.api.UpdatesProviderUi +import jp.co.soramitsu.common.domain.NetworkStateService +import jp.co.soramitsu.common.domain.model.NetworkIssueType +import jp.co.soramitsu.common.model.AssetBooleanState import jp.co.soramitsu.common.utils.Modules import jp.co.soramitsu.common.utils.mapList import jp.co.soramitsu.common.utils.orZero @@ -27,17 +30,18 @@ import jp.co.soramitsu.common.utils.requireValue import jp.co.soramitsu.core.models.Asset.StakingType import jp.co.soramitsu.core.models.ChainId import jp.co.soramitsu.core.utils.isValidAddress +import jp.co.soramitsu.coredb.model.AssetUpdateItem import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.isPolkadotOrKusama import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId -import jp.co.soramitsu.runtime.multiNetwork.chainWithAsset import jp.co.soramitsu.shared_utils.extensions.toHexString import jp.co.soramitsu.shared_utils.runtime.AccountId import jp.co.soramitsu.shared_utils.runtime.extrinsic.ExtrinsicBuilder import jp.co.soramitsu.shared_utils.runtime.metadata.moduleOrNull import jp.co.soramitsu.shared_utils.ss58.SS58Encoder.toAddress +import jp.co.soramitsu.wallet.impl.data.network.blockchain.updaters.BalanceUpdateTrigger import jp.co.soramitsu.wallet.impl.data.repository.HistoryRepository import jp.co.soramitsu.wallet.impl.domain.interfaces.AddressBookRepository import jp.co.soramitsu.wallet.impl.domain.interfaces.AssetSorting @@ -57,6 +61,7 @@ import jp.co.soramitsu.wallet.impl.domain.model.Transfer import jp.co.soramitsu.wallet.impl.domain.model.WalletAccount import jp.co.soramitsu.wallet.impl.domain.model.toPhishingModel import jp.co.soramitsu.xcm.domain.XcmEntitiesFetcher +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -64,11 +69,11 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import jp.co.soramitsu.core.models.Asset as CoreAsset private const val QR_PREFIX_SUBSTRATE = "substrate" @@ -76,11 +81,11 @@ const val QR_PREFIX_WALLET_CONNECT = "wc" private const val PREFS_WALLET_SELECTED_CHAIN_ID = "wallet_selected_chain_id" private const val PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT = "prefs_sora_card_hidden_sessions_count" private const val SORA_CARD_HIDDEN_SESSIONS_LIMIT = 5 -private const val HIDE_ZERO_BALANCES_PREFS_KEY = "hideZeroBalances" private const val CHAIN_SELECT_FILTER_APPLIED = "chain_select_filter_applied" private const val ACCOUNT_ID_MIN_TAG = 26 private const val ACCOUNT_ID_MAX_TAG = 51 private const val ASSET_SORTING_KEY = "ASSET_SORTING_KEY" +private const val ASSET_MANAGEMENT_INTRO_PASSED_KEY = "ASSET_MANAGEMENT_INTRO_PASSED_KEY" class WalletInteractorImpl( private val walletRepository: WalletRepository, @@ -93,33 +98,11 @@ class WalletInteractorImpl( private val selectedFiat: SelectedFiat, private val updatesMixin: UpdatesMixin, private val xcmEntitiesFetcher: XcmEntitiesFetcher, - private val chainsRepository: ChainsRepository + private val chainsRepository: ChainsRepository, + private val networkStateService: NetworkStateService, + private val coroutineContext: CoroutineContext = Dispatchers.Default ) : WalletInteractor, UpdatesProviderUi by updatesMixin { - override suspend fun getHideZeroBalancesForCurrentWallet(): Boolean { - val walletId = accountRepository.getSelectedMetaAccount().id - val key = getHideZeroBalancesKey(walletId) - return preferences.getBoolean(key, false) - } - - override suspend fun toggleHideZeroBalancesForCurrentWallet() { - val walletId = accountRepository.getSelectedMetaAccount().id - val key = getHideZeroBalancesKey(walletId) - val value = preferences.getBoolean(key, false) - val newValue = value.not() - preferences.putBoolean(key, newValue) - } - - override fun observeHideZeroBalanceEnabledForCurrentWallet(): Flow { - return accountRepository.selectedLightMetaAccountFlow().flatMapMerge { wallet -> - preferences.booleanFlow(getHideZeroBalancesKey(wallet.id), false) - }.distinctUntilChanged() - } - - private fun getHideZeroBalancesKey(walletId: Long): String { - return "${HIDE_ZERO_BALANCES_PREFS_KEY}_$walletId" - } - @OptIn(ExperimentalCoroutinesApi::class) override fun assetsFlow(): Flow> { return accountRepository.selectedMetaAccountFlow() @@ -184,11 +167,11 @@ class WalletInteractorImpl( return kotlin.runCatching { getCurrentAssetOrNull(chainId, chainAssetId)!! }.requireValue() } - override suspend fun getCurrentAssetOrNull(chainId: ChainId, chainAssetId: String): Asset? { + override suspend fun getCurrentAssetOrNull(chainId: ChainId, chainAssetId: String): Asset? = withContext(coroutineContext) { val metaAccount = accountRepository.getSelectedMetaAccount() val (chain, chainAsset) = chainsRepository.chainWithAsset(chainId, chainAssetId) - return walletRepository.getAsset( + return@withContext walletRepository.getAsset( metaAccount.id, metaAccount.accountId(chain)!!, chainAsset, @@ -289,10 +272,10 @@ class WalletInteractorImpl( override suspend fun getTransferFee( transfer: Transfer, additional: (suspend ExtrinsicBuilder.() -> Unit)? - ): Fee { + ): Fee = withContext(Dispatchers.Default) { val chain = chainsRepository.getChain(transfer.chainAsset.chainId) - return walletRepository.getTransferFee( + return@withContext walletRepository.getTransferFee( chain = chain, transfer = transfer, additional = additional @@ -444,42 +427,33 @@ class WalletInteractorImpl( override suspend fun getChainAddressForSelectedMetaAccount(chainId: ChainId) = getSelectedMetaAccount().address(getChain(chainId)) - override suspend fun markAssetAsHidden(chainId: ChainId, chainAssetId: String) { - manageAssetHidden(chainId, chainAssetId, true) - } - - override suspend fun markAssetAsShown(chainId: ChainId, chainAssetId: String) { - manageAssetHidden(chainId, chainAssetId, false) - } - - private suspend fun manageAssetHidden( - chainId: ChainId, - chainAssetId: String, - isHidden: Boolean - ) { - val metaAccount = accountRepository.getSelectedMetaAccount() - val chain = chainsRepository.getChain(chainId) - val accountId = metaAccount.accountId(chain) - val chainAsset = chain.assetsById[chainAssetId] ?: return - - val chainsWithAsset = chainsRepository.getChains().filter { chainItem -> - chainItem.assets.any { it.symbol == chainAsset.symbol } - } - - val assetsToManage = chainsWithAsset.map { - it.assets.filter { it.symbol == chainAsset.symbol } - }.flatten() - - accountId?.let { - assetsToManage.forEach { - walletRepository.updateAssetHidden( - chainAsset = it, - metaId = metaAccount.id, + override suspend fun updateAssetsHiddenState(state: List) { + val wallet = getSelectedMetaAccount() + val updateItems = state.mapNotNull { + val chain = getChain(it.chainId) + val asset = chain.assetsById[it.assetId] + val tokenPriceId = asset?.priceProvider?.id?.takeIf { selectedFiat.isUsd() } ?: asset?.priceId + wallet.accountId(chain)?.let { accountId -> + AssetUpdateItem( + metaId = wallet.id, + chainId = it.chainId, accountId = accountId, - isHidden = isHidden + id = it.assetId, + sortIndex = Int.MAX_VALUE, // Int.MAX_VALUE on sorting because we don't use it anymore - just random value + enabled = it.value, + tokenPriceId = tokenPriceId ) } } + walletRepository.updateAssetsHidden(updateItems) + } + + override suspend fun markAssetAsHidden(chainId: ChainId, chainAssetId: String) { + updateAssetsHiddenState(listOf(AssetBooleanState(chainId, chainAssetId, false))) + } + + override suspend fun markAssetAsShown(chainId: ChainId, chainAssetId: String) { + updateAssetsHiddenState(listOf(AssetBooleanState(chainId, chainAssetId, true))) } override fun selectedMetaAccountFlow(): Flow { @@ -639,6 +613,14 @@ class WalletInteractorImpl( preferences.putString(key, filter) } + override suspend fun saveAssetManagementIntroPassed() { + preferences.putBoolean(ASSET_MANAGEMENT_INTRO_PASSED_KEY, true) + } + + override fun getAssetManagementIntroPassed(): Boolean { + return preferences.getBoolean(ASSET_MANAGEMENT_INTRO_PASSED_KEY, defaultValue = false) + } + @OptIn(ExperimentalCoroutinesApi::class) override fun observeSelectedAccountChainSelectFilter(): Flow { return accountRepository.selectedMetaAccountFlow().map { @@ -665,6 +647,14 @@ class WalletInteractorImpl( return walletRepository.observeChainsPerAsset(accountMetaId, assetId) } + override fun observeCurrentAccountChainsPerAsset( + assetId: String + ): Flow> { + return accountRepository.selectedMetaAccountFlow().flatMapLatest { + walletRepository.observeChainsPerAsset(it.id, assetId) + } + } + override fun applyAssetSorting(sorting: AssetSorting) { preferences.putString(ASSET_SORTING_KEY, sorting.name) } @@ -674,8 +664,28 @@ class WalletInteractorImpl( AssetSorting.FiatBalance.toString() }.map { sortingAsString -> sortingAsString?.let { - AssetSorting.values().find { sorting -> sorting.name == it } + AssetSorting.entries.find { sorting -> sorting.name == it } } ?: AssetSorting.FiatBalance } } -} + + override fun networkIssuesFlow(): Flow> { + return networkStateService.networkIssuesFlow + } + + override suspend fun retryChainSync(chainId: ChainId): Result { + return withContext(coroutineContext) { + val chain = chainsRepository.getChain(chainId) + chainRegistry.setupChain(chain) + val runtime = withTimeoutOrNull(15_000L) { + chainRegistry.awaitRuntimeProvider(chainId).get() + } + BalanceUpdateTrigger.invoke(chainId) + if(runtime == null) { + return@withContext Result.failure(Exception("Failed to sync chain")) + } else { + return@withContext Result.success(Unit) + } + } + } +} \ No newline at end of file diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/AssetListHelper.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/AssetListHelper.kt index 17de8922a4..2a6ecf7af5 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/AssetListHelper.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/AssetListHelper.kt @@ -1,10 +1,7 @@ package jp.co.soramitsu.wallet.impl.presentation -import android.util.Log import java.math.BigDecimal import jp.co.soramitsu.common.compose.component.NetworkIssueItemState -import jp.co.soramitsu.common.utils.formatCrypto -import jp.co.soramitsu.common.utils.isZero import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.common.utils.sumByBigDecimal import jp.co.soramitsu.core.models.ChainId @@ -19,8 +16,7 @@ object AssetListHelper { assets: List, filteredChains: List, selectedChainId: ChainId? = null, - networkIssues: Set, - hideZeroBalancesEnabled: Boolean + networkIssues: Set ): List { val result = mutableListOf() assets.groupBy { it.asset.token.configuration.symbol } @@ -63,19 +59,7 @@ object AssetListHelper { val assetTotal = symbolAssets.sumByBigDecimal { it.asset.total.orZero() } val assetTotalFiat = symbolAssets.sumByBigDecimal { it.asset.fiatAmount.orZero() } - val assetVisibleTotal = try { - assetTotal.formatCrypto().replace(',', '.').toBigDecimal() - } catch (e: NumberFormatException) { - Log.e("AssetListHelper", "assetVisibleTotal calculation failure", e) - assetTotal - } - val isZeroBalance = assetVisibleTotal.isZero() - val assetDisabledByUser = symbolAssets.any { it.asset.enabled == false } - val assetManagedByUser = symbolAssets.any { it.asset.enabled != null } - - val isHidden = - assetDisabledByUser || (!assetManagedByUser && isZeroBalance && hideZeroBalancesEnabled) val token = symbolAssets.first().asset.token @@ -87,7 +71,7 @@ object AssetListHelper { fiatAmount = assetTotalFiat, transferable = assetTransferable, chainUrls = assetChainUrls, - isHidden = isHidden + isHidden = assetDisabledByUser ) result.add(model) } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt index 0ee1642bf3..e4ac7a2664 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt @@ -10,7 +10,6 @@ import jp.co.soramitsu.common.navigation.DelayedNavigation import jp.co.soramitsu.common.navigation.PinRequired import jp.co.soramitsu.common.navigation.SecureRouter import jp.co.soramitsu.common.navigation.payload.WalletSelectorPayload -import jp.co.soramitsu.common.presentation.StoryGroupModel import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.wallet.api.domain.model.XcmChainType @@ -89,7 +88,7 @@ interface WalletRouter : SecureRouter, WalletRouterApi { fun finishSendFlow() - fun openTransferDetail(transaction: OperationParcelizeModel.Transfer, assetPayload: AssetPayload, chainHistoryType: Chain.ExternalApi.Section.Type?) + fun openTransferDetail(transaction: OperationParcelizeModel.Transfer, assetPayload: AssetPayload, chainExplorerType: Chain.Explorer.Type?) fun openSwapDetail(operation: OperationParcelizeModel.Swap) @@ -133,8 +132,6 @@ interface WalletRouter : SecureRouter, WalletRouterApi { fun openOnboardingNavGraph(chainId: ChainId, metaId: Long, isImport: Boolean) - fun openEducationalStories(stories: StoryGroupModel) - fun openSuccessFragment(avatar: Drawable) fun openTransactionRawData(rawData: String) @@ -195,5 +192,7 @@ interface WalletRouter : SecureRouter, WalletRouterApi { fun openNFTFilter() + fun openManageAssets() + fun openServiceScreen() } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixin.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixin.kt index 194997c5b0..b589b9049f 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixin.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixin.kt @@ -39,9 +39,9 @@ interface BuyMixin { override val integrateWithBuyProviderEvent: MutableLiveData> - fun buyClicked(chainId: ChainId, chainAssetId: String, accountAddress: String) + suspend fun buyClicked(chainId: ChainId, chainAssetId: String, accountAddress: String) - fun isBuyEnabled(chainId: ChainId, chainAssetId: String): Boolean + suspend fun isBuyEnabled(chainId: ChainId, chainAssetId: String): Boolean } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixinProvider.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixinProvider.kt index 97d37249fb..b20f29f92e 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixinProvider.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetActions/buy/BuyMixinProvider.kt @@ -19,7 +19,7 @@ class BuyMixinProvider( override val integrateWithBuyProviderEvent = MutableLiveData>() - override fun isBuyEnabled(chainId: ChainId, chainAssetId: String) = + override suspend fun isBuyEnabled(chainId: ChainId, chainAssetId: String) = when (val asset = chainRegistry.getAsset(chainId, chainAssetId)) { null -> false else -> buyTokenRegistry.availableProviders(asset).isNotEmpty() @@ -39,7 +39,7 @@ class BuyMixinProvider( integrateWithBuyProviderEvent.value = Event(payload) } - override fun buyClicked(chainId: ChainId, chainAssetId: String, accountAddress: String) { + override suspend fun buyClicked(chainId: ChainId, chainAssetId: String, accountAddress: String) { val asset = chainRegistry.getAsset(chainId, chainAssetId) val availableProviders = asset?.let { buyTokenRegistry.availableProviders(it) } ?: emptyList() diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetDetails/AssetDetailsViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetDetails/AssetDetailsViewModel.kt index 8acd1aa07d..f97f509872 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetDetails/AssetDetailsViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetDetails/AssetDetailsViewModel.kt @@ -18,8 +18,7 @@ import jp.co.soramitsu.common.compose.component.MainToolbarViewState import jp.co.soramitsu.common.compose.component.MultiToggleButtonState import jp.co.soramitsu.common.compose.component.NetworkIssueType import jp.co.soramitsu.common.compose.component.ToolbarHomeIconState -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.common.mixin.api.NetworkStateUi +import jp.co.soramitsu.common.domain.model.NetworkIssue import jp.co.soramitsu.common.presentation.LoadingState import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.applyFiatRate @@ -59,13 +58,12 @@ import kotlinx.coroutines.launch class AssetDetailsViewModel @Inject constructor( private val interactor: WalletInteractor, private val getAssetBalance: AssetBalanceUseCase, - private val networkStateMixin: NetworkStateMixin, private val walletRouter: WalletRouter, private val accountInteractor: AccountInteractor, private val resourceManager: ResourceManager, private val assetNotNeedAccount: AssetNotNeedAccountUseCase, savedStateHandle: SavedStateHandle, -) : BaseViewModel(), AssetDetailsCallback, NetworkStateUi by networkStateMixin { +) : BaseViewModel(), AssetDetailsCallback { companion object { private const val KEY_ALERT_RESULT = "notNeedAlertResult" @@ -190,6 +188,16 @@ class AssetDetailsViewModel @Inject constructor( }.launchIn(viewModelScope) } + private val networkIssuesFlow = interactor.networkIssuesFlow().map { issuesMap -> + issuesMap.mapValues { + when(it.value) { + jp.co.soramitsu.common.domain.model.NetworkIssueType.Node -> NetworkIssueType.Node + jp.co.soramitsu.common.domain.model.NetworkIssueType.Network -> NetworkIssueType.Network + jp.co.soramitsu.common.domain.model.NetworkIssueType.Account -> NetworkIssueType.Account + } + } + } + private fun subscribeAssets() { val assetSortingFlow = interactor.observeAssetSorting() cachedPerChainBalanceWithAssetFlow @@ -226,9 +234,7 @@ class AssetDetailsViewModel @Inject constructor( val networkIssueType = if (asset?.hasAccount == false) { NetworkIssueType.Account } else { - networkIssues.firstOrNull { - it.chainId == chain.id - }?.type + networkIssues[chain.id] } AssetDetailsItemViewState( diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectViewModel.kt index f9bf680745..55eb4d2fd2 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectViewModel.kt @@ -52,7 +52,7 @@ class AssetSelectViewModel @Inject constructor( else -> null } } - .map { it.filterNotNull() } + .map { it.filterNotNull().filter { asset -> asset.enabled == true } } .mapList { mapAssetToAssetModel(it) } private val enteredTokenQueryFlow = MutableStateFlow("") diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectViewModel.kt index 5da8af806c..8d7a466f41 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectViewModel.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -33,10 +34,10 @@ import jp.co.soramitsu.wallet.api.presentation.WalletRouter as WalletRouterApi class ChainSelectViewModel @Inject constructor( private val walletRouter: WalletRouter, private val walletInteractor: WalletInteractor, - chainInteractor: ChainInteractor, + private val chainInteractor: ChainInteractor, savedStateHandle: SavedStateHandle, private val sharedSendState: SendSharedState, - private val accountInteractor: AccountInteractor + private val accountInteractor: AccountInteractor, ) : BaseViewModel(), ChainSelectScreenContract { private val initialSelectedChainId: ChainId? = savedStateHandle[ChainSelectFragment.KEY_SELECTED_CHAIN_ID] @@ -83,17 +84,7 @@ class ChainSelectViewModel @Inject constructor( private val chainsFlow = allChainsFlow.map { chains -> when { initialSelectedAssetId != null -> { - chains.firstOrNull { - it.assets.any { asset -> asset.id == initialSelectedAssetId } - }?.let { chainOfTheAsset -> - val symbol = chainOfTheAsset.assets - .firstOrNull { it.id == initialSelectedAssetId } - ?.symbol - val chainsWithAsset = chains.filter { - it.assets.any { it.symbol == symbol } - } - chainsWithAsset - } + walletInteractor.observeCurrentAccountChainsPerAsset(initialSelectedAssetId).first().keys.toList() } filterChainIds.isNullOrEmpty() -> { chains diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt index c09d25599d..ab0f29c879 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt @@ -34,7 +34,6 @@ import jp.co.soramitsu.common.utils.formatFiat import jp.co.soramitsu.common.utils.mapList import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.feature_wallet_impl.R -import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraMainChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraTestChainId @@ -151,7 +150,7 @@ class BalanceDetailViewModel @Inject constructor( } } - private fun isBuyEnabled(): Boolean { + private suspend fun isBuyEnabled(): Boolean { return buyMixin.isBuyEnabled( assetPayload.value.chainId, assetPayload.value.chainAssetId @@ -380,7 +379,7 @@ class BalanceDetailViewModel @Inject constructor( return actionItems } - private fun getDisabledItems(): List { + private suspend fun getDisabledItems(): List { return if (!isBuyEnabled()) { listOf(ActionItemType.BUY) } else { @@ -487,19 +486,10 @@ class BalanceDetailViewModel @Inject constructor( } override fun transactionClicked(transactionModel: OperationModel) { - launch { - val chain = interactor.getChain(assetPayload.value.chainId) - val chainHistoryType: Chain.ExternalApi.Section.Type? = chain.externalApi?.history?.type - - transactionHistoryProvider.transactionClicked( - transactionModel = transactionModel, - assetPayload = AssetPayload( - chainId = assetPayload.value.chainId, - chainAssetId = assetPayload.value.chainAssetId - ), - chainHistoryType = chainHistoryType - ) - } + transactionHistoryProvider.transactionClicked( + transactionModel = transactionModel, + assetPayload = assetPayload.value + ) } override fun tableItemClicked(itemId: Int) { diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/TransactionItem.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/TransactionItem.kt index ba67d26fdd..b492488f3a 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/TransactionItem.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/TransactionItem.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable @@ -82,21 +83,25 @@ fun TransactionItem( ) if (item.type == OperationModel.Type.Transfer) { - val headerModifier = Modifier.constrainAs(header) { - top.linkTo(parent.top) - start.linkTo(imageSpacer.end) - end.linkTo(amount.start) - width = Dimension.fillToConstraints - } + val headerModifier = Modifier + .constrainAs(header) { + top.linkTo(parent.top) + start.linkTo(imageSpacer.end) + end.linkTo(amount.start) + width = Dimension.fillToConstraints + } + .padding(end = 8.dp) B1EllipsizeMiddle( text = item.header, modifier = headerModifier ) } else { - val headerModifier = Modifier.constrainAs(header) { - top.linkTo(parent.top) - start.linkTo(imageSpacer.end) - } + val headerModifier = Modifier + .constrainAs(header) { + top.linkTo(parent.top) + start.linkTo(imageSpacer.end) + } + .padding(end = 8.dp) B1( text = item.header, textAlign = TextAlign.Start, diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt index c110bbf7ee..b2d2bbc31a 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt @@ -1,5 +1,6 @@ package jp.co.soramitsu.wallet.impl.presentation.balance.list +import android.util.Log import android.widget.LinearLayout import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.SwipeableState @@ -10,6 +11,8 @@ import co.jp.soramitsu.walletconnect.domain.WalletConnectInteractor import com.walletconnect.android.internal.common.exception.MalformedWalletConnectUri import dagger.hilt.android.lifecycle.HiltViewModel import java.math.BigDecimal +import java.math.BigInteger +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import jp.co.soramitsu.account.api.domain.PendulumPreInstalledAccountsScenario import jp.co.soramitsu.account.api.domain.interfaces.AccountInteractor @@ -26,19 +29,19 @@ import jp.co.soramitsu.common.compose.component.ChainSelectorViewStateWithFilter import jp.co.soramitsu.common.compose.component.ChangeBalanceViewState import jp.co.soramitsu.common.compose.component.MainToolbarViewStateWithFilters import jp.co.soramitsu.common.compose.component.MultiToggleButtonState -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState import jp.co.soramitsu.common.compose.component.SwipeState import jp.co.soramitsu.common.compose.component.ToolbarHomeIconState import jp.co.soramitsu.common.compose.models.LoadableListPage import jp.co.soramitsu.common.compose.models.ScreenLayout +import jp.co.soramitsu.common.compose.utils.PageScrollingCallback import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState import jp.co.soramitsu.common.data.network.coingecko.FiatChooserEvent import jp.co.soramitsu.common.data.network.coingecko.FiatCurrency import jp.co.soramitsu.common.domain.FiatCurrencies import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.domain.SelectedFiat -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.common.mixin.api.NetworkStateUi +import jp.co.soramitsu.common.domain.model.NetworkIssueType import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.mixin.api.UpdatesProviderUi import jp.co.soramitsu.common.presentation.LoadingState @@ -48,7 +51,9 @@ import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.combine import jp.co.soramitsu.common.utils.formatAsChange import jp.co.soramitsu.common.utils.formatFiat +import jp.co.soramitsu.common.utils.greaterThanOrEquals import jp.co.soramitsu.common.utils.inBackground +import jp.co.soramitsu.common.utils.lessThan import jp.co.soramitsu.common.utils.mapList import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet @@ -56,10 +61,7 @@ import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.feature_wallet_impl.R import jp.co.soramitsu.nft.data.pagination.PaginationRequest import jp.co.soramitsu.nft.domain.NFTInteractor -import jp.co.soramitsu.common.compose.utils.PageScrollingCallback import jp.co.soramitsu.nft.domain.models.NFTCollection -import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.models.NFTCollectionsScreenModel -import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.models.NFTCollectionsScreenView import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.defaultChainSort @@ -82,11 +84,15 @@ import jp.co.soramitsu.wallet.impl.presentation.balance.chainselector.toChainIte import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.AssetType import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.BalanceListItemModel import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.toAssetState +import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.toUiModel +import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.models.NFTCollectionsScreenModel +import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.models.NFTCollectionsScreenView import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.models.ScreenModel import jp.co.soramitsu.wallet.impl.presentation.model.ControllerDeprecationWarningModel import jp.co.soramitsu.wallet.impl.presentation.model.toModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -96,10 +102,10 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn @@ -110,9 +116,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.atomic.AtomicBoolean private const val CURRENT_ICON_SIZE = 40 @@ -126,7 +132,6 @@ class BalanceListViewModel @Inject constructor( private val selectedFiat: SelectedFiat, private val accountInteractor: AccountInteractor, private val updatesMixin: UpdatesMixin, - private val networkStateMixin: NetworkStateMixin, private val resourceManager: ResourceManager, private val clipboardManager: ClipboardManager, private val currentAccountAddress: CurrentAccountAddressUseCase, @@ -134,9 +139,10 @@ class BalanceListViewModel @Inject constructor( private val pendulumPreInstalledAccountsScenario: PendulumPreInstalledAccountsScenario, private val nftInteractor: NFTInteractor, private val walletConnectInteractor: WalletConnectInteractor -) : BaseViewModel(), UpdatesProviderUi by updatesMixin, NetworkStateUi by networkStateMixin, +) : BaseViewModel(), UpdatesProviderUi by updatesMixin, WalletScreenInterface { + private var awaitAssetsJob: Job? = null private val accountAddressToChainIdMap = mutableMapOf() private val _showFiatChooser = MutableLiveData() @@ -170,6 +176,7 @@ class BalanceListViewModel @Inject constructor( }.inBackground() private val selectedChainId = MutableStateFlow(null) + private val selectedChainItemFlow = combine(selectedChainId, chainsFlow) { selectedChainId, chains -> selectedChainId?.let { @@ -177,12 +184,9 @@ class BalanceListViewModel @Inject constructor( } } + private val networkIssueStateFlow = MutableStateFlow(null) + private val currentMetaAccountFlow = interactor.selectedLightMetaAccountFlow() - .onEach { - if (pendulumPreInstalledAccountsScenario.isPendulumMode(it.id)) { - selectedChainId.value = pendulumChainId - } - } private val assetTypeSelectorState = MutableStateFlow( MultiToggleButtonState( @@ -193,29 +197,25 @@ class BalanceListViewModel @Inject constructor( private val showNetworkIssues = MutableStateFlow(false) + private val currentAssetsFlow = MutableStateFlow>(emptyList()) + private val assetStates = combine( interactor.assetsFlowAndAccount(), chainInteractor.getChainsFlow(), selectedChainId, interactor.selectedMetaAccountFlow(), - networkIssuesFlow, - interactor.observeSelectedAccountChainSelectFilter(), - interactor.observeHideZeroBalanceEnabledForCurrentWallet() + interactor.observeSelectedAccountChainSelectFilter() ) { (walletId: Long, assets: List), chains: List, selectedChainId: ChainId?, currentMetaAccountFlow: MetaAccount, - networkIssues: Set, - appliedFilterAsString: String, - hideZeroBalancesEnabled: Boolean -> + appliedFilterAsString: String -> val filter = ChainSelectorViewStateWithFilters.Filter.entries.find { it.name == appliedFilterAsString } ?: ChainSelectorViewStateWithFilters.Filter.All - val shouldShowNetworkIssues = - selectedChainId == null && (networkIssues.isNotEmpty() || assets.any { it.hasAccount.not() }) - showNetworkIssues.value = shouldShowNetworkIssues + showNetworkIssues.value = false val selectedAccountFavoriteChains = currentMetaAccountFlow.favoriteChains @@ -240,19 +240,24 @@ class BalanceListViewModel @Inject constructor( else -> emptyList() } - val filteredAssets = assets + val filteredAssets = assets.asSequence() .filter { - selectedChainId == it.asset.token.configuration.chainId || - selectedChainId == null || - it.asset.token.configuration.chainId in filteredChains.map { it.id } + it.asset.enabled != false && + ((selectedChainId == null && filter == ChainSelectorViewStateWithFilters.Filter.All) || + it.asset.token.configuration.chainId == selectedChainId || + it.asset.token.configuration.chainId in filteredChains.map { it.id }) } + .toList() + + currentAssetsFlow.update { filteredAssets } + + val filteredAssetsWithoutBrokenAssets = filteredAssets.filter { it.asset.freeInPlanks.greaterThanOrEquals(BigInteger.ZERO) } val balanceListItems = AssetListHelper.processAssets( - assets = filteredAssets, + assets = filteredAssetsWithoutBrokenAssets, filteredChains = filteredChains, selectedChainId = selectedChainId, - networkIssues = networkIssues, - hideZeroBalancesEnabled = hideZeroBalancesEnabled + networkIssues = emptySet() ) val assetStates: List = balanceListItems @@ -267,7 +272,8 @@ class BalanceListViewModel @Inject constructor( } assetStates - }.onStart { emit(buildInitialAssetsList().toMutableList()) }.inBackground().share() + }.onStart { emit(buildInitialAssetsList().toMutableList()) } + .inBackground().share() @OptIn(FlowPreview::class) private fun createNFTCollectionScreenViewsFlow(): Flow, ScreenLayout>> { @@ -362,15 +368,25 @@ class BalanceListViewModel @Inject constructor( ) } - private val assetTypeState = combine( + private val assetTypeState: Flow = combine( + selectedChainId, assetTypeSelectorState, assetStates, nftInteractor.nftFiltersFlow(), - createNFTCollectionScreenViewsFlow() - ) { selectorState, assetStates, filters, (pageViews, screenLayout) -> + createNFTCollectionScreenViewsFlow(), + networkIssueStateFlow + ) { selectedChainId, selectorState, assetStates, filters, (pageViews, screenLayout), networkIssueState -> when (selectorState.currentSelection) { AssetType.Currencies -> { - WalletAssetsState.Assets(assetStates) + val isSelectedChainHasIssues = networkIssueState != null + if (isSelectedChainHasIssues) { + requireNotNull(networkIssueState) + } else { + WalletAssetsState.Assets( + assets = assetStates, + isHideVisible = selectedChainId != null + ) + } } AssetType.NFTs -> { @@ -402,14 +418,29 @@ class BalanceListViewModel @Inject constructor( .stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = null) } - private fun observeNetworkState() { - networkStateMixin.showConnectingBarFlow - .onEach { hasConnectionProblems -> - if (!hasConnectionProblems) { - refresh() - } - } - .launchIn(viewModelScope) + private fun observeNetworkIssues() { + combine( + currentAssetsFlow, + interactor.networkIssuesFlow(), + selectedChainId + ) { currentAssets, networkIssues, selectedChainId -> + if (selectedChainId == null) return@combine null + + val isAllAssetsWithProblems = + currentAssets.isNotEmpty() && currentAssets.filter { it.asset.token.configuration.chainId == selectedChainId } + .all { it.asset.freeInPlanks == null || it.asset.freeInPlanks.lessThan(BigInteger.ZERO) } + if (isAllAssetsWithProblems.not()) return@combine null + + val selectedChainIssue = networkIssues[selectedChainId] ?: NetworkIssueType.Network + + WalletAssetsState.NetworkIssue( + selectedChainId, + selectedChainIssue.toUiModel(), + false + ) + }.onEach { newState -> + networkIssueStateFlow.update { newState } + }.launchIn(viewModelScope) } // we open screen - no assets in the list @@ -485,6 +516,28 @@ class BalanceListViewModel @Inject constructor( state.value = state.value.copy(hasNetworkIssues = it) }.launchIn(this) subscribeTotalBalance() + if (interactor.getAssetManagementIntroPassed().not()) { + startManageAssetsIntroAnimation() + } + } + + @OptIn(FlowPreview::class) + private fun startManageAssetsIntroAnimation() { + awaitAssetsJob?.cancel() + awaitAssetsJob = assetStates.filter { it.isNotEmpty() } + .map { it.size } + .distinctUntilChanged() + .debounce(200L) + .onEach { + state.value = state.value.copy(scrollToBottomEvent = Event(Unit)) + interactor.saveAssetManagementIntroPassed() + awaitAssetsJob?.cancel() + } + .catch { + Log.d("BalanceListViewModel", it.message, it) + } + .launchIn(this) + awaitAssetsJob?.start() } private fun subscribeTotalBalance() { @@ -533,8 +586,9 @@ class BalanceListViewModel @Inject constructor( init { subscribeScreenState() - observeNetworkState() + observeNetworkIssues() observeFiatSymbolChange() + sync() router.chainSelectorPayloadFlow.map { chainId -> val walletId = interactor.getSelectedMetaAccount().id @@ -562,6 +616,22 @@ class BalanceListViewModel @Inject constructor( } } + override fun onManageAssetClick() { + router.openManageAssets() + } + + override fun onRetry() { + val (chainId, issueType, _) = networkIssueStateFlow.value ?: return + + if (issueType != jp.co.soramitsu.common.compose.component.NetworkIssueType.Account) { + viewModelScope.launch { + networkIssueStateFlow.update { it?.copy(retryButtonLoading = true) } + interactor.retryChainSync(chainId) + networkIssueStateFlow.update { it?.copy(retryButtonLoading = false) } + } + } + } + private fun refresh() { sync() } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt index 66a524b3b3..a1125654ba 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt @@ -1,5 +1,9 @@ package jp.co.soramitsu.wallet.impl.presentation.balance.list +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -7,6 +11,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight @@ -18,8 +23,12 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.SwipeableState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import jp.co.soramitsu.common.compose.component.ActionItemType @@ -28,6 +37,7 @@ import jp.co.soramitsu.common.compose.component.AssetBalanceViewState import jp.co.soramitsu.common.compose.component.BannerBackup import jp.co.soramitsu.common.compose.component.BannerBuyXor import jp.co.soramitsu.common.compose.component.ChangeBalanceViewState +import jp.co.soramitsu.common.compose.component.GrayButton import jp.co.soramitsu.common.compose.component.MarginVertical import jp.co.soramitsu.common.compose.component.MultiToggleButton import jp.co.soramitsu.common.compose.component.MultiToggleButtonState @@ -38,14 +48,17 @@ import jp.co.soramitsu.common.compose.theme.white16 import jp.co.soramitsu.common.compose.theme.white50 import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState import jp.co.soramitsu.common.utils.rememberForeverLazyListState -import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.NFTScreen +import jp.co.soramitsu.feature_wallet_impl.R import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.soracard.impl.presentation.SoraCardItem import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.AssetType +import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.NFTScreen import jp.co.soramitsu.wallet.impl.presentation.common.AssetsList import jp.co.soramitsu.wallet.impl.presentation.common.AssetsListInterface +import jp.co.soramitsu.wallet.impl.presentation.common.NetworkIssue +@Stable interface WalletScreenInterface : AssetsListInterface { fun onAddressClick() fun onBalanceClicked() @@ -56,6 +69,8 @@ interface WalletScreenInterface : AssetsListInterface { fun onBackupCloseClick() fun assetTypeChanged(type: AssetType) fun onRefresh() + fun onManageAssetClick() + fun onRetry() } @Composable @@ -65,12 +80,34 @@ fun WalletScreen( ) { val listState = rememberForeverLazyListState("wallet_screen") + val scale = remember { Animatable(initialValue = 1f) } + LaunchedEffect(data.scrollToTopEvent) { data.scrollToTopEvent?.getContentIfNotHandled()?.let { listState.animateScrollToItem(0) } } + LaunchedEffect(data.scrollToBottomEvent) { + data.scrollToBottomEvent?.getContentIfNotHandled()?.let { + if (data.assetsState is WalletAssetsState.Assets) { + val items = data.assetsState.assets.size + listOf("header", "footer").size + val lastItemIndex = items - 1 + listState.animateScrollToItem(lastItemIndex) + + scale.animateTo( + targetValue = 1.2f, + animationSpec = tween(durationMillis = 600) + ) + scale.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 600) + ) + } + } + } + + Column(modifier = Modifier.padding(horizontal = 16.dp)) { MarginVertical(margin = 16.dp) AssetBalance( @@ -92,21 +129,26 @@ fun WalletScreen( NFTScreen(collectionsScreen = data.assetsState.collectionScreenModel) } is WalletAssetsState.Assets -> { - val header = Banners(data, callback) + val header: @Composable () -> Unit = { Banners(data, callback) } + val footer: @Composable () -> Unit = { WalletScreenFooter(scale.value, callback::onManageAssetClick) } AssetsList( data = data.assetsState, callback = callback, header = header, - listState = listState + listState = listState, + footer = footer ) } + is WalletAssetsState.NetworkIssue -> { + NetworkIssue(data.assetsState.retryButtonLoading, callback::onRetry) + } } } } @OptIn(ExperimentalFoundationApi::class) @Composable -private fun Banners(data: WalletState, callback: WalletScreenInterface): @Composable (() -> Unit)? { +private fun Banners(data: WalletState, callback: WalletScreenInterface) { val soraCardBanner: @Composable (() -> Unit)? = if (data.soraCardState?.visible == true) { { @@ -159,14 +201,10 @@ private fun Banners(data: WalletState, callback: WalletScreenInterface): @Compos } } } - return if (soraCardBanner == null && bannersCarousel == null) { - null - } else { - { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - soraCardBanner?.invoke() - bannersCarousel?.invoke() - } + if (soraCardBanner != null || bannersCarousel != null) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + soraCardBanner?.invoke() + bannersCarousel?.invoke() } } } @@ -208,6 +246,27 @@ fun WalletScreenWithRefresh( } } +@Composable +fun WalletScreenFooter( + scale: Float, + onManageAssetsClick: () -> Unit +) { + GrayButton( + text = stringResource(id = R.string.wallet_manage_assets), + modifier = Modifier + .scale(scale) + .fillMaxWidth() + .animateContentSize( + animationSpec = tween( + durationMillis = 300, + easing = LinearOutSlowInEasing + ) + ) + .height(48.dp), + onClick = onManageAssetsClick + ) +} + @Preview @Composable private fun PreviewWalletScreen() { @@ -221,7 +280,7 @@ private fun PreviewWalletScreen() { override fun onBackupClicked() {} override fun onBackupCloseClick() {} override fun assetTypeChanged(type: AssetType) {} - override fun assetClicked(asset: AssetListItemViewState) {} + override fun assetClicked(state: AssetListItemViewState) {} override fun actionItemClicked( actionType: ActionItemType, @@ -232,6 +291,8 @@ private fun PreviewWalletScreen() { } override fun onRefresh() {} + override fun onManageAssetClick() {} + override fun onRetry() = Unit } val element = AssetListItemViewState( @@ -252,7 +313,7 @@ private fun PreviewWalletScreen() { isTestnet = false ) val assets: List = listOf( - element, element, element + element, element, element.copy(isHidden = true) ).mapIndexed { index, assetListItemViewState -> assetListItemViewState.copy(index = index) } @@ -265,7 +326,7 @@ private fun PreviewWalletScreen() { AssetType.Currencies, listOf(AssetType.Currencies, AssetType.NFTs) ), - assetsState = WalletAssetsState.Assets(emptyList()), + assetsState = WalletAssetsState.Assets(assets, isHideVisible = true), balance = AssetBalanceViewState( "TRANSFERABLE BALANCE", "ADDRESS", @@ -275,7 +336,8 @@ private fun PreviewWalletScreen() { hasNetworkIssues = true, soraCardState = SoraCardItemViewState(null, null, null, true), isBackedUp = false, - scrollToTopEvent = null + scrollToTopEvent = null, + scrollToBottomEvent = null ), callback = emptyCallback ) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt index 7b0a84a515..1f5696b3de 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt @@ -1,8 +1,11 @@ package jp.co.soramitsu.wallet.impl.presentation.balance.list +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import jp.co.soramitsu.common.compose.component.AssetBalanceViewState import jp.co.soramitsu.common.compose.component.ChangeBalanceViewState import jp.co.soramitsu.common.compose.component.MultiToggleButtonState +import jp.co.soramitsu.common.compose.component.NetworkIssueType import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.models.NFTCollectionsScreenModel @@ -10,6 +13,7 @@ import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.AssetType import jp.co.soramitsu.wallet.impl.presentation.common.AssetListState +@Stable data class WalletState( val assetsState: WalletAssetsState, val multiToggleButtonState: MultiToggleButtonState, @@ -17,26 +21,39 @@ data class WalletState( val hasNetworkIssues: Boolean, val soraCardState: SoraCardItemViewState?, val isBackedUp: Boolean, - val scrollToTopEvent: Event? + val scrollToTopEvent: Event?, + val scrollToBottomEvent: Event?, ) { companion object { val default = WalletState( multiToggleButtonState = MultiToggleButtonState(AssetType.Currencies, listOf(AssetType.Currencies, AssetType.NFTs)), - assetsState = WalletAssetsState.Assets(emptyList()), + assetsState = WalletAssetsState.Assets(emptyList(), isHideVisible = true), balance = AssetBalanceViewState("", "", false, ChangeBalanceViewState("", "")), hasNetworkIssues = false, soraCardState = null, isBackedUp = true, - scrollToTopEvent = null + scrollToTopEvent = null, + scrollToBottomEvent = null ) } } +@Stable sealed interface WalletAssetsState { - data class Assets(override val assets: List): WalletAssetsState, AssetListState(assets) + data class Assets( + override val assets: List, + val isHideVisible: Boolean + ): WalletAssetsState, AssetListState(assets) + + @Immutable + data class NetworkIssue( + val chainId: String, + val issueType: NetworkIssueType, + val retryButtonLoading: Boolean + ): WalletAssetsState @JvmInline value class NftAssets( val collectionScreenModel: NFTCollectionsScreenModel ): WalletAssetsState -} \ No newline at end of file +} diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/model/Mapper.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/model/Mapper.kt new file mode 100644 index 0000000000..907ca3a66c --- /dev/null +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/model/Mapper.kt @@ -0,0 +1,9 @@ +package jp.co.soramitsu.wallet.impl.presentation.balance.list.model + +import jp.co.soramitsu.common.domain.model.NetworkIssueType + +fun NetworkIssueType.toUiModel() = when (this) { + NetworkIssueType.Node -> jp.co.soramitsu.common.compose.component.NetworkIssueType.Node + NetworkIssueType.Network -> jp.co.soramitsu.common.compose.component.NetworkIssueType.Network + NetworkIssueType.Account -> jp.co.soramitsu.common.compose.component.NetworkIssueType.Account +} \ No newline at end of file diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/networkissues/NetworkIssuesViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/networkissues/NetworkIssuesViewModel.kt index 2716e848b8..7f93e40e81 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/networkissues/NetworkIssuesViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/networkissues/NetworkIssuesViewModel.kt @@ -10,14 +10,14 @@ import jp.co.soramitsu.common.AlertViewState import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.compose.component.NetworkIssueItemState import jp.co.soramitsu.common.compose.component.NetworkIssueType -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.common.mixin.api.NetworkStateUi +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.mixin.api.UpdatesProviderUi import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.feature_wallet_impl.R import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import jp.co.soramitsu.wallet.impl.presentation.WalletRouter +import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.toUiModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -28,15 +28,16 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @HiltViewModel +@Deprecated("Seems like we don't need this anymore") class NetworkIssuesViewModel @Inject constructor( private val walletRouter: WalletRouter, private val walletInteractor: WalletInteractor, private val accountInteractor: AccountInteractor, private val updatesMixin: UpdatesMixin, - private val networkStateMixin: NetworkStateMixin, + private val networkStateService: NetworkStateService, private val resourceManager: ResourceManager, private val assetNotNeedAccount: AssetNotNeedAccountUseCase -) : BaseViewModel(), UpdatesProviderUi by updatesMixin, NetworkStateUi by networkStateMixin { +) : BaseViewModel(), UpdatesProviderUi by updatesMixin { companion object { private const val KEY_ALERT_RESULT = "result" @@ -44,8 +45,22 @@ class NetworkIssuesViewModel @Inject constructor( private var lastSelectedNetworkIssueState: NetworkIssueItemState? = null + // todo do we need this screen? + private val networkIssuesState = networkStateService.networkIssuesFlow.map { issuesMap -> + issuesMap.entries.map { + NetworkIssueItemState( + iconUrl = "stub",//it.asset.token.configuration.chainIcon ?: it.asset.token.configuration.iconUrl, + title = "stub", + type = it.value.toUiModel(), + chainId = it.key, + chainName = "stub",//it.asset.token.configuration.chainName, + assetId = "stub"//it.asset.token.configuration.id + ) + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + val state = combine( - networkStateMixin.networkIssuesFlow.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet()), + networkIssuesState, walletInteractor.assetsFlow().map { it.filter { !it.hasAccount && !it.asset.markedNotNeed }.map { NetworkIssueItemState( diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/searchAssets/SearchAssetsViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/searchAssets/SearchAssetsViewModel.kt index 1f608ba175..2af6d8e0c2 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/searchAssets/SearchAssetsViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/searchAssets/SearchAssetsViewModel.kt @@ -7,15 +7,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import java.math.BigDecimal +import java.math.BigInteger import javax.inject.Inject import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.compose.component.ActionItemType -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState import jp.co.soramitsu.common.compose.component.SwipeState import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.common.mixin.api.NetworkStateUi import jp.co.soramitsu.common.utils.Event +import jp.co.soramitsu.common.utils.greaterThanOrEquals import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId @@ -39,9 +38,8 @@ class SearchAssetsViewModel @Inject constructor( val savedStateHandle: SavedStateHandle, private val interactor: WalletInteractor, private val chainInteractor: ChainInteractor, - private val router: WalletRouter, - private val networkStateMixin: NetworkStateMixin -) : BaseViewModel(), NetworkStateUi by networkStateMixin, SearchAssetsScreenInterface { + private val router: WalletRouter +) : BaseViewModel(), SearchAssetsScreenInterface { private val _showUnsupportedChainAlert = MutableLiveData>() val showUnsupportedChainAlert: LiveData> = _showUnsupportedChainAlert @@ -54,15 +52,17 @@ class SearchAssetsViewModel @Inject constructor( private val assetStates = combine( interactor.assetsFlow(), chainInteractor.getChainsFlow(), - networkIssuesFlow, - interactor.observeHideZeroBalanceEnabledForCurrentWallet() - ) { assets: List, chains: List, networkIssues: Set, hideZeroBalancesEnabled -> + ) { assets: List, chains: List -> + val readyToUseAssets = assets + .asSequence() + .filter { it.asset.freeInPlanks.greaterThanOrEquals(BigInteger.ZERO) } + .filter { it.asset.enabled == true } + .toList() val balanceListItems = AssetListHelper.processAssets( - assets = assets, + assets = readyToUseAssets, filteredChains = chains, - networkIssues = networkIssues, - hideZeroBalancesEnabled = hideZeroBalancesEnabled + networkIssues = emptySet() ) val assetStates: List = balanceListItems diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetListState.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetListState.kt index 547f67081b..3062599898 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetListState.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetListState.kt @@ -4,9 +4,4 @@ import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState abstract class AssetListState( open val assets: List -) { - val visibleAssets: List - get() = assets.filter { !it.isHidden } - val hiddenAssets: List - get() = assets.filter { it.isHidden } -} +) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt index 51231b748f..168ab72795 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt @@ -1,7 +1,10 @@ package jp.co.soramitsu.wallet.impl.presentation.common import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -9,16 +12,22 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.SwipeableState import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import jp.co.soramitsu.common.compose.component.ActionItemType -import jp.co.soramitsu.common.compose.component.HiddenAssetsItem -import jp.co.soramitsu.common.compose.component.HiddenItemState +import jp.co.soramitsu.common.compose.component.B0 +import jp.co.soramitsu.common.compose.component.GradientIcon +import jp.co.soramitsu.common.compose.component.H3 import jp.co.soramitsu.common.compose.component.MarginVertical import jp.co.soramitsu.common.compose.component.SwipeState +import jp.co.soramitsu.common.compose.theme.alertYellow +import jp.co.soramitsu.common.compose.theme.white50 import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState +import jp.co.soramitsu.feature_wallet_impl.R import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.wallet.impl.presentation.balance.list.WalletAssetsState interface AssetsListInterface { @OptIn(ExperimentalMaterialApi::class) @@ -32,43 +41,66 @@ fun AssetsList( data: AssetListState, callback: AssetsListInterface, listState: LazyListState = rememberLazyListState(), - header: (@Composable () -> Unit)? = null + header: (@Composable () -> Unit)? = null, + footer: (@Composable () -> Unit)? = null ) { - val isShowHidden = remember { mutableStateOf(data.visibleAssets.isEmpty()) } - val onHiddenClick = remember { { isShowHidden.value = isShowHidden.value.not() } } - - LazyColumn( - state = listState, - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(top = 8.dp) - ) { - if (header != null) { - item { header() } - } - items(data.visibleAssets, key = { it.key }) { assetState -> - SwipeableAssetListItem( - assetState = assetState, - assetClicked = callback::assetClicked, - actionItemClicked = callback::actionItemClicked - ) + if (data.assets.isEmpty()) { + Column { + MarginVertical(margin = 8.dp) + header?.invoke() + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center + ) { + EmptyAssetsContent() + } + footer?.invoke() + MarginVertical(margin = 80.dp) } - if (data.hiddenAssets.isNotEmpty()) { - item { - HiddenAssetsItem( - state = HiddenItemState(isShowHidden.value), - onClick = onHiddenClick + } else { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(top = 8.dp) + ) { + if (header != null) { + item { header() } + } + val isHideVisible = (data as? WalletAssetsState.Assets)?.isHideVisible == true + items(data.assets, key = { "${it.key}$isHideVisible" }) { assetState -> + SwipeableAssetListItem( + assetState = assetState, + isHideVisible = isHideVisible, + assetClicked = callback::assetClicked, + actionItemClicked = callback::actionItemClicked ) } - if (isShowHidden.value) { - items(data.hiddenAssets, key = { it.key }) { assetState -> - SwipeableAssetListItem( - assetState = assetState, - assetClicked = callback::assetClicked, - actionItemClicked = callback::actionItemClicked - ) - } + if (footer != null) { + item { footer() } } + item { MarginVertical(margin = 80.dp) } } - item { MarginVertical(margin = 80.dp) } + } +} + +@Composable +fun EmptyAssetsContent() { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + GradientIcon( + iconRes = R.drawable.ic_alert_24, + color = alertYellow, + modifier = Modifier.align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(bottom = 4.dp) + ) + + H3(text = stringResource(id = R.string.common_search_assets_alert_title)) + B0( + text = stringResource(id = R.string.wallet_all_assets_hidden), + color = white50 + ) } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/NetworkIssue.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/NetworkIssue.kt new file mode 100644 index 0000000000..91c8364418 --- /dev/null +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/NetworkIssue.kt @@ -0,0 +1,73 @@ +package jp.co.soramitsu.wallet.impl.presentation.common + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import jp.co.soramitsu.common.compose.component.B0 +import jp.co.soramitsu.common.compose.component.GradientIcon +import jp.co.soramitsu.common.compose.component.GrayButton +import jp.co.soramitsu.common.compose.component.H3 +import jp.co.soramitsu.common.compose.component.MarginVertical +import jp.co.soramitsu.common.compose.theme.FearlessAppTheme +import jp.co.soramitsu.common.compose.theme.alertYellow +import jp.co.soramitsu.common.compose.theme.white50 +import jp.co.soramitsu.feature_wallet_impl.R + +@Composable +fun NetworkIssue(retryButtonLoading: Boolean, onRetry: () -> Unit) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(1f)) + + GradientIcon( + iconRes = R.drawable.ic_alert_24, + color = alertYellow, + modifier = Modifier.align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(bottom = 4.dp) + ) + MarginVertical(margin = 16.dp) + H3(text = stringResource(R.string.common_search_assets_alert_title)) + MarginVertical(margin = 16.dp) + B0( + text = stringResource(R.string.network_issue_main), + color = white50, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.weight(1f)) + GrayButton( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + text = stringResource(id = R.string.common_try_again), + loading = retryButtonLoading, + onClick = onRetry + ) + MarginVertical(margin = 80.dp) + } +} + +@Preview +@Composable +fun NetworkIssuePreview() { + FearlessAppTheme { + Column { + NetworkIssue(true) { + + } + NetworkIssue(false) { + + } + } + } +} \ No newline at end of file diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/SwipeableAssetListItem.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/SwipeableAssetListItem.kt index 3108f10cd2..acc939c0f1 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/SwipeableAssetListItem.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/SwipeableAssetListItem.kt @@ -21,6 +21,7 @@ import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId @Composable fun SwipeableAssetListItem( assetState: AssetListItemViewState, + isHideVisible: Boolean, assetClicked: (AssetListItemViewState) -> Unit, actionItemClicked: (actionType: ActionItemType, chainId: ChainId, chainAssetId: String, swipeableState: SwipeableState) -> Unit ) { @@ -33,7 +34,7 @@ fun SwipeableAssetListItem( val swipeBoxViewState = remember { SwipeBoxViewState( leftStateWidth = 170.dp, - rightStateWidth = 90.dp + rightStateWidth = if (isHideVisible) 90.dp else 0.dp ) } @@ -57,11 +58,13 @@ fun SwipeableAssetListItem( } }, rightContent = { - BackgroundCornered { - ActionBar( - state = rightBarActionViewState, - onItemClick = ::onItemClick - ) + if (isHideVisible) { + BackgroundCornered { + ActionBar( + state = rightBarActionViewState, + onItemClick = ::onItemClick + ) + } } } ) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/confirm/CrossChainConfirmViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/confirm/CrossChainConfirmViewModel.kt index 624a4c482b..91c76f6ada 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/confirm/CrossChainConfirmViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/confirm/CrossChainConfirmViewModel.kt @@ -16,7 +16,6 @@ import jp.co.soramitsu.common.base.errors.ValidationException import jp.co.soramitsu.common.base.errors.ValidationWarning import jp.co.soramitsu.common.compose.component.ButtonViewState import jp.co.soramitsu.common.compose.component.TitleValueViewState -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.combine @@ -28,7 +27,7 @@ import jp.co.soramitsu.common.utils.requireValue import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.core.utils.utilityAsset import jp.co.soramitsu.feature_wallet_impl.R -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.wallet.api.domain.TransferValidationResult import jp.co.soramitsu.wallet.api.domain.ValidateTransferUseCase import jp.co.soramitsu.wallet.api.domain.fromValidationResult @@ -233,7 +232,7 @@ class CrossChainConfirmViewModel @Inject constructor( override fun copyRecipientAddressClicked() { launch { val chain = destinationNetworkFlow.value ?: return@launch - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, transferDraft.recipientAddress) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(transferDraft.recipientAddress) val externalActionsPayload = ExternalAccountActions.Payload( value = transferDraft.recipientAddress, chainId = chain.id, diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/setup/CrossChainSetupViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/setup/CrossChainSetupViewModel.kt index 1241970b4c..a3c17b9da7 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/setup/CrossChainSetupViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/cross_chain/setup/CrossChainSetupViewModel.kt @@ -403,8 +403,7 @@ class CrossChainSetupViewModel @Inject constructor( } else { R.drawable.ic_address_placeholder }, - editable = false, - showClear = false + editable = false ), originChainSelectorState = originChainSelectorState, destinationChainSelectorState = destinationChainSelectorState, diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsContent.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsContent.kt new file mode 100644 index 0000000000..0283c2b607 --- /dev/null +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsContent.kt @@ -0,0 +1,473 @@ +package jp.co.soramitsu.wallet.impl.presentation.manageassets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Switch +import androidx.compose.material.SwitchColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.CenterStart +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import jp.co.soramitsu.common.compose.component.B0 +import jp.co.soramitsu.common.compose.component.B1 +import jp.co.soramitsu.common.compose.component.B2 +import jp.co.soramitsu.common.compose.component.CorneredInput +import jp.co.soramitsu.common.compose.component.FearlessProgress +import jp.co.soramitsu.common.compose.component.H3 +import jp.co.soramitsu.common.compose.component.H5 +import jp.co.soramitsu.common.compose.component.Image +import jp.co.soramitsu.common.compose.component.MarginHorizontal +import jp.co.soramitsu.common.compose.component.MarginVertical +import jp.co.soramitsu.common.compose.component.getImageRequest +import jp.co.soramitsu.common.compose.theme.black2 +import jp.co.soramitsu.common.compose.theme.black3 +import jp.co.soramitsu.common.compose.theme.black4 +import jp.co.soramitsu.common.compose.theme.colorAccentDark +import jp.co.soramitsu.common.compose.theme.darkButtonBackground +import jp.co.soramitsu.common.compose.theme.gray2 +import jp.co.soramitsu.common.compose.theme.transparent +import jp.co.soramitsu.common.compose.theme.white +import jp.co.soramitsu.common.compose.theme.white64 +import jp.co.soramitsu.common.utils.clickableSingle +import jp.co.soramitsu.common.utils.clickableWithNoIndication +import jp.co.soramitsu.feature_wallet_impl.R +import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId + +data class ManageAssetsScreenViewState( + val assets: Map>? = null, + val selectedChainTitle: String = "", + val selectedAssetId: String? = null, + val searchQuery: String? = null, + val showAllChains: Boolean = true +) { + companion object { + val default = ManageAssetsScreenViewState() + } +} + +interface ManageAssetsContentInterface { + fun onSearchInput(input: String) + fun onChecked(assetItemState: ManageAssetItemState, checked: Boolean) + fun onItemClicked(assetItemState: ManageAssetItemState) + fun onEditClicked(assetItemState: ManageAssetItemState) + fun onDoneClicked() + fun onSelectedChainClicked() +} + +@Composable +fun ManageAssetsContent( + state: ManageAssetsScreenViewState, + callback: ManageAssetsContentInterface +) { + Column( + modifier = Modifier + .nestedScroll(rememberNestedScrollInteropConnection()) + .padding(horizontal = 16.dp) + .fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + B0( + text = stringResource(id = R.string.common_done), + modifier = Modifier + .align(CenterVertically) + .clickableSingle(onClick = callback::onDoneClicked), + color = colorAccentDark + ) + B0( + text = state.selectedChainTitle, + modifier = Modifier + .align(CenterVertically) + .clickableSingle(onClick = callback::onSelectedChainClicked) + ) + } + MarginVertical(margin = 16.dp) + CorneredInput(state = state.searchQuery, onInput = callback::onSearchInput, hintLabel = stringResource(id = R.string.common_search)) + MarginVertical(margin = 8.dp) + + if (state.assets == null) { + Box( + Modifier + .weight(1f) + .fillMaxWidth() + ) { + FearlessProgress( + Modifier.align(Alignment.Center) + ) + } + } else if (state.assets.isEmpty()) { + MarginVertical(margin = 16.dp) + Column( + horizontalAlignment = CenterHorizontally, + modifier = Modifier + .weight(1f) + .align(CenterHorizontally) + ) { + EmptyResultContent() + } + } else { + LazyColumn(modifier = Modifier.weight(1f)) { + item { + ManageAssetsHeader() + } + + items(state.assets.entries.toList()) { assetsGroup -> + val assets = assetsGroup.value + + if (assets.size == 1) { + ManageAssetItem(assets[0], callback::onEditClicked, callback::onItemClicked, callback::onChecked) + } else { + val isCollapsed = remember { mutableStateOf(true) } + + LaunchedEffect(key1 = state.searchQuery) { + if (state.searchQuery.isNullOrBlank().not()) { + isCollapsed.value = false + } + } + + GroupItem(assets, isCollapsed) + + if (isCollapsed.value.not()) { + Column { + assets.map { + ManageAssetItem(it.copy(isGrouped = true), callback::onEditClicked, callback::onItemClicked, callback::onChecked) + } + } + } + } + } + } + } + MarginVertical(margin = 52.dp) + } +} + +@Composable +private fun ManageAssetsHeader() { + Box( + modifier = Modifier + .height(44.dp) + .fillMaxWidth(), + contentAlignment = CenterStart + ) { + H5( + text = stringResource(id = R.string.wallet_manage_assets), + textAlign = TextAlign.Start + ) + } +} + +@Composable +fun EmptyResultContent() { + Column( + horizontalAlignment = CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_alert), + contentDescription = null, + tint = gray2 + ) + H3(text = stringResource(id = R.string.common_search_assets_alert_title)) + B0( + text = stringResource(id = R.string.common_empty_search), + color = gray2 + ) + } +} + +data class ManageAssetItemState( + val id: String, + val imageUrl: String?, + val chainName: String, + val assetName: String?, + val symbol: String, + val amount: String, + val fiatAmount: String?, + val chainId: ChainId, + val isChecked: Boolean, + val showEdit: Boolean, + val isZeroAmount: Boolean, + val isGrouped: Boolean = false +) + +@Composable +fun ManageAssetItem( + state: ManageAssetItemState, + onEditClick: (ManageAssetItemState) -> Unit, + onItemClick: (ManageAssetItemState) -> Unit, + onChecked: (ManageAssetItemState, Boolean) -> Unit +) { + val switchColors = object : SwitchColors { + @Composable + override fun thumbColor(enabled: Boolean, checked: Boolean): State { + val color = if (enabled) { + white + } else { + white64 + } + return rememberUpdatedState(color) + } + + @Composable + override fun trackColor(enabled: Boolean, checked: Boolean): State { + return rememberUpdatedState(transparent) + } + } + + Row( + verticalAlignment = CenterVertically, + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + .background(if (state.isGrouped) darkButtonBackground else Color.Unspecified) + .clickableWithNoIndication { + onItemClick(state) + } + ) { + AsyncImage( + model = state.imageUrl?.let { getImageRequest(LocalContext.current, it) }, + contentDescription = null, + modifier = Modifier + .testTag("ManageAssetItem_image_${state.id}") + .size(32.dp), + colorFilter = ColorFilter.tint(white64, BlendMode.DstOut).takeIf { state.isChecked.not() } + ) + MarginHorizontal(margin = 10.dp) + Row( + verticalAlignment = CenterVertically + ) { + Column { + Row( + verticalAlignment = CenterVertically + ) { + val symbolColor = if (!state.isChecked || state.isZeroAmount) black2 else Color.Unspecified + B1( + text = state.symbol, + fontWeight = FontWeight.W600, + color = symbolColor + ) + if (state.showEdit) { + MarginHorizontal(margin = 6.dp) + Image( + res = R.drawable.ic_edit_20, + modifier = Modifier + .size(20.dp) + .clickableSingle { + onEditClick(state) + } + ) + } + } + + B2(text = state.chainName, color = black2) + } + Spacer(modifier = Modifier.weight(1f)) + if (state.isChecked) { + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Center + ) { + B1( + text = state.amount, + fontWeight = FontWeight.W600 + ) + state.fiatAmount?.let { + B2(text = it, color = black2) + } + } + } + MarginHorizontal(margin = 8.dp) + val trackColor = when { + state.isChecked -> colorAccentDark + else -> black3 + } + Switch( + colors = switchColors, + checked = state.isChecked, + onCheckedChange = { onChecked(state, it) }, + modifier = Modifier + .background(color = trackColor, shape = RoundedCornerShape(20.dp)) + .padding(3.dp) + .height(20.dp) + .width(36.dp) + .align(CenterVertically) + ) + } + } +} + +@Composable +private fun GroupItem( + groupAssets: List, + isCollapsed: MutableState +) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + .clickableWithNoIndication { + isCollapsed.value = isCollapsed.value.not() + } + ) { + val image = groupAssets.firstOrNull { it.imageUrl != null }?.imageUrl?.let { + getImageRequest(LocalContext.current, it) + } + val assetName = groupAssets.firstOrNull { it.assetName != null }?.assetName.orEmpty() + val allAssetsAreHidden = groupAssets.all { it.isChecked.not() } + + AsyncImage( + model = image, + contentDescription = null, + modifier = Modifier + .testTag("ManageGroupItem_image_${groupAssets.getOrNull(0)?.symbol}") + .size(32.dp), + colorFilter = ColorFilter.tint(white64, BlendMode.DstOut).takeIf { allAssetsAreHidden } + ) + MarginHorizontal(margin = 10.dp) + Row( + verticalAlignment = CenterVertically + ) { + val groupNameColor = if (allAssetsAreHidden) black2 else Color.Unspecified + + Column { + B1(text = assetName, fontWeight = FontWeight.W600, color = groupNameColor) + B2(text = pluralStringResource(id = R.plurals.common_networks_format, groupAssets.size, groupAssets.size), color = black2) + } + Spacer(modifier = Modifier.weight(1f)) + Image( + res = R.drawable.ic_chevron_up_white, + modifier = Modifier + .size(20.dp) + .rotate(if (isCollapsed.value) 180f else 0f) + ) + MarginHorizontal(margin = 8.dp) + } + } +} + +@Preview +@Composable +private fun ManageAssetsScreenPreview() { + val items = listOf( + ManageAssetItemState( + id = "1", + imageUrl = "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/chains/white/Moonriver.svg", + chainName = "Kusama", + assetName = "Asset on Kusama", + symbol = "KSM", + amount = "0", + fiatAmount = "0$", + chainId = "", + isChecked = true, + isZeroAmount = true, + showEdit = false + ), + ManageAssetItemState( + id = "2", + imageUrl = "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/chains/white/Kusama.svg", + chainName = "Moonriver", + assetName = "Asset on Moonriver", + symbol = "MOVR", + amount = "10", + fiatAmount = "23240$", + chainId = "", + isChecked = false, + isZeroAmount = true, + showEdit = true + ), + ManageAssetItemState( + id = "3", + imageUrl = "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/chains/white/Kusama.svg", + chainName = "Westend", + assetName = "WND from the Westend", + symbol = "WND", + amount = "42", + fiatAmount = null, + chainId = "", + isChecked = true, + isZeroAmount = true, + showEdit = true + ), + ManageAssetItemState( + id = "4", + imageUrl = "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/chains/white/Kusama.svg", + chainName = "TWO-TEE", + assetName = "TWO TEE TO TWO-TWO", + symbol = "TWO", + amount = "333", + fiatAmount = null, + chainId = "", + isChecked = false, + isZeroAmount = true, + showEdit = true + ) + ) + val state = ManageAssetsScreenViewState( + selectedChainTitle = "All chains", + assets = mapOf( + "DOT" to items, + "disabled assets" to items.filter { it.isChecked.not() }, + "MOVR" to items.filter { it.symbol == "MOVR" }, + "KSM" to items.filter { it.symbol == "KSM" }, + "WND" to items.filter { it.symbol == "WND" }, + ), + searchQuery = null + ) + ManageAssetItem(items[0], {}, {}, { _, _ -> }) + Column( + Modifier.background(black4) + ) { + ManageAssetsContent( + state = state, + callback = object : ManageAssetsContentInterface { + override fun onSearchInput(input: String) {} + override fun onChecked(assetItemState: ManageAssetItemState, checked: Boolean) {} + override fun onItemClicked(assetItemState: ManageAssetItemState) {} + override fun onEditClicked(assetItemState: ManageAssetItemState) {} + override fun onDoneClicked() {} + override fun onSelectedChainClicked() {} + } + ) + } +} diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsFragment.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsFragment.kt new file mode 100644 index 0000000000..84925e01fc --- /dev/null +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsFragment.kt @@ -0,0 +1,40 @@ +package jp.co.soramitsu.wallet.impl.presentation.manageassets + +import android.content.DialogInterface +import android.widget.FrameLayout +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import com.google.android.material.bottomsheet.BottomSheetBehavior +import dagger.hilt.android.AndroidEntryPoint +import jp.co.soramitsu.common.base.BaseComposeBottomSheetDialogFragment +import jp.co.soramitsu.common.compose.component.BottomSheetScreen + +@AndroidEntryPoint +class ManageAssetsFragment : BaseComposeBottomSheetDialogFragment() { + + override val viewModel: ManageAssetsViewModel by viewModels() + + @Composable + override fun Content(padding: PaddingValues) { + val state by viewModel.state.collectAsState() + BottomSheetScreen { + ManageAssetsContent( + state = state, + callback = viewModel + ) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + viewModel.onDialogClose() + } + override fun setupBehavior(behavior: BottomSheetBehavior) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + behavior.isHideable = true + behavior.skipCollapsed = true + } +} diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsViewModel.kt new file mode 100644 index 0000000000..22abe724f6 --- /dev/null +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/manageassets/ManageAssetsViewModel.kt @@ -0,0 +1,242 @@ +package jp.co.soramitsu.wallet.impl.presentation.manageassets + +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import jp.co.soramitsu.account.api.domain.interfaces.AccountInteractor +import jp.co.soramitsu.common.base.BaseViewModel +import jp.co.soramitsu.common.compose.component.ChainSelectorViewStateWithFilters +import jp.co.soramitsu.common.model.AssetBooleanState +import jp.co.soramitsu.common.resources.ResourceManager +import jp.co.soramitsu.common.utils.formatCrypto +import jp.co.soramitsu.common.utils.isZero +import jp.co.soramitsu.common.utils.mapList +import jp.co.soramitsu.common.utils.orZero +import jp.co.soramitsu.core.models.ChainId +import jp.co.soramitsu.feature_wallet_impl.R +import jp.co.soramitsu.wallet.impl.data.mappers.mapAssetToAssetModel +import jp.co.soramitsu.wallet.impl.domain.ChainInteractor +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor +import jp.co.soramitsu.wallet.impl.presentation.WalletRouter +import jp.co.soramitsu.wallet.impl.presentation.model.AssetModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +@HiltViewModel +class ManageAssetsViewModel @Inject constructor( + private val walletRouter: WalletRouter, + private val walletInteractor: WalletInteractor, + private val accountInteractor: AccountInteractor, + private val chainInteractor: ChainInteractor, + private val resourceManager: ResourceManager +) : BaseViewModel(), ManageAssetsContentInterface { + + private val initialAssetStates = MutableStateFlow>(emptyList()) + private val currentAssetStates = MutableStateFlow>(emptyList()) + + private val selectedChainIdFlow = MutableStateFlow(null) + + private val savedChainFlow = selectedChainIdFlow.map { chainId -> + chainId?.let { walletInteractor.getChain(it) } + } + + private val assetModelsFlow: Flow> = + combine( + walletInteractor.assetsFlow(), + chainInteractor.getChainsFlow(), + walletInteractor.selectedMetaAccountFlow(), + walletInteractor.observeSelectedAccountChainSelectFilter(), + selectedChainIdFlow + ) { assets, chains, currentMetaAccount, appliedFilterAsString, selectedChainId -> + val filter = ChainSelectorViewStateWithFilters.Filter.entries.find { + it.name == appliedFilterAsString + } ?: ChainSelectorViewStateWithFilters.Filter.All + + val selectedAccountFavoriteChains = currentMetaAccount.favoriteChains + val chainsWithFavoriteInfo = chains.map { chain -> + chain to (selectedAccountFavoriteChains[chain.id]?.isFavorite == true) + } + val filteredChains = when { + selectedChainId != null -> chains.filter { it.id == selectedChainId } + filter == ChainSelectorViewStateWithFilters.Filter.All -> chainsWithFavoriteInfo.map { it.first } + + filter == ChainSelectorViewStateWithFilters.Filter.Favorite -> + chainsWithFavoriteInfo.filter { (_, isFavorite) -> isFavorite }.map { it.first } + + filter == ChainSelectorViewStateWithFilters.Filter.Popular -> + chainsWithFavoriteInfo.filter { (chain, _) -> + chain.rank != null + }.sortedBy { (chain, _) -> + chain.rank + }.map { it.first } + + else -> emptyList() + } + + assets.filter { + (selectedChainId == null && filter == ChainSelectorViewStateWithFilters.Filter.All) || + it.asset.token.configuration.chainId == selectedChainId || + it.asset.token.configuration.chainId in filteredChains.map { it.id } + } + } + .mapList { + when { + it.hasAccount -> it.asset + else -> null + } + } + .map { it.filterNotNull() } + .mapList { mapAssetToAssetModel(it) } + + + private val enteredTokenQueryFlow = MutableStateFlow("") + + val state = MutableStateFlow(ManageAssetsScreenViewState.default) + + private fun subscribeScreenState() { + combine( + savedChainFlow, + walletInteractor.observeSelectedAccountChainSelectFilter() + ) { chain, filterAsText -> + val filterApplied = ChainSelectorViewStateWithFilters.Filter.entries.find { + it.name == filterAsText + } ?: ChainSelectorViewStateWithFilters.Filter.All + + val selectedChainTitle = chain?.name ?: when(filterApplied) { + ChainSelectorViewStateWithFilters.Filter.All -> + resourceManager.getString(R.string.chain_selection_all_networks) + + ChainSelectorViewStateWithFilters.Filter.Popular -> + resourceManager.getString(R.string.network_management_popular) + + ChainSelectorViewStateWithFilters.Filter.Favorite -> + resourceManager.getString(R.string.network_managment_favourite) + } + state.value = state.value.copy(selectedChainTitle = selectedChainTitle) + }.launchIn(this) + + combine(assetModelsFlow, enteredTokenQueryFlow, currentAssetStates) { assetModels, searchQuery, currentStates -> + val sortedAssets = assetModels + .filter { + searchQuery.isEmpty() || + it.token.configuration.symbol.contains(searchQuery, true) || + it.token.configuration.name.orEmpty().contains(searchQuery, true) + } + .sortedWith(compareBy { + it.isHidden == true + }.thenByDescending { + it.fiatAmount.orZero() + }.thenByDescending { + it.available.orZero() + }.thenBy { + it.token.configuration.chainName + }) + .map { model -> + model.copy(isHidden = currentStates.firstOrNull { + it.assetId == model.token.configuration.id && it.chainId == model.token.configuration.chainId + }?.value == false) + } + + val assets = sortedAssets.map { + it.toManageAssetItemState() + } + + val groupedAssets: Map> = assets.groupBy { + it.symbol + } + + groupedAssets to searchQuery + }.onEach { (assets, searchQuery) -> + state.value = state.value.copy(assets = assets, searchQuery = searchQuery) + }.launchIn(this) + } + + init { + subscribeScreenState() + + accountInteractor.selectedMetaAccountFlow().map { it.id }.distinctUntilChanged().map { + selectedChainIdFlow.value = walletInteractor.getSavedChainId(it) + + walletInteractor.assetsFlow().firstOrNull()?.let { assets -> + val assetStates = assets.map { + AssetBooleanState( + chainId = it.asset.token.configuration.chainId, + assetId = it.asset.token.configuration.id, + value = it.asset.enabled != false + ) + } + initialAssetStates.value = assetStates + currentAssetStates.value = assetStates + } + }.launchIn(this) + + walletRouter.chainSelectorPayloadFlow.map { chainId -> + val walletId = accountInteractor.selectedLightMetaAccount().id + walletInteractor.saveChainId(walletId, chainId) + selectedChainIdFlow.value = chainId + }.launchIn(this) + } + + private fun AssetModel.toManageAssetItemState() = ManageAssetItemState( + id = token.configuration.id, + imageUrl = token.configuration.iconUrl, + chainName = token.configuration.chainName, + assetName = token.configuration.name, + symbol = token.configuration.symbol.uppercase(), + amount = available.orZero().formatCrypto(), + fiatAmount = getAsFiatWithCurrency(available) ?: "${token.fiatSymbol.orEmpty()}0".takeIf { token.configuration.priceId != null || token.configuration.priceProvider != null }, + chainId = token.configuration.chainId, + isChecked = isHidden != true, + isZeroAmount = available.orZero().isZero(), + showEdit = false + ) + + override fun onSearchInput(input: String) { + enteredTokenQueryFlow.value = input + } + + override fun onChecked(assetItemState: ManageAssetItemState, checked: Boolean) { + currentAssetStates.value = currentAssetStates.value.map { + if (it.assetId == assetItemState.id && it.chainId == assetItemState.chainId) { + it.copy(value = checked) + } else { + it + } + } + } + + override fun onItemClicked(assetItemState: ManageAssetItemState) { + } + + override fun onEditClicked(assetItemState: ManageAssetItemState) { + } + + override fun onDoneClicked() { + walletRouter.back() + } + + override fun onSelectedChainClicked() { + launch { + val selectedChainId = savedChainFlow.firstOrNull()?.id + walletRouter.openSelectChain(selectedChainId, isFilteringEnabled = true) + } + } + + fun onDialogClose() { + walletRouter.setChainSelectorPayload(selectedChainIdFlow.value) + val initial = initialAssetStates.value + val changes = currentAssetStates.value.filter { + it !in initial + } + kotlinx.coroutines.MainScope().launch { + walletInteractor.updateAssetsHiddenState(changes) + } + } +} + diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/receive/ReceiveViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/receive/ReceiveViewModel.kt index ce11237f8d..a45babd204 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/receive/ReceiveViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/receive/ReceiveViewModel.kt @@ -73,8 +73,6 @@ class ReceiveViewModel @Inject constructor( private val assetPayload = savedStateHandle.get(ReceiveFragment.KEY_ASSET_PAYLOAD)!! - private val assetSymbolToShow = chainRegistry.getAsset(assetPayload.chainId, assetPayload.chainAssetId)?.symbol - private val accountFlow = interactor.selectedAccountFlow(assetPayload.chainId) private val assetFlow = chainAssetsManager.assetFlow.onStart { emit(interactor.getCurrentAsset(assetPayload.chainId, assetPayload.chainAssetId)) @@ -165,11 +163,13 @@ class ReceiveViewModel @Inject constructor( soraMainChainId, soraTestChainId ) + val assetSymbol = chainRegistry.getAsset(assetPayload.chainId, assetPayload.chainAssetId)?.symbol + LoadingState.Loaded( ReceiveScreenViewState( account = account, qrCode = qrCode, - assetSymbol = assetSymbolToShow.orEmpty().uppercase(), + assetSymbol = assetSymbol.orEmpty().uppercase(), multiToggleButtonState = receiveTypeState, amountInputViewState = amountInputViewState, requestAllowed = allowRequest diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/confirm/ConfirmSendViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/confirm/ConfirmSendViewModel.kt index effa7d9739..cec6ae12c0 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/confirm/ConfirmSendViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/confirm/ConfirmSendViewModel.kt @@ -18,7 +18,6 @@ import jp.co.soramitsu.common.base.errors.ValidationException import jp.co.soramitsu.common.base.errors.ValidationWarning import jp.co.soramitsu.common.compose.component.ButtonViewState import jp.co.soramitsu.common.compose.component.TitleValueViewState -import jp.co.soramitsu.common.data.network.BlockExplorerUrlBuilder import jp.co.soramitsu.common.resources.ResourceManager import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.applyFiatRate @@ -38,7 +37,7 @@ import jp.co.soramitsu.polkaswap.api.models.Market import jp.co.soramitsu.polkaswap.api.models.WithDesired import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.bokoloCashTokenId -import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.wallet.api.domain.TransferValidationResult import jp.co.soramitsu.wallet.api.domain.ValidateTransferUseCase import jp.co.soramitsu.wallet.api.domain.fromValidationResult @@ -252,7 +251,7 @@ class ConfirmSendViewModel @Inject constructor( launch { val chainId = transferDraft.assetPayload.chainId val chain = chainRegistry.getChain(chainId) - val supportedExplorers = chain.explorers.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, transferDraft.recipientAddress) + val supportedExplorers = chain.explorers.getSupportedAddressExplorers(transferDraft.recipientAddress) val externalActionsPayload = ExternalAccountActions.Payload( value = transferDraft.recipientAddress, chainId = chainId, diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/setup/SendSetupViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/setup/SendSetupViewModel.kt index 522bc23022..15352b1d5f 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/setup/SendSetupViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/send/setup/SendSetupViewModel.kt @@ -64,6 +64,7 @@ import jp.co.soramitsu.wallet.impl.presentation.WalletRouter import jp.co.soramitsu.wallet.impl.presentation.balance.chainselector.ChainSelectScreenContract import jp.co.soramitsu.wallet.impl.presentation.send.SendSharedState import jp.co.soramitsu.wallet.impl.presentation.send.TransferDraft +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -150,9 +151,10 @@ class SendSetupViewModel @Inject constructor( } private val defaultAddressInputState = AddressInputState( - title = resourceManager.getString(R.string.send_fund), - "", - R.drawable.ic_address_placeholder + title = resourceManager.getString(R.string.send_to), + input = "", + image = R.drawable.ic_address_placeholder, + editable = false ) private val defaultAmountInputState = AmountInputViewState( @@ -262,6 +264,7 @@ class SendSetupViewModel @Inject constructor( } }.stateIn(this, SharingStarted.Eagerly, defaultAmountInputState) + @OptIn(ExperimentalCoroutinesApi::class) private val feeAmountFlow = combine( addressInputTrimmedFlow, isInputAddressValidFlow, @@ -352,7 +355,7 @@ class SendSetupViewModel @Inject constructor( message = getPhishingMessage(phishing.type), extras = listOf( phishing.name?.let { resourceManager.getString(R.string.username_setup_choose_title) to it }, - phishing.type?.let { resourceManager.getString(R.string.reason) to it.capitalizedName }, + phishing.type.let { resourceManager.getString(R.string.reason) to it.capitalizedName }, phishing.subtype?.let { resourceManager.getString(R.string.scam_additional_stub) to it } ).mapNotNull { it }, isExpanded = isExpanded, @@ -389,66 +392,7 @@ class SendSetupViewModel @Inject constructor( private val sendAllToggleState: MutableStateFlow = MutableStateFlow(ToggleState.INITIAL) private var existentialDepositCheckJob: Job? = null - val state = combine( - selectedChain, - addressInputTrimmedFlow, - chainSelectorStateFlow, - amountInputViewState, - feeInfoViewStateFlow, - warningInfoStateFlow, - buttonStateFlow, - isSoftKeyboardOpenFlow, - lockInputFlow, - assetFlow, - sendAllToggleState - ) { chain, address, chainSelectorState, amountInputState, feeInfoState, warningInfoState, buttonState, isSoftKeyboardOpen, isInputLocked, asset, sendAllState -> - val isAddressValid = when (chain) { - null -> false - else -> walletInteractor.validateSendAddress(chain.id, address) - } - - confirmedValidations.clear() - - val quickAmountInputValues = if (asset?.token?.configuration?.currencyId == bokoloCashTokenId) { - emptyList() - } else { - QuickAmountInput.values().toList() - } - - val isHistorySupportedByChain = chain?.externalApi?.history != null - - val existentialDeposit = asset?.token?.configuration?.let { existentialDepositUseCase(it) }.orZero() - val sendAllAllowed = existentialDeposit > BigInteger.ZERO - - SendSetupViewState( - toolbarState = toolbarViewState, - addressInputState = AddressInputState( - title = resourceManager.getString(R.string.send_to), - input = address, - image = when { - isAddressValid.not() -> R.drawable.ic_address_placeholder - else -> addressIconGenerator.createAddressIcon( - chain?.isEthereumBased == true, - address, - AddressIconGenerator.SIZE_BIG - ) - }, - editable = false, - showClear = isInputLocked.not() - ), - chainSelectorState = chainSelectorState, - amountInputState = amountInputState, - feeInfoState = feeInfoState, - warningInfoState = warningInfoState, - buttonState = buttonState, - isSoftKeyboardOpen = isSoftKeyboardOpen, - isInputLocked = isInputLocked, - quickAmountInputValues = quickAmountInputValues, - isHistoryAvailable = isHistorySupportedByChain, - sendAllChecked = sendAllState in listOf(ToggleState.CHECKED, ToggleState.CONFIRMED), - sendAllAllowed = sendAllAllowed - ) - }.stateIn(viewModelScope, SharingStarted.Eagerly, defaultState) + val state = MutableStateFlow(defaultState) init { sharedState.clear() @@ -460,9 +404,97 @@ class SendSetupViewModel @Inject constructor( sharedState.update(payload.chainId, payload.chainAssetId) } initSendToAddress?.let { sharedState.updateAddress(it) } + + state.onEach { + confirmedValidations.clear() + }.launchIn(this) + sharedState.addressFlow.onEach { it?.let { addressInputFlow.value = it } }.launchIn(this) + + subscribeScreenState() + } + + private fun subscribeScreenState() { + chainSelectorStateFlow.onEach { + state.value = state.value.copy(chainSelectorState = it) + }.launchIn(this) + + amountInputViewState.onEach { + state.value = state.value.copy(amountInputState = it) + }.launchIn(this) + + feeInfoViewStateFlow.onEach { + state.value = state.value.copy(feeInfoState = it) + }.launchIn(this) + + warningInfoStateFlow.onEach { + state.value = state.value.copy(warningInfoState = it) + }.launchIn(this) + + buttonStateFlow.onEach { + state.value = state.value.copy(buttonState = it) + }.launchIn(this) + + isSoftKeyboardOpenFlow.onEach { + state.value = state.value.copy(isSoftKeyboardOpen = it) + }.launchIn(this) + + sendAllToggleState.onEach { + state.value = state.value.copy(sendAllChecked = it in listOf(ToggleState.CHECKED, ToggleState.CONFIRMED)) + }.launchIn(this) + + lockInputFlow.onEach { isInputLocked -> + state.value = state.value.copy( + isInputLocked = isInputLocked, + addressInputState = state.value.addressInputState.copy(showClear = isInputLocked.not()) + ) + }.launchIn(this) + + assetFlow.onEach { asset -> + val quickAmountInputValues = if (asset?.token?.configuration?.currencyId == bokoloCashTokenId) { + emptyList() + } else { + QuickAmountInput.entries + } + + val existentialDeposit = asset?.token?.configuration?.let { existentialDepositUseCase(it) }.orZero() + val sendAllAllowed = existentialDeposit > BigInteger.ZERO + + state.value = state.value.copy( + quickAmountInputValues = quickAmountInputValues, + sendAllAllowed = sendAllAllowed + ) + }.launchIn(this) + + combine( + selectedChain, + addressInputTrimmedFlow + ) { chain, address -> + val isAddressValid = when (chain) { + null -> false + else -> walletInteractor.validateSendAddress(chain.id, address) + } + + val image: Any = if (isAddressValid.not()) { + R.drawable.ic_address_placeholder + } else { + addressIconGenerator.createAddressIcon( + chain?.isEthereumBased == true, + address, + AddressIconGenerator.SIZE_BIG + ) + } + + state.value = state.value.copy( + addressInputState = state.value.addressInputState.copy( + input = address, + image = image + ), + isHistoryAvailable = chain?.externalApi?.history != null + ) + }.launchIn(this) } private fun observeExistentialDeposit(showMaxInput: Boolean) { @@ -823,6 +855,9 @@ class SendSetupViewModel @Inject constructor( if (checked) { onQuickAmountInput(1.0) + } else { + visibleAmountFlow.value = BigDecimal.ZERO + initialAmountFlow.value = BigDecimal.ZERO } } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt index f5032b8d29..267d2e26c9 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailFragment.kt @@ -74,7 +74,7 @@ class ExtrinsicDetailFragment : BaseFragment(R.layout. ) = showExternalActionsSheet( copyLabelRes = R.string.common_copy_address, value = address, - explorers = viewModel.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address), + explorers = viewModel.getSupportedAddressExplorers(address), externalViewCallback = viewModel::openUrl ) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt index 1701b41956..282d47c9cd 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/extrinsic/ExtrinsicDetailViewModel.kt @@ -21,6 +21,8 @@ import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers import kotlinx.coroutines.flow.flow import javax.inject.Inject +import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers private const val ICON_SIZE_DP = 32 @@ -55,6 +57,9 @@ class ExtrinsicDetailViewModel @Inject constructor( fun getSupportedExplorers(type: BlockExplorerUrlBuilder.Type, value: String) = chainExplorers.replayCache.firstOrNull()?.getSupportedExplorers(type, value).orEmpty() + fun getSupportedAddressExplorers(address: String): Map = + chainExplorers.replayCache.firstOrNull()?.getSupportedAddressExplorers(address).orEmpty() + private suspend fun getIcon(address: String) = addressIconGenerator.createAddressModel( address, ICON_SIZE_DP, diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailFragment.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailFragment.kt index bfdc425969..a8bd83ee92 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailFragment.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailFragment.kt @@ -92,7 +92,7 @@ class RewardDetailFragment : BaseFragment(R.layout.fragme private fun showExternalAddressActions(address: String) = showExternalActionsSheet( copyLabelRes = R.string.common_copy_address, value = address, - explorers = viewModel.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address), + explorers = viewModel.getSupportedAddressExplorers(address), externalViewCallback = viewModel::openUrl ) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailViewModel.kt index e65604cef9..0f65aa1ff9 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/reward/RewardDetailViewModel.kt @@ -21,6 +21,8 @@ import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers import kotlinx.coroutines.flow.flow import javax.inject.Inject +import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers private const val ICON_SIZE_DP = 32 @@ -61,6 +63,9 @@ class RewardDetailViewModel @Inject constructor( fun getSupportedExplorers(type: BlockExplorerUrlBuilder.Type, value: String) = chainExplorers.replayCache.firstOrNull()?.getSupportedExplorers(type, value).orEmpty() + fun getSupportedAddressExplorers(address: String): Map = + chainExplorers.replayCache.firstOrNull()?.getSupportedAddressExplorers(address).orEmpty() + private suspend fun getIcon(address: String) = addressIconGenerator.createAddressModel(address, ICON_SIZE_DP, addressDisplayUseCase(address)) fun openUrl(url: String) { diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransactionDetailViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransactionDetailViewModel.kt index 9a33951f07..3040bf45fb 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransactionDetailViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransactionDetailViewModel.kt @@ -18,6 +18,7 @@ import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.feature_wallet_impl.R import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain +import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedAddressExplorers import jp.co.soramitsu.runtime.multiNetwork.chain.model.getSupportedExplorers import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import jp.co.soramitsu.wallet.impl.presentation.AssetPayload @@ -45,7 +46,7 @@ class TransactionDetailViewModel @Inject constructor( val operation = savedStateHandle.get(KEY_TRANSACTION)!! val assetPayload = savedStateHandle.get(KEY_ASSET_PAYLOAD)!! - val historyType = savedStateHandle.get(KEY_HISTORY_TYPE)!! + val explorerType = savedStateHandle.get(KEY_EXPLORER_TYPE) private val _showExternalViewEvent = MutableLiveData>() val showExternalTransactionActionsEvent: LiveData> = _showExternalViewEvent @@ -62,16 +63,18 @@ class TransactionDetailViewModel @Inject constructor( private val chainExplorers = flow { emit(chainRegistry.getChain(assetPayload.chainId).explorers) }.share() - fun getSupportedExplorers(historyType: Chain.ExternalApi.Section.Type, value: String): Map { - val explorerUrlType: BlockExplorerUrlBuilder.Type = when (historyType) { - Chain.ExternalApi.Section.Type.ETHERSCAN -> BlockExplorerUrlBuilder.Type.TX - Chain.ExternalApi.Section.Type.REEF -> BlockExplorerUrlBuilder.Type.TRANSFER + fun getSupportedExplorers(explorerType: Chain.Explorer.Type, value: String): Map { + val explorerUrlType: BlockExplorerUrlBuilder.Type = when (explorerType) { + Chain.Explorer.Type.OKLINK, + Chain.Explorer.Type.ETHERSCAN -> BlockExplorerUrlBuilder.Type.TX + Chain.Explorer.Type.REEF -> BlockExplorerUrlBuilder.Type.TRANSFER else -> BlockExplorerUrlBuilder.Type.EXTRINSIC } return chainExplorers.replayCache.firstOrNull()?.getSupportedExplorers(explorerUrlType, value).orEmpty() } - fun getSupportedExplorers(type: BlockExplorerUrlBuilder.Type, value: String) = - chainExplorers.replayCache.firstOrNull()?.getSupportedExplorers(type, value).orEmpty() + + fun getSupportedAddressExplorers(address: String): Map = + chainExplorers.replayCache.firstOrNull()?.getSupportedAddressExplorers(address).orEmpty() val retryAddressModelLiveData = if (operation.isIncome) senderAddressModelLiveData else recipientAddressModelLiveData diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransferDetailFragment.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransferDetailFragment.kt index 4f09288501..44bf2ab7b2 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransferDetailFragment.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/detail/transfer/TransferDetailFragment.kt @@ -25,17 +25,17 @@ import jp.co.soramitsu.wallet.impl.presentation.model.OperationStatusAppearance const val KEY_TRANSACTION = "KEY_DRAFT" const val KEY_ASSET_PAYLOAD = "KEY_ASSET_PAYLOAD" -const val KEY_HISTORY_TYPE = "KEY_HISTORY_TYPE" +const val KEY_EXPLORER_TYPE = "KEY_EXPLORER_TYPE" @AndroidEntryPoint class TransferDetailFragment : BaseFragment(R.layout.fragment_transfer_details) { companion object { - fun getBundle(operation: OperationParcelizeModel.Transfer, assetPayload: AssetPayload, chainHistoryType: Chain.ExternalApi.Section.Type?) = + fun getBundle(operation: OperationParcelizeModel.Transfer, assetPayload: AssetPayload, chainExplorerType: Chain.Explorer.Type?) = bundleOf( KEY_TRANSACTION to operation, KEY_ASSET_PAYLOAD to assetPayload, - KEY_HISTORY_TYPE to chainHistoryType + KEY_EXPLORER_TYPE to chainExplorerType ) } @@ -148,7 +148,7 @@ class TransferDetailFragment : BaseFragment(R.layout private fun showExternalAddressActions(address: String) = showExternalActionsSheet( copyLabelRes = R.string.common_copy_address, value = address, - explorers = viewModel.getSupportedExplorers(BlockExplorerUrlBuilder.Type.ACCOUNT, address), + explorers = viewModel.getSupportedAddressExplorers(address), externalViewCallback = viewModel::openUrl ) @@ -156,7 +156,7 @@ class TransferDetailFragment : BaseFragment(R.layout showExternalActionsSheet( copyLabelRes = R.string.transaction_details_copy_hash, value = hash, - explorers = viewModel.getSupportedExplorers(viewModel.historyType, hash), + explorers = viewModel.explorerType?.let { viewModel.getSupportedExplorers(it, hash) }.orEmpty(), externalViewCallback = viewModel::openUrl ) } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryMixin.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryMixin.kt index 5a0fe35a0d..aac4686f99 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryMixin.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryMixin.kt @@ -29,8 +29,7 @@ interface TransactionHistoryUi { fun transactionClicked( transactionModel: OperationModel, - assetPayload: AssetPayload, - chainHistoryType: Chain.ExternalApi.Section.Type? + assetPayload: AssetPayload ) } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryProvider.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryProvider.kt index d355c45132..87b1931ab0 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryProvider.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/transaction/history/mixin/TransactionHistoryProvider.kt @@ -207,8 +207,7 @@ class TransactionHistoryProvider( override fun transactionClicked( transactionModel: OperationModel, - assetPayload: AssetPayload, - chainHistoryType: Chain.ExternalApi.Section.Type? + assetPayload: AssetPayload ) { launch { val operations = currentData @@ -217,11 +216,12 @@ class TransactionHistoryProvider( val chain = walletInteractor.getChain(assetPayload.chainId) val utilityAsset = chain.assets.firstOrNull { it.isUtility } + val chainExplorerType: Chain.Explorer.Type? = chain.explorers.firstOrNull()?.type withContext(Dispatchers.Main) { when (val operation = mapOperationToParcel(clickedOperation, resourceManager, utilityAsset)) { is OperationParcelizeModel.Transfer -> { - router.openTransferDetail(operation, assetPayload, chainHistoryType) + router.openTransferDetail(operation, assetPayload, chainExplorerType) } is OperationParcelizeModel.Extrinsic -> { diff --git a/feature-walletconnect-api/src/main/java/co/jp/soramitsu/walletconnect/domain/WalletConnectRouter.kt b/feature-walletconnect-api/src/main/java/co/jp/soramitsu/walletconnect/domain/WalletConnectRouter.kt index c3b788496e..e9771463e5 100644 --- a/feature-walletconnect-api/src/main/java/co/jp/soramitsu/walletconnect/domain/WalletConnectRouter.kt +++ b/feature-walletconnect-api/src/main/java/co/jp/soramitsu/walletconnect/domain/WalletConnectRouter.kt @@ -18,7 +18,8 @@ interface WalletConnectRouter { fun openOperationSuccessAndPopUpToNearestRelatedScreen( operationHash: String?, chainId: ChainId?, - customMessage: String? + customMessage: String?, + customTitle: String? = null ) fun openSelectMultipleChains( diff --git a/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/connectioninfo/ConnectionInfoViewModel.kt b/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/connectioninfo/ConnectionInfoViewModel.kt index a7c0e62f60..0eadb47c28 100644 --- a/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/connectioninfo/ConnectionInfoViewModel.kt +++ b/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/connectioninfo/ConnectionInfoViewModel.kt @@ -135,7 +135,8 @@ class ConnectionInfoViewModel @Inject constructor( walletConnectRouter.openOperationSuccessAndPopUpToNearestRelatedScreen( null, null, - resourceManager.getString(R.string.connection_disconnect_success_message, dappName) + resourceManager.getString(R.string.connection_disconnect_success_message, dappName), + resourceManager.getString(R.string.all_done) ) } }, diff --git a/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/sessionproposal/SessionProposalViewModel.kt b/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/sessionproposal/SessionProposalViewModel.kt index 86c078b1af..ef3079dd37 100644 --- a/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/sessionproposal/SessionProposalViewModel.kt +++ b/feature-walletconnect-impl/src/main/java/jp/co/soramitsu/walletconnect/impl/presentation/sessionproposal/SessionProposalViewModel.kt @@ -229,7 +229,8 @@ class SessionProposalViewModel @Inject constructor( walletConnectRouter.openOperationSuccessAndPopUpToNearestRelatedScreen( null, null, - resourceManager.getString(R.string.connection_approve_success_message, proposal.name) + resourceManager.getString(R.string.connection_approve_success_message, proposal.name), + resourceManager.getString(R.string.all_done) ) } isApproving.value = false @@ -273,7 +274,8 @@ class SessionProposalViewModel @Inject constructor( walletConnectRouter.openOperationSuccessAndPopUpToNearestRelatedScreen( null, null, - resourceManager.getString(R.string.common_rejected) + resourceManager.getString(R.string.common_rejected), + resourceManager.getString(R.string.all_done) ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5179cfd2c..8e832ea416 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] -accompanistVersion = "0.28.0" +accompanistVersion = "0.34.0" activityCompose = "1.8.2" -android_plugin = "8.2.2" +android_plugin = "8.3.2" appcompat = "1.6.1" architectureComponentVersion = "2.7.0" beaconVersion = "3.2.4" biometricVersion = "1.1.0" -bouncyCastleVersion = "1.77" +bouncyCastleVersion = "1.78" cardViewVersion = "1.0.0" -coilVersion = "2.2.2" -compose = "1.6.3" -composeCompiler = "1.5.10" +coilVersion = "2.6.0" +compose = "1.6.5" +composeCompiler = "1.5.11" composeShimmer = "1.0.4" composeThemeAdapter = "1.2.1" constraintlayoutComposeVersion = "1.0.1" @@ -20,45 +20,46 @@ coroutines = "1.8.0" customview = "1.2.0-alpha02" customviewPoolingcontainer = "1.0.0" dagger = "2.49" -detekt = "1.23.0" +detekt = "1.23.6" firebaseAppdistributionGradle = "4.2.0" fragmentKtx = "1.6.2" googleServices = "4.4.1" gson = "2.10.1" hiltNavComposeVersion = "1.2.0" insetterVersion = "0.5.0" -jna = "5.9.0" +jna = "5.14.0" junit = "4.13.2" junitVersion = "1.1.5" -kotlin = "1.9.22" +kotlin = "1.9.23" kotlinxSerializationjson = "1.6.3" legacySupportV4 = "1.0.0" material = "1.11.0" +middleEllipsisTextVersion = "1.1.0" mockitoKotlin = "5.2.1" mockitoVersion = "5.10.0" mockitoInlineVersion = "5.2.0" navControllerVersion = "2.7.7" okhttpVersion = "4.11.0" opencsv = "5.7.1" -orgJacocoCore = "0.8.8" -playPublisher = "3.8.4" +orgJacocoCore = "0.8.12" +playPublisher = "3.9.1" progressButtonsVersion = "2.1.0" recyclerviewVersion = "1.3.2" retrofit = "2.9.0" roomVersion = "2.6.1" rules = "1.5.0" runner = "1.5.2" -sharedFeaturesVersion = "1.1.1.29-FLW" +sharedFeaturesVersion = "1.1.1.30-FLW" shimmerVersion = "0.5.0" sonarqubeGradlePlugin = "3.3" -soraUiCore = "0.2.20" +soraUiCore = "0.2.22" storiesVersion = "3.0.1" -walletconnectBom = "1.18.0" +walletconnectBom = "1.31.4" web3j = "4.8.8-android" wsVersion = "2.14" xNetworking = "0.2.5-temp7" zxingEmbeddedVersion = "4.3.0" -zxingVersion = "3.5.1" +zxingVersion = "3.5.3" [libraries] appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } @@ -88,6 +89,7 @@ lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.r lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "architectureComponentVersion" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "architectureComponentVersion" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttpVersion" } +middle-ellipsis-text = { module = "io.github.mataku:middle-ellipsis-text", version.ref = "middleEllipsisTextVersion" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoVersion" } mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoInlineVersion" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c657399023..a781b73d47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Aug 04 19:19:31 YEKT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/di/ChainRegistryModule.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/di/ChainRegistryModule.kt index a64ab61a8a..2f2a359efa 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/di/ChainRegistryModule.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/di/ChainRegistryModule.kt @@ -8,13 +8,15 @@ import javax.inject.Provider import javax.inject.Singleton import jp.co.soramitsu.common.data.network.NetworkApiCreator import jp.co.soramitsu.common.data.storage.Preferences +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.interfaces.FileProvider -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.core.network.JsonFactory import jp.co.soramitsu.core.runtime.ChainConnection import jp.co.soramitsu.core.runtime.RuntimeFactory +import jp.co.soramitsu.coredb.dao.AssetDao import jp.co.soramitsu.coredb.dao.ChainDao +import jp.co.soramitsu.coredb.dao.MetaAccountDao import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.ChainSyncService import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository @@ -44,8 +46,10 @@ class ChainRegistryModule { @Singleton fun provideChainSyncService( dao: ChainDao, - chainFetcher: ChainFetcher - ) = ChainSyncService(dao, chainFetcher) + chainFetcher: ChainFetcher, + metaAccountDao: MetaAccountDao, + assetDao: AssetDao, + ) = ChainSyncService(dao, chainFetcher, metaAccountDao, assetDao) @Provides @Singleton @@ -94,13 +98,13 @@ class ChainRegistryModule { runtimeSyncService: RuntimeSyncService, runtimeFilesCache: RuntimeFilesCache, chainDao: ChainDao, - networkStateMixin: NetworkStateMixin + networkStateService: NetworkStateService ) = RuntimeProviderPool( runtimeFactory, runtimeSyncService, runtimeFilesCache, chainDao, - networkStateMixin + networkStateService ) @Provides @@ -113,20 +117,21 @@ class ChainRegistryModule { socketProvider: Provider, externalRequirementsFlow: MutableStateFlow, nodesSettingsStorage: NodesSettingsStorage, - networkStateMixin: NetworkStateMixin + networkStateService: NetworkStateService ) = ConnectionPool( socketProvider, externalRequirementsFlow, nodesSettingsStorage, - networkStateMixin + networkStateService ) @Provides @Singleton fun provideRuntimeVersionSubscriptionPool( chainDao: ChainDao, - runtimeSyncService: RuntimeSyncService - ) = RuntimeSubscriptionPool(chainDao, runtimeSyncService) + runtimeSyncService: RuntimeSyncService, + networkStateService: NetworkStateService + ) = RuntimeSubscriptionPool(chainDao, runtimeSyncService, networkStateService) @Provides @Singleton @@ -136,9 +141,10 @@ class ChainRegistryModule { @Provides @Singleton fun provideEthereumPool( - networkStateMixin: NetworkStateMixin + networkStateService: NetworkStateService ) = - EthereumConnectionPool(networkStateMixin) + EthereumConnectionPool(networkStateService) + @Provides @Singleton @@ -150,8 +156,10 @@ class ChainRegistryModule { chainSyncService: ChainSyncService, runtimeSyncService: RuntimeSyncService, updatesMixin: UpdatesMixin, - networkStateMixin: NetworkStateMixin, - ethereumConnectionPool: EthereumConnectionPool + networkStateService: NetworkStateService, + ethereumConnectionPool: EthereumConnectionPool, + assetReadOnlyCache: AssetDao, + chainsRepository: ChainsRepository, ): ChainRegistry = ChainRegistry( runtimeProviderPool, chainConnectionPool, @@ -160,8 +168,10 @@ class ChainRegistryModule { chainSyncService, runtimeSyncService, updatesMixin, - networkStateMixin, - ethereumConnectionPool + networkStateService, + ethereumConnectionPool, + assetReadOnlyCache, + chainsRepository ) @Provides diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/di/RuntimeModule.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/di/RuntimeModule.kt index ce3414cdf3..02ad3e28e9 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/di/RuntimeModule.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/di/RuntimeModule.kt @@ -54,12 +54,18 @@ class RuntimeModule { ): StorageDataSource = LocalStorageSource(chainRegistry, storageCache) @Provides - @Named(REMOTE_STORAGE_SOURCE) @Singleton fun provideRemoteStorageSource( chainRegistry: ChainRegistry, bulkRetriever: BulkRetriever - ): StorageDataSource = RemoteStorageSource(chainRegistry, bulkRetriever) + ): RemoteStorageSource = RemoteStorageSource(chainRegistry, bulkRetriever) + + @Provides + @Named(REMOTE_STORAGE_SOURCE) + @Singleton + fun provideRemoteStorageDataSource( + remoteStorageSource: RemoteStorageSource + ): StorageDataSource = remoteStorageSource @Provides @Singleton diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/ChainRegistry.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/ChainRegistry.kt index 364ce67809..dc4beaa127 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/ChainRegistry.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/ChainRegistry.kt @@ -2,30 +2,26 @@ package jp.co.soramitsu.runtime.multiNetwork import android.util.Log import javax.inject.Inject -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState -import jp.co.soramitsu.common.compose.component.NetworkIssueType -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.mixin.api.UpdatesMixin import jp.co.soramitsu.common.mixin.api.UpdatesProviderUi import jp.co.soramitsu.common.utils.diffed -import jp.co.soramitsu.common.utils.inBackground +import jp.co.soramitsu.common.utils.failure import jp.co.soramitsu.common.utils.mapList -import jp.co.soramitsu.common.utils.requireException -import jp.co.soramitsu.common.utils.requireValue import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.core.models.IChain import jp.co.soramitsu.core.runtime.ChainConnection import jp.co.soramitsu.core.runtime.IChainRegistry -import jp.co.soramitsu.core.utils.utilityAsset +import jp.co.soramitsu.coredb.dao.AssetReadOnlyCache import jp.co.soramitsu.coredb.dao.ChainDao import jp.co.soramitsu.coredb.model.chain.ChainNodeLocal import jp.co.soramitsu.runtime.multiNetwork.chain.ChainSyncService +import jp.co.soramitsu.runtime.multiNetwork.chain.ChainsRepository import jp.co.soramitsu.runtime.multiNetwork.chain.mapChainLocalToChain import jp.co.soramitsu.runtime.multiNetwork.chain.mapNodeLocalToNode import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.NodeId -import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId import jp.co.soramitsu.runtime.multiNetwork.connection.ConnectionPool import jp.co.soramitsu.runtime.multiNetwork.connection.EthereumConnectionPool import jp.co.soramitsu.runtime.multiNetwork.runtime.RuntimeProvider @@ -33,18 +29,27 @@ import jp.co.soramitsu.runtime.multiNetwork.runtime.RuntimeProviderPool import jp.co.soramitsu.runtime.multiNetwork.runtime.RuntimeSubscriptionPool import jp.co.soramitsu.runtime.multiNetwork.runtime.RuntimeSyncService import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.shared_utils.wsrpc.state.SocketStateMachine +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext data class ChainService( @@ -60,154 +65,184 @@ class ChainRegistry @Inject constructor( private val chainSyncService: ChainSyncService, private val runtimeSyncService: RuntimeSyncService, private val updatesMixin: UpdatesMixin, - private val networkStateMixin: NetworkStateMixin, - private val ethereumConnectionPool: EthereumConnectionPool -) : IChainRegistry, CoroutineScope by CoroutineScope(Dispatchers.Default), - UpdatesProviderUi by updatesMixin { + private val networkStateService: NetworkStateService, + private val ethereumConnectionPool: EthereumConnectionPool, + assetsCache: AssetReadOnlyCache, + private val chainsRepository: ChainsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default +) : IChainRegistry, UpdatesProviderUi by updatesMixin { - val syncedChains = MutableSharedFlow>() + val scope = CoroutineScope(dispatcher + SupervisorJob()) + + val syncedChains = MutableStateFlow>(emptyList()) val currentChains = syncedChains .filter { it.isNotEmpty() } .distinctUntilChanged() - .shareIn(this, SharingStarted.Eagerly, replay = 1) + .shareIn(scope, SharingStarted.Eagerly, replay = 1) + + private val enabledAssetsFlow = assetsCache.observeAllEnabledAssets() + .onStart { emit(emptyList()) } + + private val chainsToSync = chainDao.joinChainInfoFlow() + .mapList(::mapChainLocalToChain) + .combine(enabledAssetsFlow) { chains, enabledAssets -> + val popularChains = chains.filter { it.rank != null } + val enabledChains = + enabledAssets.mapNotNull { asset -> chains.find { chain -> chain.id == asset.chainId } } + val chainsWithCrowdloans = chains.filter { it.hasCrowdloans } + val chainsWithStaking = chains.filter { + it.assets.any { asset -> asset.staking == Asset.StakingType.PARACHAIN || asset.staking == Asset.StakingType.RELAYCHAIN || asset.supportStakingPool } + } + val identityHolders = + chains.filter { chain -> chain.identityChain != null }.map { it.identityChain } + .mapNotNull { identityChain -> chains.find { it.id == identityChain } } + + (popularChains + enabledChains + chainsWithCrowdloans + chainsWithStaking + identityHolders).toSet() + .filter { /*it.disabled*/ it.nodes.isNotEmpty() } + } + .diffed() + .filter { it.addedOrModified.isNotEmpty() || it.removed.isNotEmpty() } + .flowOn(dispatcher) - val chainsById = currentChains.map { chains -> chains.associateBy { it.id } } - .inBackground() - .shareIn(this, SharingStarted.Eagerly, replay = 1) + var configsSyncDeferred: MutableList> = mutableListOf() init { syncUp() } + suspend fun syncConfigs() = withContext(dispatcher) { + val chainSyncDeferred = async { chainSyncService.syncUp() } + val typesResultDeferred = async { runtimeSyncService.syncTypes() } + + configsSyncDeferred.add(chainSyncDeferred) + configsSyncDeferred.add(typesResultDeferred) + + val chainsSyncResult = chainSyncDeferred.await() + val typesResult = typesResultDeferred.await() + + return@withContext if(chainsSyncResult.isSuccess && typesResult.isSuccess) { + Result.success(Unit) + } else { + Result.failure("failed to load chains configs") + } + } + fun syncUp() { - launch { - runCatching { - chainSyncService.syncUp() - runtimeSyncService.syncTypes() - } - chainDao.joinChainInfoFlow() - .mapList(::mapChainLocalToChain) - .diffed() - .filter { it.addedOrModified.isNotEmpty() || it.removed.isNotEmpty() } - .collect { (removed, addedOrModified, all) -> - val s = supervisorScope { - runCatching { - removed.forEach { - val chainId = it.id - if (it.isEthereumChain) { - ethereumConnectionPool.stop(chainId) - return@forEach + scope.launch { + configsSyncDeferred.joinAll() + + chainsToSync + .onEach { (removed, addedOrModified, all) -> + coroutineScope { + val removedDeferred = removed.map { + async { connectionPool.getConnectionOrNull(it.id)?.socketService?.pause() } + } + + updatesMixin.startChainsSyncUp(addedOrModified.filter { it.nodes.isNotEmpty() } + .map { it.id }) + + val syncDeferred = addedOrModified.map { chain -> + async { + runCatching { + setupChain(chain) + }.onFailure { + networkStateService.notifyChainSyncProblem(chain.id) + Log.e( + "ChainRegistry", + "error while sync in chain registry $it" + ) + }.onSuccess { + networkStateService.notifyChainSyncSuccess( + chain.id + ) } - runtimeProviderPool.removeRuntimeProvider(chainId) - runtimeSubscriptionPool.removeSubscription(chainId) - runtimeSyncService.unregisterChain(chainId) - connectionPool.removeConnection(chainId) } - updatesMixin.startChainsSyncUp(addedOrModified.filter { it.nodes.isNotEmpty() } - .map { it.id }) - addedOrModified - .filter { /*it.disabled*/ it.nodes.isNotEmpty() } - .map { chain -> - launch chainLaunch@{ - if (chain.isEthereumChain) { - runCatching { - ethereumConnectionPool.setupConnection( - chain, - onSelectedNodeChange = { chainId, newNodeUrl -> - notifyNodeSwitched(NodeId(chainId to newNodeUrl)) - }) - } - return@chainLaunch - } - - runCatching { - val connection = connectionPool.setupConnection( - chain, - onSelectedNodeChange = { chainId, newNodeUrl -> - notifyNodeSwitched(NodeId(chainId to newNodeUrl)) - } - ) - runtimeSubscriptionPool.setupRuntimeSubscription( - chain, - connection - ) - runtimeSyncService.registerChain(chain) - runtimeProviderPool.setupRuntimeProvider(chain) - }.onFailure { networkStateMixin.notifyChainSyncProblem(chain.toSyncIssue()) } - .onSuccess { - networkStateMixin.notifyChainSyncSuccess( - chain.id - ) - } - } - } } - }.onFailure { - Log.e( - "ChainRegistry", - "error while sync in chain registry $it" - ) - }.requireValue() - joinAll(*s.toTypedArray()) + + (removedDeferred + syncDeferred).awaitAll() + } this@ChainRegistry.syncedChains.emit(all) + } + .launchIn(scope) } } - override fun getConnection(chainId: String) = connectionPool.getConnection(chainId) - - @Deprecated( - "Since we have ethereum chains, which don't have runtime, we must use the function with nullable return value", - ReplaceWith("getRuntimeOrNull(chainId)") - ) - override suspend fun getRuntime(chainId: ChainId): RuntimeSnapshot { - return getRuntimeProvider(chainId).get() + fun stopChain(chain: Chain) { + val chainId = chain.id + if (chain.isEthereumChain) { + ethereumConnectionPool.stop(chainId) + return + } + runtimeProviderPool.removeRuntimeProvider(chainId) + runtimeSubscriptionPool.removeSubscription(chainId) + runtimeSyncService.unregisterChain(chainId) + connectionPool.removeConnection(chainId) } - suspend fun getRuntimeOrNull(chainId: ChainId): RuntimeSnapshot? { - return kotlin.runCatching { getRuntimeProvider(chainId).getOrNullWithTimeout() }.getOrNull() - } + suspend fun setupChain(chain: Chain) { + if (chain.isEthereumChain) { + ethereumConnectionPool.setupConnection(chain, ::notifyNodeSwitched) + return + } - fun getConnectionOrNull(chainId: String) = connectionPool.getConnectionOrNull(chainId) + val connection = connectionPool.getConnectionOrNull(chain.id)?.let { + if (it.state.value is SocketStateMachine.State.Paused) { + it.socketService.resume() + } + it + } ?: connectionPool.setupConnection(chain, ::notifyNodeSwitched) - suspend fun getRuntimeProvider(chainId: String): RuntimeProvider { - return runtimeProviderPool.getRuntimeProvider(chainId) - } + if (runtimeProviderPool.getRuntimeProviderOrNull(chain.id)?.getOrNull() != null) return - suspend fun getRuntimeProviderOrNull(chainId: String): RuntimeProvider? { - return runtimeProviderPool.getRuntimeProviderOrNull(chainId) + if (connection.state.value !is SocketStateMachine.State.Connected) { + connection.socketService.start(chain.nodes.first().url) + } + + runtimeSubscriptionPool.setupRuntimeSubscription(chain, connection) + runtimeSyncService.registerChain(chain) + runtimeProviderPool.setupRuntimeProvider(chain) } - fun getAsset(chainId: ChainId, chainAssetId: String) = - chainsById.replayCache.lastOrNull()?.get(chainId)?.assets?.firstOrNull { - it.id == chainAssetId + suspend fun checkChainSyncedUp(chain: Chain): Boolean { + if (chain.isEthereumChain) { + return ethereumConnectionPool.getOrNull(chain.id) != null } + val runtime = runtimeProviderPool.getRuntimeProviderOrNull(chain.id)?.getOrNull() + + return connectionPool.getConnectionOrNull(chain.id) != null && runtime != null + } + + suspend fun getAsset(chainId: ChainId, chainAssetId: String): Asset? { + return getChain(chainId).assetsById[chainAssetId] + } override suspend fun getChain(chainId: ChainId): Chain { - return chainsById.first().getValue(chainId) + return chainsRepository.getChain(chainId) } override suspend fun getChains(): List { - return chainsById.first().values.toList() + return chainsRepository.getChains() } fun nodesFlow(chainId: String) = chainDao.nodesFlow(chainId) .mapList(::mapNodeLocalToNode) suspend fun switchNode(id: NodeId) { - withContext(Dispatchers.Default) { + withContext(dispatcher) { val chain = getChain(id.chainId) if (!chain.isEthereumChain) { - connectionPool.getConnection(id.chainId).socketService.switchUrl(id.nodeUrl) - notifyNodeSwitched(id) + connectionPool.getConnectionOrNull(id.chainId)?.socketService?.switchUrl(id.nodeUrl)?.let { + notifyNodeSwitched(id.chainId, id.nodeUrl) + } } } } - private fun notifyNodeSwitched(id: NodeId) { - launch(Dispatchers.IO) { - chainDao.selectNode(id.chainId, id.nodeUrl) + private fun notifyNodeSwitched(chainId: ChainId, nodeUrl: String) { + scope.launch { + chainDao.selectNode(chainId, nodeUrl) } } @@ -224,7 +259,8 @@ class ChainRegistry @Inject constructor( suspend fun deleteNode(id: NodeId) = chainDao.deleteNode(id.chainId, id.nodeUrl) - suspend fun getNode(id: NodeId) = mapNodeLocalToNode(chainDao.getNode(id.chainId, id.nodeUrl)) + suspend fun getNode(id: NodeId) = + mapNodeLocalToNode(chainDao.getNode(id.chainId, id.nodeUrl)) suspend fun updateNode(id: NodeId, name: String, url: String) = chainDao.updateNode(id.chainId, id.nodeUrl, name, url) @@ -232,56 +268,47 @@ class ChainRegistry @Inject constructor( suspend fun getRemoteRuntimeVersion(chainId: ChainId): Int? { return chainDao.runtimeInfo(chainId)?.remoteVersion } -} -suspend fun ChainRegistry.getChain(chainId: ChainId): Chain { - return getChain(chainId) -} + suspend fun chainWithAsset( + chainId: ChainId, + assetId: String + ): Pair { + val chain = getChain(chainId) -suspend fun ChainRegistry.chainWithAsset(chainId: ChainId, assetId: String): Pair { - val chain = chainsById.first().getValue(chainId) + return chain to chain.assetsById.getValue(assetId) + } - return chain to chain.assetsById.getValue(assetId) -} + override fun getConnection(chainId: String) = connectionPool.getConnectionOrThrow(chainId) -suspend fun ChainRegistry.getRuntime(chainId: ChainId): RuntimeSnapshot { - return getRuntimeProvider(chainId).get() -} + suspend fun awaitConnection(chainId: ChainId) = connectionPool.awaitConnection(chainId) + @Deprecated( + "Since we have ethereum chains, which don't have runtime, we must use the function with nullable return value", + ReplaceWith("getRuntimeOrNull(chainId)") + ) + override suspend fun getRuntime(chainId: ChainId): RuntimeSnapshot { + return awaitRuntimeProvider(chainId).get() + } -suspend fun ChainRegistry.getRuntimeOrNull(chainId: ChainId): RuntimeSnapshot? { - return getRuntimeProviderOrNull(chainId)?.getOrNull() -} + suspend fun getRuntimeOrNull(chainId: ChainId): RuntimeSnapshot? { + return getRuntimeProviderOrNull(chainId)?.getOrNull() + } -suspend fun ChainRegistry.getRuntimeCatching(chainId: ChainId): Result { - val providerResult = kotlin.runCatching { getRuntimeProvider(chainId) } + suspend fun awaitRuntimeProvider(chainId: String): RuntimeProvider { + return runtimeProviderPool.awaitRuntimeProvider(chainId) + } - return if (providerResult.isFailure) { - Result.failure(providerResult.requireException()) - } else { - kotlin.runCatching { providerResult.requireValue().get() } + fun getRuntimeProviderOrNull(chainId: String): RuntimeProvider? { + return runtimeProviderPool.getRuntimeProviderOrNull(chainId) } -} -fun ChainRegistry.getSocket(chainId: ChainId) = getConnection(chainId).socketService -fun ChainRegistry.getSocketOrNull(chainId: ChainId) = getConnectionOrNull(chainId)?.socketService + fun getEthereumConnectionOrNull(chainId: String) = ethereumConnectionPool.getOrNull(chainId) + suspend fun awaitEthereumConnection(chainId: String) = ethereumConnectionPool.await(chainId) -suspend fun ChainRegistry.getService(chainId: ChainId): ChainService { - return ChainService( - runtimeProvider = getRuntimeProvider(chainId), - connection = getConnection(chainId) - ) + suspend fun getService(chainId: ChainId): ChainService { + return ChainService( + runtimeProvider = awaitRuntimeProvider(chainId), + connection = getConnection(chainId) + ) + } } -fun Chain.toSyncIssue(): NetworkIssueItemState { - return NetworkIssueItemState( - iconUrl = this.icon, - title = this.name, - type = when { - this.nodes.size > 1 -> NetworkIssueType.Node - else -> NetworkIssueType.Network - }, - chainId = this.id, - chainName = this.name, - assetId = this.utilityAsset?.id.orEmpty() - ) -} diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/ChainSyncService.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/ChainSyncService.kt index 80f3f21d99..90a7036b63 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/ChainSyncService.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/ChainSyncService.kt @@ -1,48 +1,80 @@ package jp.co.soramitsu.runtime.multiNetwork.chain +import jp.co.soramitsu.coredb.dao.AssetDao import jp.co.soramitsu.coredb.dao.ChainDao +import jp.co.soramitsu.coredb.dao.MetaAccountDao +import jp.co.soramitsu.coredb.model.AssetLocal import jp.co.soramitsu.coredb.model.chain.JoinedChainInfo import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain -import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId -import jp.co.soramitsu.runtime.multiNetwork.chain.model.reefChainId import jp.co.soramitsu.runtime.multiNetwork.chain.remote.ChainFetcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class ChainSyncService( private val dao: ChainDao, - private val chainFetcher: ChainFetcher + private val chainFetcher: ChainFetcher, + private val metaAccountDao: MetaAccountDao, + private val assetsDao: AssetDao ) { suspend fun syncUp() = withContext(Dispatchers.Default) { - val localChainsJoinedInfo = dao.getJoinChainInfo() + runCatching { + val localChainsJoinedInfo = dao.getJoinChainInfo() - val remoteChains = chainFetcher.getChains() - .filter { - !it.disabled && (it.assets?.isNotEmpty() == true) - } - .map { - it.toChain() - } + val remoteChains = chainFetcher.getChains() + .filter { + !it.disabled && (it.assets?.isNotEmpty() == true) + } + .map { + it.toChain() + } - val localChains = localChainsJoinedInfo.map(::mapChainLocalToChain) + val localChains = localChainsJoinedInfo.map(::mapChainLocalToChain) - val remoteMapping = remoteChains.associateBy(Chain::id) - val localMapping = localChains.associateBy(Chain::id) + val remoteMapping = remoteChains.associateBy(Chain::id) + val localMapping = localChains.associateBy(Chain::id) - val newOrUpdated = remoteChains.mapNotNull { remoteChain -> - val localVersion = localMapping[remoteChain.id] + val newOrUpdated = remoteChains.mapNotNull { remoteChain -> + val localVersion = localMapping[remoteChain.id] - when { - localVersion == null -> remoteChain // new - localVersion != remoteChain -> remoteChain // updated - else -> null // same - } - }.map(::mapChainToChainLocal) + when { + localVersion == null -> remoteChain // new + localVersion != remoteChain -> remoteChain // updated + else -> null // same + } + }.map(::mapChainToChainLocal) - val removed = localChainsJoinedInfo.filter { it.chain.id !in remoteMapping } - .map(JoinedChainInfo::chain) + val removed = localChainsJoinedInfo.filter { it.chain.id !in remoteMapping } + .map(JoinedChainInfo::chain) + dao.update(removed, newOrUpdated) + val metaAccounts = metaAccountDao.getMetaAccounts() - dao.update(removed, newOrUpdated) + if (metaAccounts.isEmpty()) return@runCatching Unit + val newAssets = + newOrUpdated.filter { it.chain.id !in localMapping.keys }.map { it.assets } + .flatten() + + val newLocalAssets = metaAccounts.map { metaAccount -> + newAssets.mapNotNull { + val chain = remoteMapping[it.chainId] + val accountId = if (chain?.isEthereumBased == true) { + metaAccount.ethereumAddress + } else { + metaAccount.substrateAccountId + } ?: return@mapNotNull null + AssetLocal( + accountId = accountId, + id = it.id, + chainId = it.chainId, + metaId = metaAccount.id, + tokenPriceId = it.priceId, + enabled = false + ) + } + }.flatten() + runCatching { assetsDao.insertAssets(newLocalAssets) }.onFailure { + it.printStackTrace() + } + } } } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/Mappers.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/Mappers.kt index 7e4f44f358..537f0351ca 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/Mappers.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/Mappers.kt @@ -42,6 +42,7 @@ private fun mapExplorerTypeRemoteToExplorerType(explorer: String) = when (explor "subscan" -> Chain.Explorer.Type.SUBSCAN "etherscan" -> Chain.Explorer.Type.ETHERSCAN "oklink" -> Chain.Explorer.Type.OKLINK + "okx explorer" -> Chain.Explorer.Type.OKLINK "zeta" -> Chain.Explorer.Type.ZETA "reef" -> Chain.Explorer.Type.REEF else -> Chain.Explorer.Type.UNKNOWN @@ -155,7 +156,8 @@ fun ChainRemote.toChain(): Chain { chainlinkProvider = CHAINLINK_PROVIDER_OPTION in optionsOrEmpty, supportNft = NFT_OPTION in optionsOrEmpty, paraId = this.paraId, - isUsesAppId = USES_APP_ID_OPTION in optionsOrEmpty + isUsesAppId = USES_APP_ID_OPTION in optionsOrEmpty, + identityChain = identityChain ) } @@ -264,7 +266,8 @@ fun mapChainLocalToChain(chainLocal: JoinedChainInfo): Chain { paraId = paraId, chainlinkProvider = isChainlinkProvider, supportNft = supportNft, - isUsesAppId = isUsesAppId + isUsesAppId = isUsesAppId, + identityChain = identityChain ) } } @@ -337,7 +340,8 @@ fun mapChainToChainLocal(chain: Chain): JoinedChainInfo { paraId = paraId, isChainlinkProvider = chainlinkProvider, supportNft = supportNft, - isUsesAppId = isUsesAppId + isUsesAppId = isUsesAppId, + identityChain = identityChain ) } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/model/Chain.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/model/Chain.kt index 6aee03c36a..3a4d021b03 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/model/Chain.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/model/Chain.kt @@ -52,7 +52,8 @@ data class Chain( val isEthereumChain: Boolean, val chainlinkProvider: Boolean, val supportNft: Boolean, - val isUsesAppId: Boolean + val isUsesAppId: Boolean, + val identityChain: String? ) : IChain { val assetsById = assets.associateBy(CoreAsset::id) @@ -77,7 +78,12 @@ data class Chain( enum class Type { POLKASCAN, SUBSCAN, ETHERSCAN, OKLINK, ZETA, REEF, UNKNOWN; - val capitalizedName: String = name.lowercase().replaceFirstChar { it.titlecase() } + val capitalizedName: String + get() = if (this == OKLINK) { + "OKX explorer" + } else { + name.lowercase().replaceFirstChar { it.titlecase() } + } } } @@ -106,6 +112,7 @@ data class Chain( if (chainlinkProvider != other.chainlinkProvider) return false if (supportNft != other.supportNft) return false if (isUsesAppId != other.isUsesAppId) return false + if (identityChain != other.identityChain) return false // custom comparison logic val defaultNodes = nodes.filter { it.isDefault } @@ -139,6 +146,7 @@ data class Chain( result = 31 * result + chainlinkProvider.hashCode() result = 31 * result + supportNft.hashCode() result = 31 * result + isUsesAppId.hashCode() + result = 31 * result + (identityChain?.hashCode() ?: 0) return result } } @@ -154,6 +162,22 @@ fun List.getSupportedExplorers(type: BlockExplorerUrlBuilder.Typ } }.toMap() +fun List.getSupportedAddressExplorers(address: String) = mapNotNull { + val type = when (it.type) { + Chain.Explorer.Type.ETHERSCAN, + Chain.Explorer.Type.OKLINK -> { + BlockExplorerUrlBuilder.Type.ADDRESS + } + else -> { + BlockExplorerUrlBuilder.Type.ACCOUNT + } + } + + BlockExplorerUrlBuilder(it.url, it.types).build(type, address)?.let { url -> + it.type to url + } +}.toMap() + @Deprecated("Use defaultChainSort() to get Polkadot at first place", ReplaceWith("defaultChainSort()")) fun ChainId.isPolkadotOrKusama() = this in listOf(polkadotChainId, kusamaChainId) diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/remote/model/ChainRemote.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/remote/model/ChainRemote.kt index 46581cc64d..5aced5b2a1 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/remote/model/ChainRemote.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/chain/remote/model/ChainRemote.kt @@ -13,5 +13,6 @@ data class ChainRemote( val addressPrefix: Int, val options: List?, val parentId: String?, - val disabled: Boolean = false + val disabled: Boolean = false, + val identityChain: String? = null ) diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/ConnectionPool.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/ConnectionPool.kt index f7f00b7d17..508e22f320 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/ConnectionPool.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/ConnectionPool.kt @@ -1,17 +1,12 @@ package jp.co.soramitsu.runtime.multiNetwork.connection -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Provider import jp.co.soramitsu.common.BuildConfig -import jp.co.soramitsu.common.compose.component.NetworkIssueItemState -import jp.co.soramitsu.common.compose.component.NetworkIssueType -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin -import jp.co.soramitsu.common.mixin.api.NetworkStateUi +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.core.models.ChainNode import jp.co.soramitsu.core.runtime.ChainConnection -import jp.co.soramitsu.core.utils.utilityAsset import jp.co.soramitsu.runtime.multiNetwork.ChainState import jp.co.soramitsu.runtime.multiNetwork.ChainsStateTracker import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain @@ -22,157 +17,98 @@ import jp.co.soramitsu.shared_utils.wsrpc.state.SocketStateMachine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet -private const val ConnectingStatusDebounce = 750L class ConnectionPool @Inject constructor( private val socketServiceProvider: Provider, private val externalRequirementFlow: MutableStateFlow, private val nodesSettingsStorage: NodesSettingsStorage, - private val networkStateMixin: NetworkStateMixin -) : NetworkStateUi by networkStateMixin, CoroutineScope by CoroutineScope(Dispatchers.Default) { + private val networkStateService: NetworkStateService +) : CoroutineScope by CoroutineScope(Dispatchers.Default) { - private val pool = ConcurrentHashMap() + private val poolFlow = MutableStateFlow>(emptyMap()) private val connectionWatcher = MutableStateFlow(Event(Unit)) - private val connections = connectionWatcher.flatMapLatest { - val connListFlow = pool.map { - it.value.isConnecting.map { isConnecting -> - it.value.chain.id to isConnecting - } - } - val connChainsListFlow = combine(connListFlow) { chains -> - chains.toMap() - } - connChainsListFlow - } - - private val connectionIssues = connectionWatcher.flatMapLatest { - val connListFlow = pool.map { - - it.value.isConnecting.map { isConnecting -> - it.value.chain to isConnecting - } - } - val connectionIssues = combine(connListFlow) { chains -> - val issues = - chains.filter { (_, isConnecting) -> isConnecting }.mapNotNull { (iChain, _) -> - val chain = iChain as? Chain ?: return@mapNotNull null - NetworkIssueItemState( - iconUrl = chain.icon, - title = chain.name, - type = when { - chain.nodes.size > 1 -> NetworkIssueType.Node - else -> NetworkIssueType.Network - }, - chainId = chain.id, - chainName = chain.name, - assetId = chain.utilityAsset?.id.orEmpty() - ) - } - issues - } - - connectionIssues - } - - private val showConnecting = connectionWatcher.flatMapLatest { - val isConnectedListFlow = pool.map { it.value.isConnected } - val hasConnectionsFlow = combine(isConnectedListFlow) { it.any { it } } - - val isPausedListFlow = pool.map { it.value.isPaused } - val hasPausesFlow = combine(isPausedListFlow) { it.any { it } } - - val isConnectingListFlow = pool.map { it.value.isConnecting } - val hasConnectingFlow = combine(isConnectingListFlow) { it.any { it } } - .filter { connecting -> connecting } - val showConnecting = combine( - hasConnectionsFlow, - hasConnectingFlow, - hasPausesFlow - ) { connected, connecting, paused -> - !(connected || paused) && connecting - } - showConnecting + suspend fun awaitConnection(chainId: ChainId): ChainConnection { + return poolFlow.map { it[chainId] }.filterNotNull().first() } - .distinctUntilChanged() - .debounce(ConnectingStatusDebounce) - init { - connections.onEach { - networkStateMixin.updateChainConnection(it) - }.launchIn(scope = this) - - connectionIssues.onEach { - networkStateMixin.updateNetworkIssues(it) - }.launchIn(this) - - showConnecting.onEach { - networkStateMixin.updateShowConnecting(it) - }.launchIn(this) - } - - fun getConnection(chainId: ChainId): ChainConnection = pool.getValue(chainId) - - fun getConnectionOrNull(chainId: ChainId): ChainConnection? = pool.getOrDefault(chainId, null) + fun getConnectionOrNull(chainId: ChainId): ChainConnection? = poolFlow.value[chainId] + fun getConnectionOrThrow(chainId: ChainId): ChainConnection = poolFlow.value.getValue(chainId) fun setupConnection( chain: Chain, onSelectedNodeChange: (chainId: ChainId, newNodeUrl: String) -> Unit ): ChainConnection { var isNew = false - val connection = pool.getOrPut(chain.id) { - isNew = true - val nodes = chain.nodes.map { - it.fillDwellirApiKey() - } + val connection = poolFlow.updateAndGet { currentPool -> + if (currentPool.containsKey(chain.id)) { + currentPool + } else { + isNew = true - ChainConnection( - chain = chain, - socketService = socketServiceProvider.get(), - initialNodes = nodes, - externalRequirementFlow = externalRequirementFlow, - onSelectedNodeChange = { onSelectedNodeChange(chain.id, clearDwellirApiKey(it)) }, - isAutoBalanceEnabled = { nodesSettingsStorage.getIsAutoSelectNodes(chain.id) } - ).also { connection -> - connection.state.onEach {connectionState -> - val newState = when(connectionState) { - is SocketStateMachine.State.Connected -> ChainState.ConnectionStatus.Connected(connectionState.url) - is SocketStateMachine.State.Connecting -> ChainState.ConnectionStatus.Connecting(connectionState.url) - is SocketStateMachine.State.Disconnected -> ChainState.ConnectionStatus.Disconnected - is SocketStateMachine.State.Paused -> ChainState.ConnectionStatus.Paused(connectionState.url) - is SocketStateMachine.State.WaitingForReconnect -> ChainState.ConnectionStatus.Connecting(connectionState.url) - } - ChainsStateTracker.updateState(chain){ it.copy(connectionStatus = newState) } - - }.launchIn(this) + val nodes = chain.nodes.map { + it.fillDwellirApiKey() + } + + val newConnection = ChainConnection( + chain = chain, + socketService = socketServiceProvider.get(), + initialNodes = nodes, + externalRequirementFlow = externalRequirementFlow, + onSelectedNodeChange = { onSelectedNodeChange(chain.id, clearDwellirApiKey(it)) }, + isAutoBalanceEnabled = { nodesSettingsStorage.getIsAutoSelectNodes(chain.id) } + ).also { connection -> + connection.state.onEach { connectionState -> + val newState = when (connectionState) { + is SocketStateMachine.State.Connected -> ChainState.ConnectionStatus.Connected(connectionState.url) + is SocketStateMachine.State.Connecting -> ChainState.ConnectionStatus.Connecting(connectionState.url) + is SocketStateMachine.State.Disconnected -> ChainState.ConnectionStatus.Disconnected + is SocketStateMachine.State.Paused -> ChainState.ConnectionStatus.Paused(connectionState.url) + is SocketStateMachine.State.WaitingForReconnect -> ChainState.ConnectionStatus.Connecting(connectionState.url) + } + + ChainsStateTracker.updateState(chain) { it.copy(connectionStatus = newState) } + + when (connectionState) { + is SocketStateMachine.State.Connected -> networkStateService.notifyConnectionSuccess(chain.id) + is SocketStateMachine.State.WaitingForReconnect -> networkStateService.notifyConnectionProblem(chain.id) + else -> Unit + } + }.launchIn(this@ConnectionPool) + } + + currentPool + (chain.id to newConnection) } - } + }[chain.id]!! if (isNew) { connectionWatcher.tryEmit(Event(Unit)) } - connection.considerUpdateNodes(chain.nodes) + if (connection.chain.nodes != chain.nodes) { + connection.considerUpdateNodes(chain.nodes) + } return connection } fun removeConnection(chainId: ChainId) { - pool.remove(chainId)?.apply { finish() } + val connection = getConnectionOrNull(chainId) + poolFlow.update { it - chainId } + connection?.finish() connectionWatcher.tryEmit(Event(Unit)) + networkStateService.notifyConnectionSuccess(chainId) } } - fun ChainNode.fillDwellirApiKey(): ChainNode { return copy(url = fillDwellirApiKey(url)) } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/EthereumConnectionPool.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/EthereumConnectionPool.kt index 6130a8cfd1..a0574c8617 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/EthereumConnectionPool.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/connection/EthereumConnectionPool.kt @@ -2,9 +2,10 @@ package jp.co.soramitsu.runtime.multiNetwork.connection import android.util.Log import java.net.URI -import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger import jp.co.soramitsu.common.BuildConfig -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.common.utils.cycle import jp.co.soramitsu.core.models.ChainNode import jp.co.soramitsu.runtime.multiNetwork.chain.model.BSCChainId @@ -16,17 +17,22 @@ import jp.co.soramitsu.runtime.multiNetwork.chain.model.goerliChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.polygonChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.polygonTestnetChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.sepoliaChainId -import jp.co.soramitsu.runtime.multiNetwork.toSyncIssue import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope +import okhttp3.OkHttpClient import org.web3j.protocol.Web3j import org.web3j.protocol.Web3jService import org.web3j.protocol.http.HttpService @@ -36,33 +42,49 @@ import org.web3j.protocol.websocket.WebSocketService private const val EVM_CONNECTION_TAG = "EVM Connection" class EthereumConnectionPool( - private val networkStateMixin: NetworkStateMixin, + private val networkStateService: NetworkStateService, ) { - private val pool = ConcurrentHashMap() + private val poolStateFlow = MutableStateFlow>(mutableMapOf()) + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + suspend fun await(chainId: String): EthereumChainConnection { + return poolStateFlow.map { it[chainId] }.filterNotNull().first() + } - fun setupConnection( - chain: Chain, - onSelectedNodeChange: (chainId: ChainId, newNodeUrl: String) -> Unit - ) { - pool[chain.id] = EthereumChainConnection( - chain, - onSelectedNodeChange = onSelectedNodeChange - ) { networkStateMixin.notifyChainSyncProblem(chain.toSyncIssue()) } + fun getOrNull(chainId: String): EthereumChainConnection? { + return poolStateFlow.value[chainId] } - fun get(chainId: String): EthereumChainConnection? { - val connection = pool.getOrDefault(chainId, null) - if (connection != null && connection.statusFlow.value !is EvmConnectionStatus.Connected) { - connection.statusFlow.update { null } - } + fun setupConnection(chain: Chain, onSelectedNodeChange: (chainId: ChainId, newNodeUrl: String) -> Unit): EthereumChainConnection { + val connection = poolStateFlow.updateAndGet { currentPool -> + if (currentPool.containsKey(chain.id)) { + currentPool + } else { + val newConnection = EthereumChainConnection( + chain, + onSelectedNodeChange = onSelectedNodeChange + ) { networkStateService.notifyConnectionProblem(chain.id) } + + newConnection.statusFlow.onEach { status -> + if (status is EvmConnectionStatus.Connected) { + networkStateService.notifyConnectionSuccess(chain.id) + } else { + networkStateService.notifyConnectionProblem(chain.id) + } + }.launchIn(scope) + + currentPool.toMutableMap().apply { put(chain.id, newConnection) } + } + }[chain.id]!! + return connection } fun stop(chainId: String) { - pool.getOrDefault(chainId, null)?.let { - it.web3j?.shutdown() - pool.remove(chainId) + poolStateFlow.update { currentPool -> + currentPool.toMutableMap().apply { + remove(chainId)?.apply { web3j?.shutdown() } + } } } } @@ -85,7 +107,6 @@ class EthereumChainConnection( private const val WSS_NODE_PREFIX = "wss" } - private val nodes: List = formatWithApiKeys(chain) private var nodesCycle = nodes.cycle().iterator() @@ -95,11 +116,7 @@ class EthereumChainConnection( val statusFlow = MutableStateFlow(null) - private val nodesAttempts = ConcurrentHashMap().also { - nodes.forEach { node -> - it[node.url] = 0 - } - } + private val nodesAttempts = nodes.associate { it.url to AtomicInteger(0) } init { require(chain.isEthereumChain) @@ -115,13 +132,11 @@ class EthereumChainConnection( } onSelectedNodeChange(chain.id, url) } - is EvmConnectionStatus.Error, is EvmConnectionStatus.Closed, null -> { connectNextNode() } - else -> Unit } }.launchIn(scope) @@ -130,20 +145,20 @@ class EthereumChainConnection( private fun connectNextNode() { scope.launch { supervisorScope { - if (nodesAttempts.all { it.value >= 5 }) { + if (nodesAttempts.all { it.value.get() >= 5 }) { allNodesHaveFailed() return@supervisorScope } + delay(1000) // delay between reconnects + val nextNode = nodesCycle.next() - if (nodesAttempts.getOrDefault(nextNode.url, 0) >= 5) { + if ((nodesAttempts[nextNode.url]?.get() ?: 0) >= 5) { statusFlow.update { null } return@supervisorScope } - if (connection != null && connection is EVMConnection.WSS && (connection as EVMConnection.WSS).socket.isOpen) { - connection?.web3j?.shutdown() - } + connection?.web3j?.shutdown() // shutdown previous connection when { nextNode.url.startsWith("wss") -> { @@ -171,7 +186,7 @@ class EthereumChainConnection( } } - nodesAttempts[nextNode.url] = (nodesAttempts[nextNode.url] ?: 0) + 1 + nodesAttempts[nextNode.url]?.incrementAndGet() } } } @@ -209,7 +224,15 @@ sealed class EVMConnection( } class HTTP(url: String, onStatusChanged: (EvmConnectionStatus) -> Unit) : - EVMConnection(url, HttpService(url, false), onStatusChanged) { + EVMConnection( + url, HttpService( + url, OkHttpClient.Builder() + .connectTimeout(60L, TimeUnit.SECONDS) + .writeTimeout(60L, TimeUnit.SECONDS) + .readTimeout(60L, TimeUnit.SECONDS) + .retryOnConnectionFailure(true).build() + ), onStatusChanged + ) { init { onStatusChanged(EvmConnectionStatus.Connecting(url)) runCatching { web3j.ethBlockNumber().send() } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProvider.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProvider.kt index 281fb1476e..217de2d1d1 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProvider.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProvider.kt @@ -1,6 +1,6 @@ package jp.co.soramitsu.runtime.multiNetwork.runtime -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.core.runtime.ConstructedRuntime import jp.co.soramitsu.core.runtime.RuntimeFactory import jp.co.soramitsu.coredb.dao.ChainDao @@ -8,7 +8,6 @@ import jp.co.soramitsu.runtime.multiNetwork.ChainState import jp.co.soramitsu.runtime.multiNetwork.ChainsStateTracker import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.reefChainId -import jp.co.soramitsu.runtime.multiNetwork.toSyncIssue import jp.co.soramitsu.shared_utils.runtime.RuntimeSnapshot import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -31,7 +30,7 @@ class RuntimeProvider( private val runtimeSyncService: RuntimeSyncService, private val runtimeFilesCache: RuntimeFilesCache, private val chainDao: ChainDao, - private val networkStateMixin: NetworkStateMixin, + private val networkStateService: NetworkStateService, private val chain: Chain ) : CoroutineScope by CoroutineScope(Dispatchers.Default) { @@ -141,10 +140,10 @@ class RuntimeProvider( } runtimeFlow.emit(runtime) ChainsStateTracker.updateState(chainId) { it.copy(runtimeConstruction = ChainState.Status.Completed) } - networkStateMixin.notifyChainSyncSuccess(chainId) + networkStateService.notifyChainSyncSuccess(chainId) }.onFailure { error -> ChainsStateTracker.updateState(chainId) { it.copy(runtimeConstruction = ChainState.Status.Failed(error)) } - networkStateMixin.notifyChainSyncProblem(chain.toSyncIssue()) + networkStateService.notifyChainSyncProblem(chain.id) when (error) { ChainInfoNotInCacheException -> runtimeSyncService.cacheNotFound(chainId) else -> error.printStackTrace() diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderPool.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderPool.kt index 05f8d0a691..0ad82e729c 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderPool.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderPool.kt @@ -1,43 +1,59 @@ package jp.co.soramitsu.runtime.multiNetwork.runtime -import java.util.concurrent.ConcurrentHashMap -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin +import jp.co.soramitsu.common.data.network.runtime.binding.cast +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.core.runtime.RuntimeFactory import jp.co.soramitsu.coredb.dao.ChainDao import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update class RuntimeProviderPool( private val runtimeFactory: RuntimeFactory, private val runtimeSyncService: RuntimeSyncService, private val runtimeFilesCache: RuntimeFilesCache, private val chainDao: ChainDao, - private val networkStateMixin: NetworkStateMixin + private val networkStateService: NetworkStateService ) { - private val pool = ConcurrentHashMap() + private val poolStateFlow = + MutableStateFlow>(mutableMapOf()) - fun getRuntimeProvider(chainId: String): RuntimeProvider { - return pool.getValue(chainId) + suspend fun awaitRuntimeProvider(chainId: String): RuntimeProvider { + return poolStateFlow.map { it.getOrDefault(chainId, null) }.first { it != null }.cast() } fun getRuntimeProviderOrNull(chainId: String): RuntimeProvider? { - return pool.getOrDefault(chainId, null) + return poolStateFlow.value.getOrDefault(chainId, null) } fun setupRuntimeProvider(chain: Chain): RuntimeProvider { - return pool.getOrPut(chain.id) { - RuntimeProvider( - runtimeFactory, - runtimeSyncService, - runtimeFilesCache, - chainDao, - networkStateMixin, - chain - ) + if (poolStateFlow.value.containsKey(chain.id)) { + return poolStateFlow.value.getValue(chain.id) + } else { + poolStateFlow.update { prev -> + prev.also { + it[chain.id] = RuntimeProvider( + runtimeFactory, + runtimeSyncService, + runtimeFilesCache, + chainDao, + networkStateService, + chain + ) + } + } + return poolStateFlow.value.getValue(chain.id) } } fun removeRuntimeProvider(chainId: String) { - pool.remove(chainId)?.apply { finish() } + poolStateFlow.update { prev -> + prev.also { + it.remove(chainId)?.apply { finish() } + } + } } } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSubscriptionPool.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSubscriptionPool.kt index 6c805cb26c..0e7c98c494 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSubscriptionPool.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSubscriptionPool.kt @@ -5,10 +5,12 @@ import jp.co.soramitsu.coredb.dao.ChainDao import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import kotlinx.coroutines.cancel import java.util.concurrent.ConcurrentHashMap +import jp.co.soramitsu.common.domain.NetworkStateService class RuntimeSubscriptionPool( private val chainDao: ChainDao, - private val runtimeSyncService: RuntimeSyncService + private val runtimeSyncService: RuntimeSyncService, + private val networkStateService: NetworkStateService, ) { private val pool = ConcurrentHashMap() @@ -17,7 +19,7 @@ class RuntimeSubscriptionPool( fun setupRuntimeSubscription(chain: Chain, connection: ChainConnection): RuntimeVersionSubscription { return pool.getOrPut(chain.id) { - RuntimeVersionSubscription(chain.id, connection, chainDao, runtimeSyncService) + RuntimeVersionSubscription(chain.id, connection, chainDao, runtimeSyncService, networkStateService) } } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSyncService.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSyncService.kt index 322d03d7f6..51bc1a0fd5 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSyncService.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeSyncService.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonObject @@ -109,7 +110,7 @@ class RuntimeSyncService( val metadataHash = if (force || shouldSyncMetadata) { val runtimeMetadata = - connectionPool.getConnection(chainId).socketService.executeAsyncCatching( + connectionPool.awaitConnection(chainId).socketService.executeAsyncCatching( GetMetadataRequest, mapper = pojo().nonNull() ).getOrNull() @@ -139,17 +140,20 @@ class RuntimeSyncService( ) } - suspend fun syncTypes() { - val types = typesFetcher.getTypes(BuildConfig.TYPES_URL) - val defaultTypes = typesFetcher.getTypes(BuildConfig.DEFAULT_V13_TYPES_URL) - val array = Json.decodeFromString(types) - val chainIdToTypes = - array.mapNotNull { element -> - val chainId = - element.jsonObject["chainId"]?.jsonPrimitive?.content ?: return@mapNotNull null - ChainTypesLocal(chainId, element.toString()) - }.toMutableList().apply { add(ChainTypesLocal("default", defaultTypes)) } - chainDao.insertTypes(chainIdToTypes) + suspend fun syncTypes(): Result = withContext(Dispatchers.Default) { + runCatching { + val types = typesFetcher.getTypes(BuildConfig.TYPES_URL) + val defaultTypes = typesFetcher.getTypes(BuildConfig.DEFAULT_V13_TYPES_URL) + val array = Json.decodeFromString(types) + val chainIdToTypes = + array.mapNotNull { element -> + val chainId = + element.jsonObject["chainId"]?.jsonPrimitive?.content + ?: return@mapNotNull null + ChainTypesLocal(chainId, element.toString()) + }.toMutableList().apply { add(ChainTypesLocal("default", defaultTypes)) } + chainDao.insertTypes(chainIdToTypes) + } } private fun cancelExistingSync(chainId: String) { diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeVersionSubscription.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeVersionSubscription.kt index 04a3045e7d..d91224018f 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeVersionSubscription.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeVersionSubscription.kt @@ -1,18 +1,28 @@ package jp.co.soramitsu.runtime.multiNetwork.runtime import android.util.Log +import jp.co.soramitsu.common.domain.NetworkStateService import jp.co.soramitsu.core.runtime.ChainConnection import jp.co.soramitsu.coredb.dao.ChainDao import jp.co.soramitsu.runtime.multiNetwork.ChainState import jp.co.soramitsu.runtime.multiNetwork.ChainsStateTracker +import jp.co.soramitsu.shared_utils.wsrpc.executeAsync +import jp.co.soramitsu.shared_utils.wsrpc.mappers.nonNull +import jp.co.soramitsu.shared_utils.wsrpc.mappers.pojo +import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.chain.RuntimeVersion +import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.chain.RuntimeVersionRequest +import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.chain.StateRuntimeVersionRequest import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.chain.SubscribeRuntimeVersionRequest +import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.chain.SubscribeStateRuntimeVersionRequest import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.chain.runtimeVersionChange -import jp.co.soramitsu.shared_utils.wsrpc.state.SocketStateMachine import jp.co.soramitsu.shared_utils.wsrpc.subscriptionFlow +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -23,17 +33,37 @@ class RuntimeVersionSubscription( private val chainId: String, connection: ChainConnection, private val chainDao: ChainDao, - private val runtimeSyncService: RuntimeSyncService -) : CoroutineScope by CoroutineScope(Dispatchers.IO + SupervisorJob()) { + private val runtimeSyncService: RuntimeSyncService, + private val networkStateService: NetworkStateService, + dispatcher: CoroutineDispatcher = Dispatchers.Default +) { + private val scope = CoroutineScope(dispatcher + SupervisorJob()) init { runCatching { ChainsStateTracker.updateState(chainId) { it.copy(runtimeVersion = ChainState.Status.Started) } - launch { + + scope.launch { // await connection - connection.state.first { it is SocketStateMachine.State.Connected } + connection.isConnected.first() connection.socketService.subscriptionFlow(SubscribeRuntimeVersionRequest) .map { it.runtimeVersionChange().specVersion } + .catch { + emitAll( + connection.socketService.subscriptionFlow( + SubscribeStateRuntimeVersionRequest + ) + .map { it.runtimeVersionChange().specVersion } + .catch { + + val version = connection.getVersionChainRpc() + ?: connection.getVersionStateRpc() + ?: error("Runtime version not obtained") + + emit(version) + } + ) + } .onEach { runtimeVersionResult -> chainDao.updateRemoteRuntimeVersion( chainId, @@ -41,9 +71,11 @@ class RuntimeVersionSubscription( ) runtimeSyncService.applyRuntimeVersion(chainId) + ChainsStateTracker.updateState(chainId) { it.copy(runtimeVersion = ChainState.Status.Completed) } } .catch { error -> + networkStateService.notifyChainSyncProblem(chainId) ChainsStateTracker.updateState(chainId) { it.copy( runtimeVersion = ChainState.Status.Failed( @@ -61,4 +93,22 @@ class RuntimeVersionSubscription( } } } + + private suspend fun ChainConnection.getVersionChainRpc(): Int? = runCatching { + socketService.executeAsync( + request = RuntimeVersionRequest(), + mapper = pojo().nonNull() + ).specVersion + }.getOrNull() + + private suspend fun ChainConnection.getVersionStateRpc(): Int? = runCatching { + socketService.executeAsync( + request = StateRuntimeVersionRequest(), + mapper = pojo().nonNull() + ).specVersion + }.getOrNull() + + fun cancel() { + scope.coroutineContext.cancel() + } } \ No newline at end of file diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/SubscribeRuntimeVersionRequest.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/SubscribeRuntimeVersionRequest.kt deleted file mode 100644 index 0207e7408c..0000000000 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/multiNetwork/runtime/SubscribeRuntimeVersionRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package jp.co.soramitsu.runtime.multiNetwork.runtime - -import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.RuntimeRequest - -object SubscribeRuntimeVersionRequest : RuntimeRequest( - method = "state_subscribeRuntimeVersion", - params = listOf() -) diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/network/updaters/SingleChainUpdateSystem.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/network/updaters/SingleChainUpdateSystem.kt index 9998e8df97..a1934faa0a 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/network/updaters/SingleChainUpdateSystem.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/network/updaters/SingleChainUpdateSystem.kt @@ -6,7 +6,6 @@ import jp.co.soramitsu.core.updater.UpdateSystem import jp.co.soramitsu.core.updater.Updater import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain -import jp.co.soramitsu.runtime.multiNetwork.getSocket import jp.co.soramitsu.shared_utils.wsrpc.request.runtime.storage.subscribeUsing import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -23,8 +22,8 @@ class SingleChainUpdateSystem( ) : UpdateSystem { override fun start(): Flow = chainFlow.flatMapLatest { chain -> - val socket = chainRegistry.getSocket(chain.id) - val runtimeMetadata = chainRegistry.getRuntime(chain.id).metadata + val socket = chainRegistry.awaitConnection(chain.id).socketService + val runtimeMetadata = chainRegistry.awaitRuntimeProvider(chain.id).get().metadata val scopeFlows = updaters.groupBy(Updater::scope).map { (scope, scopeUpdaters) -> scope.invalidationFlow().flatMapLatest { diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/repository/ChainStateRepository.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/repository/ChainStateRepository.kt index 63a9512d8e..72d1bd12ac 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/repository/ChainStateRepository.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/repository/ChainStateRepository.kt @@ -10,7 +10,6 @@ import jp.co.soramitsu.core.extrinsic.mortality.IChainStateRepository import jp.co.soramitsu.runtime.di.LOCAL_STORAGE_SOURCE import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.runtime.multiNetwork.getRuntime import jp.co.soramitsu.runtime.storage.source.StorageDataSource import jp.co.soramitsu.runtime.storage.source.observeNonNull import jp.co.soramitsu.runtime.storage.source.queryNonNull @@ -28,7 +27,7 @@ class ChainStateRepository @Inject constructor( ) : IChainStateRepository { override suspend fun expectedBlockTimeInMillis(chainId: ChainId, defaultTime: BigInteger): BigInteger { - val runtime = chainRegistry.getRuntime(chainId) + val runtime = chainRegistry.awaitRuntimeProvider(chainId).get() return runCatching { runtime.metadata.babe().numberConstant("ExpectedBlockTime", runtime) diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/state/SingleAssetSharedState.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/state/SingleAssetSharedState.kt index 0cd90b82b8..1d7f7eabce 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/state/SingleAssetSharedState.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/state/SingleAssetSharedState.kt @@ -6,7 +6,6 @@ import jp.co.soramitsu.core.models.Asset import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId -import jp.co.soramitsu.runtime.multiNetwork.chainWithAsset import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/storage/DbStorageCache.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/storage/DbStorageCache.kt index 4bcf8cb685..df83f0a214 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/storage/DbStorageCache.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/storage/DbStorageCache.kt @@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +private const val SQLITE_MAX_VARIABLE_NUMBER = 900 + class DbStorageCache( private val storageDao: StorageDao ) : StorageCache { @@ -66,10 +68,12 @@ class DbStorageCache( } override suspend fun getEntries(fullKeys: List, chainId: String): List { - return storageDao.observeEntries(chainId, fullKeys) - .filter { it.size == fullKeys.size } - .mapList { mapStorageEntryFromLocal(it) } - .first() + return fullKeys.chunked(SQLITE_MAX_VARIABLE_NUMBER).map { chunkKeys -> + storageDao.observeEntries(chainId, chunkKeys) + .filter { it.size == chunkKeys.size } + .mapList { mapStorageEntryFromLocal(it) } + .first() + }.flatten() } } diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/BaseStorageSource.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/BaseStorageSource.kt index 372aa75608..791975a4cc 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/BaseStorageSource.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/BaseStorageSource.kt @@ -19,7 +19,7 @@ abstract class BaseStorageSource( protected abstract suspend fun query(key: String, chainId: String, at: BlockHash?): String? - protected abstract suspend fun queryKeys(keys: List, chainId: String, at: BlockHash?): Map + abstract suspend fun queryKeys(keys: List, chainId: String, at: BlockHash?): Map protected abstract suspend fun observe(key: String, chainId: String): Flow diff --git a/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/RemoteStorageSource.kt b/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/RemoteStorageSource.kt index 7e43bcb483..66a49f6cdb 100644 --- a/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/RemoteStorageSource.kt +++ b/runtime/src/main/java/jp/co/soramitsu/runtime/storage/source/RemoteStorageSource.kt @@ -63,6 +63,6 @@ class RemoteStorageSource( return response?.result as? String? } - private fun getSocketService(chainId: String) = - chainRegistry.getConnection(chainId).socketService + private suspend fun getSocketService(chainId: String) = + chainRegistry.awaitConnection(chainId).socketService } diff --git a/runtime/src/test/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderTest.kt b/runtime/src/test/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderTest.kt index 5758019d13..b7024d4789 100644 --- a/runtime/src/test/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderTest.kt +++ b/runtime/src/test/java/jp/co/soramitsu/runtime/multiNetwork/runtime/RuntimeProviderTest.kt @@ -1,6 +1,6 @@ package jp.co.soramitsu.runtime.multiNetwork.runtime -import jp.co.soramitsu.common.mixin.api.NetworkStateMixin +import jp.co.soramitsu.common.mixin.api.networkStateService import jp.co.soramitsu.core.runtime.ConstructedRuntime import jp.co.soramitsu.core.runtime.RuntimeFactory import jp.co.soramitsu.coredb.dao.ChainDao @@ -50,7 +50,7 @@ class RuntimeProviderTest { lateinit var chainDao: ChainDao @Mock - lateinit var networkStateMixin: NetworkStateMixin + lateinit var networkStateService: networkStateService lateinit var runtimeProvider: RuntimeProvider @@ -208,7 +208,7 @@ class RuntimeProviderTest { runtimeSyncService, runtimeFilesCache, chainDao, - networkStateMixin, + networkStateService, chain ) }