From f6d56f660d402acd857b01844a4a792514b18c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mithat=20Sinan=20Sar=C4=B1?= <36641492+mitsinsar@users.noreply.github.com> Date: Fri, 27 Dec 2024 00:15:00 +0300 Subject: [PATCH] PERA-1357 :: Finalize Account Cache, Asset Cache and Polling (#91) --- .../app/di/AccountInformationModule.kt | 34 +- .../co/algorand/app/di/ViewModelModules.kt | 6 +- .../kotlin/co/algorand/app/ui/AppViewModel.kt | 30 ++ .../app/ui/navigation/AppNavigation.kt | 13 +- .../ui/navigation/BottomNavigationGraph.kt | 2 +- .../app/ui/screens/accounts/AccountsScreen.kt | 318 ++++++++++++++++++ .../{home => accounts}/AccountsViewModel.kt | 41 ++- .../app/ui/screens/home/AccountsScreen.kt | 161 --------- gradle/libs.versions.toml | 2 + wallet-sdk/build.gradle.kts | 1 + .../IndexerApiServiceProvider.android.kt | 37 -- ...droid.kt => HttpClientProvider.android.kt} | 8 +- .../entity/LedgerBleEntityMapperImplTest.kt | 6 +- .../mapper/model/LedgerBleMapperImplTest.kt | 3 +- .../detail/di/AccountDetailKoinModule.kt | 27 ++ .../domain/model/AccountRegistrationType.kt | 24 ++ .../detail/domain/model/AccountState.kt | 19 ++ .../detail/domain/model/AccountType.kt | 34 ++ .../domain/usecase/AccountDetailUseCases.kt | 29 ++ .../GetAccountRegistrationTypeUseCase.kt | 32 ++ .../domain/usecase/GetAccountStateUseCase.kt | 29 ++ .../domain/usecase/GetAccountTypeUseCase.kt | 58 ++++ .../info/data/database/dao/AssetHoldingDao.kt | 2 +- .../AccountAssetHoldingsFetchHelper.kt | 20 ++ .../AccountAssetHoldingsFetchHelperImpl.kt | 50 +++ .../AccountInformationCacheHelper.kt | 20 ++ .../AccountInformationCacheHelperImpl.kt | 59 ++++ .../AccountInformationFetchHelper.kt | 20 ++ .../AccountInformationFetchHelperImpl.kt | 95 ++++++ .../AccountInformationRepositoryImpl.kt | 152 +++++++++ .../repository/AssetHoldingCacheHelper.kt | 20 ++ .../repository/AssetHoldingCacheHelperImpl.kt | 45 +++ ...AccountFetchRequestExcludesQueryBuilder.kt | 35 ++ ...ice.kt => AccountInformationApiService.kt} | 2 +- ...kt => AccountInformationApiServiceImpl.kt} | 4 +- .../info/di/AccountInformationKoinModule.kt | 84 ++++- .../manager/AccountCacheManager.kt} | 9 +- .../domain/manager/AccountCacheManagerImpl.kt | 90 +++++ ...AccountAssets.kt => AccountCacheStatus.kt} | 10 +- .../AccountInformationRepository.kt | 42 +++ .../usecase/AccountInformationUseCases.kt | 53 +++ .../GetAccountDetailCacheStatusFlowUseCase.kt | 38 +++ .../local/data/database/dao/Algo25Dao.kt | 4 +- .../local/data/database/dao/Bip39Dao.kt | 5 +- .../local/data/database/dao/LedgerBleDao.kt | 4 +- .../local/data/database/dao/NoAuthDao.kt | 4 +- .../data/database/model/LedgerBleEntity.kt | 5 +- .../entity/LedgerBleEntityMapperImpl.kt | 3 +- .../data/mapper/model/LedgerBleMapperImpl.kt | 3 +- .../local/di/LocalAccountsKoinModule.kt | 13 + .../local/domain/model/LocalAccount.kt | 1 + .../asset/data/model/AssetCreatorResponse.kt | 6 +- .../common/asset/data/model/AssetResponse.kt | 44 +-- .../model/NodeAssetDetailParamsResponse.kt | 8 +- .../data/model/NodeAssetDetailResponse.kt | 4 +- .../collectible/CollectibleMediaResponse.kt | 8 +- .../model/collectible/CollectibleResponse.kt | 16 +- .../collectible/CollectibleSearchResponse.kt | 4 +- .../collectible/CollectibleTraitResponse.kt | 4 +- .../model/collectible/CollectionResponse.kt | 6 +- .../common/asset/di/AssetDetailKoinModules.kt | 16 +- .../domain/manager/AssetDetailCacheManager.kt | 23 ++ .../manager/AssetDetailCacheManagerImpl.kt | 89 +++++ .../asset/domain/model/AssetCacheStatus.kt | 24 ++ .../usecase/GetAssetDetailCacheStatusFlow.kt | 20 ++ .../data/model/ShouldRefreshRequestBody.kt | 22 ++ .../block/data/model/ShouldRefreshResponse.kt | 21 ++ .../repository/BlockPollingRepositoryImpl.kt | 47 +++ .../data/service/BlockPollingApiService.kt | 21 ++ .../service/BlockPollingApiServiceImpl.kt | 34 ++ .../common/block/di/BlockPollingKoinModule.kt | 52 +++ .../repository/BlockPollingRepository.kt | 26 ++ .../domain/usecase/BlockPollingUseCases.kt | 31 ++ .../ShouldUpdateAccountCacheUseCase.kt | 34 ++ .../UpdateLastKnownBlockNumberUseCase.kt | 27 ++ .../cache/LifecycleAwareCacheManager.kt | 29 ++ .../cache/LifecycleAwareCacheManagerImpl.kt | 82 +++++ .../common/cache/di/CacheKoinModule.kt | 33 ++ .../cache/domain/model/AppCacheStatus.kt | 20 ++ .../cache/domain/usecase/CacheUseCases.kt | 33 ++ .../ClearPreviousSessionCacheUseCase.kt | 27 ++ .../usecase/GetAppCacheStatusFlowUseCase.kt | 48 +++ .../domain/usecase/InitializeAppCacheImpl.kt | 34 ++ .../usecase/UpdateAccountCacheUseCase.kt | 35 ++ .../common/di/CommonModuleKoinModules.kt | 10 +- .../algorand/common/di/NetworkKoinModule.kt | 8 +- .../common/foundation/cache/CacheResult.kt | 76 +++++ .../cache/SingleInMemoryLocalCache.kt | 39 +++ .../network/HttpClientExtensions.kt | 17 +- ...lientProvider.kt => HttpClientProvider.kt} | 2 +- .../network/PeraContentNegotiation.kt | 2 + .../network/algod/AlgodHttpClientProvider.kt | 24 ++ .../{ => algod}/AlgodInterceptorPlugin.kt | 2 +- .../{ => algod}/GetAlgodInterceptorConfig.kt | 2 +- .../GetIndexerInterceptorConfig.kt | 2 +- .../indexer/IndexerApiServiceProvider.kt | 24 ++ .../{ => indexer}/IndexerInterceptorPlugin.kt | 2 +- .../pera/GetPeraMobileInterceptorConfig.kt | 17 + .../pera/PeraMobileHttpClientProvider.kt | 24 ++ .../pera/PeraMobileInterceptorPlugin.kt | 78 +++++ .../network/pera/PeraMobileUserAgent.kt | 23 ++ .../common/utils/date/DateKoinModule.kt | 19 ++ .../common/utils/date/TimeProvider.kt | 17 + .../common/utils/date/TimeProviderImpl.kt | 22 ++ .../service/IndexerApiServiceProvider.ios.kt | 37 -- .../foundation/database/PeraDatabase.kt | 3 +- ...vider.ios.kt => HttpClientProvider.ios.kt} | 8 +- 107 files changed, 2835 insertions(+), 357 deletions(-) create mode 100644 composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/AppViewModel.kt create mode 100644 composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsScreen.kt rename composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/{home => accounts}/AccountsViewModel.kt (65%) delete mode 100644 composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsScreen.kt delete mode 100644 wallet-sdk/src/androidMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.android.kt rename wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/{AlgodHttpClientProvider.android.kt => HttpClientProvider.android.kt} (75%) create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/di/AccountDetailKoinModule.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountRegistrationType.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountState.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountType.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/AccountDetailUseCases.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountRegistrationTypeUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountStateUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountTypeUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelper.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelperImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelper.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelperImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelper.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelperImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationRepositoryImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelper.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelperImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/IndexerAccountFetchRequestExcludesQueryBuilder.kt rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/{IndexerApiService.kt => AccountInformationApiService.kt} (96%) rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/{IndexerApiServiceImpl.kt => AccountInformationApiServiceImpl.kt} (96%) rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/{data/service/IndexerApiServiceProvider.kt => domain/manager/AccountCacheManager.kt} (68%) create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManagerImpl.kt rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/{AccountAssets.kt => AccountCacheStatus.kt} (86%) create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/repository/AccountInformationRepository.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/AccountInformationUseCases.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/GetAccountDetailCacheStatusFlowUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManager.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManagerImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/model/AssetCacheStatus.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/usecase/GetAssetDetailCacheStatusFlow.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshRequestBody.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshResponse.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/repository/BlockPollingRepositoryImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiService.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiServiceImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/di/BlockPollingKoinModule.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/repository/BlockPollingRepository.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/BlockPollingUseCases.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/ShouldUpdateAccountCacheUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/UpdateLastKnownBlockNumberUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManager.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManagerImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/di/CacheKoinModule.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/model/AppCacheStatus.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/CacheUseCases.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/GetAppCacheStatusFlowUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/InitializeAppCacheImpl.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/UpdateAccountCacheUseCase.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/CacheResult.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/SingleInMemoryLocalCache.kt rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/{AlgodHttpClientProvider.kt => HttpClientProvider.kt} (87%) create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodHttpClientProvider.kt rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/{ => algod}/AlgodInterceptorPlugin.kt (97%) rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/{ => algod}/GetAlgodInterceptorConfig.kt (92%) rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/{ => indexer}/GetIndexerInterceptorConfig.kt (92%) create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerApiServiceProvider.kt rename wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/{ => indexer}/IndexerInterceptorPlugin.kt (97%) create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/GetPeraMobileInterceptorConfig.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileHttpClientProvider.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileInterceptorPlugin.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileUserAgent.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/DateKoinModule.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProvider.kt create mode 100644 wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProviderImpl.kt delete mode 100644 wallet-sdk/src/iosMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.ios.kt rename wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/{AlgodHttpClientProvider.ios.kt => HttpClientProvider.ios.kt} (75%) diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/AccountInformationModule.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/AccountInformationModule.kt index d843ed8f..dcc1e42b 100644 --- a/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/AccountInformationModule.kt +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/AccountInformationModule.kt @@ -12,8 +12,13 @@ package co.algorand.app.di -import com.algorand.common.foundation.network.GetIndexerInterceptorConfig -import com.algorand.common.foundation.network.IndexerInterceptorPluginConfig +import com.algorand.common.foundation.network.algod.AlgodInterceptorPluginConfig +import com.algorand.common.foundation.network.algod.GetAlgodInterceptorConfig +import com.algorand.common.foundation.network.indexer.GetIndexerInterceptorConfig +import com.algorand.common.foundation.network.indexer.IndexerInterceptorPluginConfig +import com.algorand.common.foundation.network.pera.GetPeraMobileInterceptorConfig +import com.algorand.common.foundation.network.pera.PeraMobileInterceptorPluginConfig +import com.algorand.common.foundation.network.pera.PeraMobileUserAgent import org.koin.dsl.module val accountInformationModule = module { @@ -25,4 +30,29 @@ val accountInformationModule = module { ) } } + factory { + GetPeraMobileInterceptorConfig { + PeraMobileInterceptorPluginConfig( + baseUrl = "-", + apiKey = "-", + userAgent = PeraMobileUserAgent( + packageName = "-", + appVersion = "-", + appName = "-", + osVersion = "-", + deviceModel = "-", + languageTag = "-", + clientType = "-" + ) + ) + } + } + factory { + GetAlgodInterceptorConfig { + AlgodInterceptorPluginConfig( + baseUrl = "-", + apiKey = "-" + ) + } + } } diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/ViewModelModules.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/ViewModelModules.kt index abe89718..f8a848fd 100644 --- a/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/ViewModelModules.kt +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/di/ViewModelModules.kt @@ -1,6 +1,7 @@ package co.algorand.app.di -import co.algorand.app.ui.screens.home.AccountsViewModel +import co.algorand.app.ui.AppViewModel +import co.algorand.app.ui.screens.accounts.AccountsViewModel import co.algorand.app.ui.widgets.snackbar.SnackbarViewModel import com.algorand.common.viewmodel.EventDelegate import com.algorand.common.viewmodel.StateDelegate @@ -9,5 +10,6 @@ import org.koin.dsl.module val provideViewModelModules = module { single { SnackbarViewModel(EventDelegate()) } - viewModel { AccountsViewModel(get(), get(), get(), get(), get(), StateDelegate()) } + viewModel { AccountsViewModel(get(), get(), get(), get(), get(), StateDelegate(), get()) } + viewModel { AppViewModel(get()) } } diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/AppViewModel.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/AppViewModel.kt new file mode 100644 index 00000000..024eff7b --- /dev/null +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/AppViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package co.algorand.app.ui + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.algorand.common.cache.domain.usecase.InitializeAppCache +import kotlinx.coroutines.launch + +class AppViewModel( + private val initializeAppCache: InitializeAppCache +) : ViewModel() { + + fun initCache(lifecycle: Lifecycle) { + viewModelScope.launch { + initializeAppCache(lifecycle) + } + } +} diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/AppNavigation.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/AppNavigation.kt index 2020ef52..64a7197a 100644 --- a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/AppNavigation.kt +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/AppNavigation.kt @@ -7,25 +7,36 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import co.algorand.app.ui.AppViewModel import co.algorand.app.ui.screens.CoreActionsBottomSheet import co.algorand.app.ui.screens.PeraTypographyPreviewScreen import co.algorand.app.ui.screens.PeraTypographyPreviewScreenNavigation import co.algorand.app.ui.screens.QrScannerScreen import co.algorand.app.ui.screens.QrScannerScreenNavigation import com.algorand.common.ui.theme.PeraTheme +import org.koin.compose.viewmodel.koinViewModel @Composable -fun AppNavigation() { +fun AppNavigation( + appViewModel: AppViewModel = koinViewModel() +) { val navController = rememberNavController() val snackbarHostState = remember { SnackbarHostState() } val isBottomSheetVisible = remember { mutableStateOf(false) } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(Unit) { + appViewModel.initCache(lifecycleOwner.lifecycle) + } + Scaffold( modifier = Modifier .background(color = PeraTheme.colors.background) diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/BottomNavigationGraph.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/BottomNavigationGraph.kt index 28eed64a..3c9461d6 100644 --- a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/BottomNavigationGraph.kt +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/navigation/BottomNavigationGraph.kt @@ -19,7 +19,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute import co.algorand.app.ui.screens.DiscoverScreen -import co.algorand.app.ui.screens.home.AccountsScreen +import co.algorand.app.ui.screens.accounts.AccountsScreen import co.algorand.app.ui.screens.NftsScreen import co.algorand.app.ui.screens.SettingsScreen import co.algorand.app.ui.widgets.snackbar.SnackBarLayout diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsScreen.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsScreen.kt new file mode 100644 index 00000000..c8dba0c7 --- /dev/null +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsScreen.kt @@ -0,0 +1,318 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package co.algorand.app.ui.screens.accounts + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.navigation.NavController +import co.algorand.app.ui.widgets.snackbar.SnackbarViewModel +import com.algorand.common.account.info.domain.model.AccountInformation +import com.algorand.common.ui.theme.PeraTheme +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountsScreen( + navController: NavController, + snackbarViewModel: SnackbarViewModel, + tag: String, + accountsViewModel: AccountsViewModel = koinViewModel() +) { + + LifecycleStartEffect(Unit) { + accountsViewModel.initAccounts() + onStopOrDispose {} + } + + val bottomSheetState = rememberStandardBottomSheetState( + skipHiddenState = false, + initialValue = SheetValue.Hidden + ) + val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState) + var selectedAccountAddress by remember { mutableStateOf("") } + + var isRecoverAlgo25AccountModalVisible by remember { mutableStateOf(false) } + + val scope = rememberCoroutineScope() + + BottomSheetScaffold( + modifier = Modifier.fillMaxSize().background(PeraTheme.colors.background), + scaffoldState = scaffoldState, + sheetPeekHeight = 0.dp, + sheetContainerColor = PeraTheme.colors.background, + sheetContent = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + style = PeraTheme.typography.body.regular.sans, + text = selectedAccountAddress, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + OutlinedButton( + onClick = { + accountsViewModel.deleteAccount(selectedAccountAddress) + scope.launch { + scaffoldState.bottomSheetState.hide() + } + }, + ) { + Text(text = "Delete") + } + Spacer(modifier = Modifier.height(24.dp)) + } + }, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + val state = accountsViewModel.state.collectAsState() + val currentState = state.value + if (currentState is AccountsViewModel.ViewState.Accounts) { + val addresses = currentState.accounts.keys.toList() + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(addresses) { address -> + AccountItem( + address = address, + info = currentState.accounts[address] + ) { + selectedAccountAddress = address + scope.launch { + scaffoldState.bottomSheetState.expand() + } + } + } + } + } + + AccountActionsFab( + onAddAlgo25Click = accountsViewModel::addAlgo25Account, + onAddBip39Click = accountsViewModel::addBip39Account, + onRecoverClick = { + isRecoverAlgo25AccountModalVisible = true + } + ) + } + + if (isRecoverAlgo25AccountModalVisible) { + RecoverAlgo25AccountModal( + onDismiss = { + isRecoverAlgo25AccountModalVisible = false + }, + onRecoverAccount = { + accountsViewModel.recoverAccount(it) + } + ) + } + } +} + +@Composable +private fun AccountActionsFab( + onAddAlgo25Click: () -> Unit, + onAddBip39Click: () -> Unit, + onRecoverClick: () -> Unit +) { + var isMenuExpanded by remember { mutableStateOf(false) } + val rotationDegree = animateFloatAsState(if (isMenuExpanded) 45f else 0f) + Column( + horizontalAlignment = Alignment.End + ) { + AnimatedVisibility(isMenuExpanded) { + Column( + modifier = Modifier.padding(bottom = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.End + ) { + + ExtendedFloatingActionButton( + onClick = { + onRecoverClick() + isMenuExpanded = false + }, + icon = { }, + text = { Text(text = "Recover Algo25") }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ExtendedFloatingActionButton( + onClick = { + onAddBip39Click() + isMenuExpanded = false + }, + icon = { }, + text = { Text(text = "Add Bip39 account") }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ExtendedFloatingActionButton( + onClick = { + onAddAlgo25Click() + isMenuExpanded = false + }, + icon = { }, + text = { Text(text = "Add Algo25 account") }, + ) + } + + } + + SmallFloatingActionButton( + modifier = Modifier + .rotate(rotationDegree.value) + .padding(16.dp), + onClick = { isMenuExpanded = !isMenuExpanded }, + shape = CircleShape + ) { + Icon(Icons.Filled.Add, "Small floating action button.") + } + } +} + +@Composable +private fun AccountItem(address: String, info: AccountInformation?, onClick: (String) -> Unit) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .clickable { + if (info != null) { + onClick(address) + } + } + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + modifier = Modifier.padding(end = 16.dp), + text = "Address", + style = PeraTheme.typography.body.regular.sansBold, + color = PeraTheme.colors.textGray, + fontSize = 12.sp + ) + Text( + text = shortenAccountAddress(address) + ) + } + Column { + Text( + modifier = Modifier.padding(end = 16.dp), + text = "Algo Balance", + style = PeraTheme.typography.body.regular.sansBold, + color = PeraTheme.colors.textGray, + fontSize = 12.sp + ) + Text( + text = info?.amount ?: "-" + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RecoverAlgo25AccountModal( + onDismiss: () -> Unit = {}, + onRecoverAccount: (String) -> Unit = {} +) { + val textValue = remember { mutableStateOf("") } + ModalBottomSheet( + onDismissRequest = onDismiss, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier.fillMaxWidth(), + value = textValue.value, + onValueChange = { + textValue.value = it + }, + label = { Text("") } + ) + Spacer(modifier = Modifier.height(24.dp)) + OutlinedButton( + onClick = { + onRecoverAccount(textValue.value) + onDismiss() + }, + ) { + Text(text = "Recover") + } + } + } + ) +} + +private fun shortenAccountAddress(address: String): String { + val charSize = address.length + return address.slice(0..5) + "..." + address.slice(charSize - 5 until charSize) +} diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsViewModel.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsViewModel.kt similarity index 65% rename from composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsViewModel.kt rename to composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsViewModel.kt index 4c825fbc..6dd73d7d 100644 --- a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsViewModel.kt +++ b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/accounts/AccountsViewModel.kt @@ -10,11 +10,13 @@ * limitations under the License */ -package co.algorand.app.ui.screens.home +package co.algorand.app.ui.screens.accounts import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.algorand.app.ui.screens.home.AccountsViewModel.ViewState +import co.algorand.app.ui.screens.accounts.AccountsViewModel.ViewState +import com.algorand.common.account.info.domain.model.AccountInformation +import com.algorand.common.account.info.domain.usecase.GetAllAccountInformationFlow import com.algorand.common.account.local.domain.model.LocalAccount import com.algorand.common.account.local.domain.usecase.AddAlgo25Account import com.algorand.common.account.local.domain.usecase.AddBip39Account @@ -24,7 +26,9 @@ import com.algorand.common.algosdk.AlgoAccountSdk import com.algorand.common.viewmodel.StateDelegate import com.algorand.common.viewmodel.StateViewModel import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch class AccountsViewModel( @@ -33,7 +37,8 @@ class AccountsViewModel( private val addAlgo25Account: AddAlgo25Account, private val algoAccountSdk: AlgoAccountSdk, private val deleteLocalAccount: DeleteLocalAccount, - private val stateDelegate: StateDelegate + private val stateDelegate: StateDelegate, + private val getAllAccountInformationFlow: GetAllAccountInformationFlow ) : ViewModel(), StateViewModel by stateDelegate { private var accountObserveJob: Job? = null @@ -42,12 +47,32 @@ class AccountsViewModel( stateDelegate.setDefaultState(ViewState.Idle) } + fun recoverAccount(mnemonic: String) { + val account = algoAccountSdk.recoverAlgo25Account(mnemonic) + if (account != null) { + val localAccount = LocalAccount.Algo25( + address = account.address, + secretKey = account.secretKey + ) + viewModelScope.launch { + addAlgo25Account(localAccount) + } + } + } + fun initAccounts() { if (accountObserveJob != null) return accountObserveJob = viewModelScope.launch { - getAllLocalAccountAddressesAsFlow().collectLatest { addresses -> - stateDelegate.updateState { ViewState.Accounts(addresses) } - } + combine( + getAllLocalAccountAddressesAsFlow().distinctUntilChanged(), + getAllAccountInformationFlow().distinctUntilChanged() + ) { localAccounts, cachedAccounts -> + val accounts = mutableMapOf() + localAccounts.forEach { address -> + accounts[address] = cachedAccounts[address] + } + stateDelegate.updateState { ViewState.Accounts(accounts) } + }.launchIn(this) } } @@ -81,6 +106,6 @@ class AccountsViewModel( sealed interface ViewState { data object Idle : ViewState - data class Accounts(val accounts: List) : ViewState + data class Accounts(val accounts: Map) : ViewState } } diff --git a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsScreen.kt b/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsScreen.kt deleted file mode 100644 index 4c377abe..00000000 --- a/composeTestApp/src/commonMain/kotlin/co/algorand/app/ui/screens/home/AccountsScreen.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2022 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package co.algorand.app.ui.screens.home - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.SheetValue -import androidx.compose.material3.Text -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LifecycleStartEffect -import androidx.navigation.NavController -import co.algorand.app.ui.widgets.snackbar.SnackbarViewModel -import com.algorand.common.ui.theme.PeraTheme -import kotlinx.coroutines.launch -import org.koin.compose.viewmodel.koinViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AccountsScreen( - navController: NavController, - snackbarViewModel: SnackbarViewModel, - tag: String, - accountsViewModel: AccountsViewModel = koinViewModel() -) { - - LifecycleStartEffect(Unit) { - accountsViewModel.initAccounts() - onStopOrDispose {} - } - - val bottomSheetState = rememberStandardBottomSheetState( - skipHiddenState = false, - initialValue = SheetValue.Hidden - ) - val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState) - var selectedAccountAddress by remember { mutableStateOf("") } - - val scope = rememberCoroutineScope() - - BottomSheetScaffold( - modifier = Modifier.fillMaxSize().background(PeraTheme.colors.background), - scaffoldState = scaffoldState, - sheetPeekHeight = 0.dp, - sheetContainerColor = PeraTheme.colors.background, - sheetContent = { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - style = PeraTheme.typography.body.regular.sans, - text = selectedAccountAddress, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(24.dp)) - - OutlinedButton( - onClick = { - accountsViewModel.deleteAccount(selectedAccountAddress) - scope.launch { - scaffoldState.bottomSheetState.hide() - } - }, - ) { - Text(text = "Delete") - } - Spacer(modifier = Modifier.height(24.dp)) - } - }, - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - val state = accountsViewModel.state.collectAsState() - val currentState = state.value - if (currentState is AccountsViewModel.ViewState.Accounts) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(currentState.accounts) { address -> - Text( - modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - selectedAccountAddress = address - scope.launch { - scaffoldState.bottomSheetState.expand() - } - }, - text = shortenAccountAddress(address) - ) - } - } - } - - ExtendedFloatingActionButton( - onClick = { accountsViewModel.addAlgo25Account() }, - modifier = Modifier.padding(end = 16.dp, bottom = 24.dp), - icon = { Icon(Icons.Filled.Add, "Add Algo25 account") }, - text = { Text(text = "Add Algo25 account") }, - ) - - ExtendedFloatingActionButton( - onClick = { accountsViewModel.addBip39Account() }, - modifier = Modifier.padding(end = 16.dp, bottom = 100.dp), - icon = { Icon(Icons.Filled.Add, "Add Bip39 account") }, - text = { Text(text = "Add Bip39 account") }, - ) - } - } -} - -private fun shortenAccountAddress(address: String): String { - val charSize = address.length - return address.slice(0..5) + "..." + address.slice(charSize - 5 until charSize) -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ef460ca..ba557695 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,6 +48,7 @@ khex = "1.1.2" koin = "4.0.1-Beta1" kotlin = "2.0.21" kotlinfixture = "1.2.0" +kotlinxDatetime = "0.5.0" kotlinxSerialization = "1.7.3" kotlinTest = "2.0.21" kspGradlePlugin = "2.0.21-1.0.25" @@ -136,6 +137,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-iosArm64 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-iosArm64", version.ref = "coroutines" } kotlinx-coroutines-iosX64 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines--core-iosX64", version.ref = "coroutines" } kotlinx-coroutines-iosSimulatorArm64 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-iosSimulatorArm64", version.ref = "coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinfixture = { module = "com.appmattus.fixture:fixture", version.ref = "kotlinfixture" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } diff --git a/wallet-sdk/build.gradle.kts b/wallet-sdk/build.gradle.kts index b576e970..05c46faf 100644 --- a/wallet-sdk/build.gradle.kts +++ b/wallet-sdk/build.gradle.kts @@ -104,6 +104,7 @@ kotlin { implementation(libs.ktor.client.core) implementation(libs.room.runtime) implementation(libs.sqlite.bundled) + implementation(libs.kotlinx.datetime) } iosMain.dependencies { implementation(libs.ktor.client.darwin) diff --git a/wallet-sdk/src/androidMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.android.kt b/wallet-sdk/src/androidMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.android.kt deleted file mode 100644 index e25ca24f..00000000 --- a/wallet-sdk/src/androidMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.android.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2022 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.common.account.info.data.service - -import com.algorand.common.foundation.network.IndexerInterceptorPlugin -import com.algorand.common.foundation.network.PeraJsonNegotiation -import io.ktor.client.HttpClient -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.ANDROID -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.serialization.kotlinx.json.json - -internal actual fun getIndexerApiHttpClient(indexerInterceptorPlugin: IndexerInterceptorPlugin): HttpClient { - return HttpClient(OkHttp) { - install(Logging) { - logger = Logger.ANDROID - level = LogLevel.BODY - } - install(ContentNegotiation) { - json(PeraJsonNegotiation) - } - install(indexerInterceptorPlugin) - } -} diff --git a/wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.android.kt b/wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.android.kt similarity index 75% rename from wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.android.kt rename to wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.android.kt index 0ae127ea..f1b85a99 100644 --- a/wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.android.kt +++ b/wallet-sdk/src/androidMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.android.kt @@ -14,22 +14,16 @@ package com.algorand.common.foundation.network import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.ANDROID import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging -import io.ktor.serialization.kotlinx.json.json -internal actual fun getAlgodHttpClient(algodInterceptorPlugin: AlgodInterceptorPlugin): HttpClient { +internal actual fun createHttpClient(): HttpClient { return HttpClient(OkHttp) { install(Logging) { logger = Logger.ANDROID level = LogLevel.BODY } - install(ContentNegotiation) { - json(PeraJsonNegotiation) - } - install(algodInterceptorPlugin) } } diff --git a/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImplTest.kt b/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImplTest.kt index dcb2ed77..64c566a4 100644 --- a/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImplTest.kt +++ b/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImplTest.kt @@ -33,14 +33,16 @@ internal class LedgerBleEntityMapperImplTest { LocalAccount.LedgerBle( address = "unencrypted_address", deviceMacAddress = "mac_address", - indexInLedger = 1 + indexInLedger = 1, + bluetoothName = "bluetooth_name" ) ) val expected = LedgerBleEntity( encryptedAddress = "encrypted_address", deviceMacAddress = "mac_address", - accountIndexInLedger = 1 + accountIndexInLedger = 1, + bluetoothName = "bluetooth_name" ) assertEquals(expected, result) } diff --git a/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImplTest.kt b/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImplTest.kt index bf946af6..a2b73963 100644 --- a/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImplTest.kt +++ b/wallet-sdk/src/androidUnitTest/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImplTest.kt @@ -36,7 +36,8 @@ internal class LedgerBleMapperImplTest { val expected = LocalAccount.LedgerBle( address = "decrypted_address", deviceMacAddress = LEDGER_BLE_ENTITY.deviceMacAddress, - indexInLedger = LEDGER_BLE_ENTITY.accountIndexInLedger + indexInLedger = LEDGER_BLE_ENTITY.accountIndexInLedger, + bluetoothName = LEDGER_BLE_ENTITY.bluetoothName ) assertEquals(expected, result) } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/di/AccountDetailKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/di/AccountDetailKoinModule.kt new file mode 100644 index 00000000..03c70513 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/di/AccountDetailKoinModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.di + +import com.algorand.common.account.detail.domain.usecase.GetAccountRegistrationType +import com.algorand.common.account.detail.domain.usecase.GetAccountRegistrationTypeUseCase +import com.algorand.common.account.detail.domain.usecase.GetAccountState +import com.algorand.common.account.detail.domain.usecase.GetAccountStateUseCase +import com.algorand.common.account.detail.domain.usecase.GetAccountType +import com.algorand.common.account.detail.domain.usecase.GetAccountTypeUseCase +import org.koin.dsl.module + +internal val accountDetailKoinModule = module { + factory { GetAccountTypeUseCase(get(), get()) } + factory { GetAccountRegistrationTypeUseCase(get()) } + factory { GetAccountStateUseCase(get(), get()) } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountRegistrationType.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountRegistrationType.kt new file mode 100644 index 00000000..d436cec4 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountRegistrationType.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.model + +sealed interface AccountRegistrationType { + + data object Algo25 : AccountRegistrationType + + data object LedgerBle : AccountRegistrationType + + data object NoAuth : AccountRegistrationType + + data object Bip39 : AccountRegistrationType +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountState.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountState.kt new file mode 100644 index 00000000..26336d55 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountState.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.model + +data class AccountState( + val address: String, + val accountRegistrationType: AccountRegistrationType?, + val accountType: AccountType? +) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountType.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountType.kt new file mode 100644 index 00000000..986f275b --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/model/AccountType.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.model + +sealed interface AccountType { + + data object Algo25 : AccountType + + data object LedgerBle : AccountType + + data object Rekeyed : AccountType + + data object RekeyedAuth : AccountType + + data object NoAuth : AccountType + + data object Bip39 : AccountType + + companion object { + fun AccountType.canSignTransaction(): Boolean { + return this is Algo25 || this is LedgerBle || this is RekeyedAuth + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/AccountDetailUseCases.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/AccountDetailUseCases.kt new file mode 100644 index 00000000..b52282e3 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/AccountDetailUseCases.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.usecase + +import com.algorand.common.account.detail.domain.model.AccountRegistrationType +import com.algorand.common.account.detail.domain.model.AccountState +import com.algorand.common.account.detail.domain.model.AccountType + +fun interface GetAccountState { + suspend operator fun invoke(address: String): AccountState +} + +fun interface GetAccountType { + suspend operator fun invoke(address: String): AccountType? +} + +fun interface GetAccountRegistrationType { + suspend operator fun invoke(address: String): AccountRegistrationType? +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountRegistrationTypeUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountRegistrationTypeUseCase.kt new file mode 100644 index 00000000..0b18d11a --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountRegistrationTypeUseCase.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.usecase + +import com.algorand.common.account.detail.domain.model.AccountRegistrationType +import com.algorand.common.account.local.domain.model.LocalAccount +import com.algorand.common.account.local.domain.usecase.GetLocalAccounts + +internal class GetAccountRegistrationTypeUseCase( + private val getLocalAccounts: GetLocalAccounts +) : GetAccountRegistrationType { + + override suspend fun invoke(address: String): AccountRegistrationType? { + return when (getLocalAccounts().firstOrNull { it.address == address }) { + is LocalAccount.Algo25 -> AccountRegistrationType.Algo25 + is LocalAccount.LedgerBle -> AccountRegistrationType.LedgerBle + is LocalAccount.NoAuth -> AccountRegistrationType.NoAuth + is LocalAccount.Bip39 -> AccountRegistrationType.Bip39 + else -> null + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountStateUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountStateUseCase.kt new file mode 100644 index 00000000..2937b4ab --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountStateUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.usecase + +import com.algorand.common.account.detail.domain.model.AccountState + +internal class GetAccountStateUseCase( + private val getAccountType: GetAccountType, + private val getAccountRegistrationType: GetAccountRegistrationType +) : GetAccountState { + + override suspend operator fun invoke(address: String): AccountState { + return AccountState( + address = address, + accountRegistrationType = getAccountRegistrationType(address), + accountType = getAccountType(address) + ) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountTypeUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountTypeUseCase.kt new file mode 100644 index 00000000..f26a087a --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/detail/domain/usecase/GetAccountTypeUseCase.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.detail.domain.usecase + +import com.algorand.common.account.detail.domain.model.AccountType +import com.algorand.common.account.info.domain.model.AccountInformation +import com.algorand.common.account.info.domain.usecase.GetAccountInformation +import com.algorand.common.account.local.domain.model.LocalAccount +import com.algorand.common.account.local.domain.usecase.GetLocalAccounts + +internal class GetAccountTypeUseCase( + private val getLocalAccounts: GetLocalAccounts, + private val getAccountInformation: GetAccountInformation +) : GetAccountType { + + override suspend fun invoke(address: String): AccountType? { + val localAccounts = getLocalAccounts() + val cachedAccount = getAccountInformation(address) ?: return null + val account = localAccounts.firstOrNull { it.address == address } ?: return null + return if (cachedAccount.rekeyAdminAddress != null) { + getAccountTypeForRekeyedAccount(account, localAccounts, cachedAccount) + } else { + getAccountTypeForNonRekeyedAccount(account) + } + } + + private fun getAccountTypeForRekeyedAccount( + account: LocalAccount, + localAccounts: List, + cachedAccount: AccountInformation + ): AccountType { + val doWeHaveSigner = localAccounts.any { it.address == cachedAccount.rekeyAdminAddress } + return when { + doWeHaveSigner -> AccountType.RekeyedAuth + account is LocalAccount.NoAuth -> AccountType.NoAuth + else -> AccountType.Rekeyed + } + } + + private fun getAccountTypeForNonRekeyedAccount(account: LocalAccount): AccountType { + return when (account) { + is LocalAccount.Algo25 -> AccountType.Algo25 + is LocalAccount.LedgerBle -> AccountType.LedgerBle + is LocalAccount.NoAuth -> AccountType.NoAuth + is LocalAccount.Bip39 -> AccountType.Bip39 + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/database/dao/AssetHoldingDao.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/database/dao/AssetHoldingDao.kt index 57a5fbe9..41ea8271 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/database/dao/AssetHoldingDao.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/database/dao/AssetHoldingDao.kt @@ -70,7 +70,7 @@ internal interface AssetHoldingDao { } @Query("SELECT * FROM asset_holding_table") - fun getAllAsFlow(): Flow + fun getAllAsFlow(): Flow> @Query("SELECT * FROM asset_holding_table WHERE encrypted_address = :encryptedAddress") fun getAssetsByAddressAsFlow(encryptedAddress: String): Flow> diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelper.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelper.kt new file mode 100644 index 00000000..fee2c632 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelper.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.model.AssetHoldingResponse +import com.algorand.common.foundation.PeraResult + +internal interface AccountAssetHoldingsFetchHelper { + suspend fun fetchAccountAssetHoldings(accountAddress: String): PeraResult> +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelperImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelperImpl.kt new file mode 100644 index 00000000..08d2810a --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountAssetHoldingsFetchHelperImpl.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.model.AssetHoldingResponse +import com.algorand.common.account.info.data.service.AccountInformationApiService +import com.algorand.common.foundation.PeraResult + +internal class AccountAssetHoldingsFetchHelperImpl( + private val indexerApi: AccountInformationApiService +) : AccountAssetHoldingsFetchHelper { + + override suspend fun fetchAccountAssetHoldings(accountAddress: String): PeraResult> { + return fetch(accountAddress, null, mutableListOf()) + } + + private suspend fun fetch( + address: String, + nextToken: String?, + holdings: MutableList + ): PeraResult> { + return indexerApi.getAccountAssets(address, ASSET_FETCH_LIMIT_PER_PAGE, nextToken).use( + onSuccess = { response -> + holdings.addAll(response.assets.orEmpty()) + if (response.nextToken != null && !response.assets.isNullOrEmpty()) { + fetch(address, response.nextToken, holdings) + } else { + PeraResult.Success(holdings) + } + }, + onFailed = { exception, code -> + PeraResult.Error(exception, code) + } + ) + } + + companion object { + private const val ASSET_FETCH_LIMIT_PER_PAGE = 5000 + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelper.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelper.kt new file mode 100644 index 00000000..c4f7026c --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelper.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.model.AccountInformationResponse +import com.algorand.common.account.info.domain.model.AccountInformation + +internal interface AccountInformationCacheHelper { + suspend fun cacheAccountInformation(address: String, response: AccountInformationResponse): AccountInformation? +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelperImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelperImpl.kt new file mode 100644 index 00000000..6ec1e46e --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationCacheHelperImpl.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.database.dao.AccountInformationDao +import com.algorand.common.account.info.data.database.model.AccountInformationEntity +import com.algorand.common.account.info.data.mapper.AccountInformationEntityMapper +import com.algorand.common.account.info.data.mapper.AccountInformationErrorEntityMapper +import com.algorand.common.account.info.data.mapper.AccountInformationMapper +import com.algorand.common.account.info.data.model.AccountInformationResponse +import com.algorand.common.account.info.domain.model.AccountInformation +import com.algorand.common.account.info.domain.model.AssetHolding + +internal class AccountInformationCacheHelperImpl ( + private val accountInformationEntityMapper: AccountInformationEntityMapper, + private val accountInformationMapper: AccountInformationMapper, + private val accountInformationDao: AccountInformationDao, + private val accountInformationErrorEntityMapper: AccountInformationErrorEntityMapper, + private val assetHoldingCacheHelper: AssetHoldingCacheHelper +) : AccountInformationCacheHelper { + + override suspend fun cacheAccountInformation( + address: String, + response: AccountInformationResponse + ): AccountInformation? { + val entity = accountInformationEntityMapper(response) + return if (entity != null) { + val assetHoldings = + assetHoldingCacheHelper.cacheAssetHolding(address, response.accountInformation?.allAssetHoldingList) + cacheAccountInformation(entity, assetHoldings) + } else { + cacheErrorAccountInformation(address) + null + } + } + + private suspend fun cacheAccountInformation( + entity: AccountInformationEntity, + assetHoldings: List + ): AccountInformation { + accountInformationDao.insert(entity) + return accountInformationMapper(entity, assetHoldings) + } + + private suspend fun cacheErrorAccountInformation(address: String) { + val errorEntity = accountInformationErrorEntityMapper(address) + accountInformationDao.insert(errorEntity) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelper.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelper.kt new file mode 100644 index 00000000..08e02f57 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelper.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.model.AccountInformationResponse +import com.algorand.common.foundation.PeraResult + +internal interface AccountInformationFetchHelper { + suspend fun fetchAccount(address: String): PeraResult +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelperImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelperImpl.kt new file mode 100644 index 00000000..170dafa8 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationFetchHelperImpl.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.mapper.AccountInformationResponseMapper +import com.algorand.common.account.info.data.model.AccountInformationResponse +import com.algorand.common.account.info.data.model.IndexerAccountFetchRequestExcludes.ASSETS +import com.algorand.common.account.info.data.model.IndexerAccountFetchRequestExcludes.CREATED_APPS +import com.algorand.common.account.info.data.model.IndexerAccountFetchRequestExcludes.CREATED_ASSETS +import com.algorand.common.account.info.data.service.AccountInformationApiService +import com.algorand.common.foundation.PeraResult +import kotlinx.io.IOException + +internal class AccountInformationFetchHelperImpl( + private val indexerApi: AccountInformationApiService, + private val accountAssetHoldingsFetchHelper: AccountAssetHoldingsFetchHelper, + private val accountInformationResponseMapper: AccountInformationResponseMapper, +) : AccountInformationFetchHelper { + + override suspend fun fetchAccount(address: String): PeraResult { + val excludesQuery = IndexerAccountFetchRequestExcludesQueryBuilder.newBuilder() + .addExclude(CREATED_ASSETS) + .addExclude(CREATED_APPS) + .build() + return indexerApi.getAccountInformation(address, excludesQuery).use( + onSuccess = { + PeraResult.Success(it) + }, + onFailed = { exception, code -> + processFailedResponse(address, exception, code) + } + ) + } + + private suspend fun processFailedResponse( + address: String, + exception: Exception, + errorCode: Int? + ): PeraResult { + return when { + errorCode == ACCOUNT_NOT_FOUND -> { + PeraResult.Success(accountInformationResponseMapper.createEmptyAccount(address)) + } + exception is IOException -> PeraResult.Error(exception, errorCode) + else -> fetchAccountAndAssetsSeparately(address) + } + } + + private suspend fun fetchAccountAndAssetsSeparately(address: String): PeraResult { + val excludesQuery = IndexerAccountFetchRequestExcludesQueryBuilder.newBuilder() + .addExclude(CREATED_ASSETS) + .addExclude(CREATED_APPS) + .addExclude(ASSETS) + .build() + return indexerApi.getAccountInformation(address, excludesQuery).use( + onSuccess = { response -> + fetchAssets(address, response) + }, + onFailed = { exception, code -> + PeraResult.Error(exception, code) + } + ) + } + + private suspend fun fetchAssets( + address: String, + response: AccountInformationResponse + ): PeraResult { + return accountAssetHoldingsFetchHelper.fetchAccountAssetHoldings(address).use( + onSuccess = { assetHoldings -> + val accountInfo = response.copy( + accountInformation = response.accountInformation?.copy(allAssetHoldingList = assetHoldings) + ) + PeraResult.Success(accountInfo) + }, + onFailed = { exception, code -> + PeraResult.Error(exception, code) + } + ) + } + + private companion object { + const val ACCOUNT_NOT_FOUND = 404 + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationRepositoryImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationRepositoryImpl.kt new file mode 100644 index 00000000..365c5b06 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AccountInformationRepositoryImpl.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.database.dao.AccountInformationDao +import com.algorand.common.account.info.data.database.dao.AssetHoldingDao +import com.algorand.common.account.info.data.mapper.AccountInformationMapper +import com.algorand.common.account.info.data.mapper.AssetHoldingMapper +import com.algorand.common.account.info.domain.model.AccountInformation +import com.algorand.common.account.info.domain.repository.AccountInformationRepository +import com.algorand.common.encryption.AddressEncryptionManager +import com.algorand.common.foundation.PeraResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.withContext + +internal class AccountInformationRepositoryImpl( + private val accountInformationMapper: AccountInformationMapper, + private val accountInformationDao: AccountInformationDao, + private val assetHoldingDao: AssetHoldingDao, + private val assetHoldingMapper: AssetHoldingMapper, + private val addressEncryptionManager: AddressEncryptionManager, + private val accountInformationCacheHelper: AccountInformationCacheHelper, + private val accountInformationFetchHelper: AccountInformationFetchHelper +) : AccountInformationRepository { + + override suspend fun fetchAccountInformation(address: String): PeraResult { + return accountInformationFetchHelper.fetchAccount(address).use( + onSuccess = { response -> + val accountInformation = accountInformationMapper(response) + if (accountInformation == null) { + PeraResult.Error(Exception()) + } else { + PeraResult.Success(accountInformation) + } + }, + onFailed = { exception, _ -> + PeraResult.Error(exception) + } + ) + } + + override fun getCachedAccountInformationCountFlow(): Flow { + return accountInformationDao.getTableSizeAsFlow() + } + + override suspend fun getAllAssetHoldingIds(addresses: List): List { + return assetHoldingDao.getAssetIdsByAddresses(addresses).toSet().toList() + } + + override suspend fun fetchAndCacheAccountInformation( + addresses: List + ): Map { + return withContext(Dispatchers.IO) { + val result = mutableMapOf() + addresses.map { address -> + async { + result[address] = accountInformationFetchHelper.fetchAccount(address).use( + onSuccess = { response -> + accountInformationCacheHelper.cacheAccountInformation(address, response) + }, + onFailed = { _, _ -> + null + } + ) + } + }.awaitAll() + result + } + } + + override suspend fun getAllAccountInformation(): Map { + val accountInformationMap = mutableMapOf() + accountInformationDao.getAll().forEach { + val assetEntities = assetHoldingDao.getAssetsByAddress(it.encryptedAddress) + val assetHoldings = assetHoldingMapper(assetEntities) + val decryptedAddress = addressEncryptionManager.decrypt(it.encryptedAddress) + accountInformationMap[decryptedAddress] = accountInformationMapper(it, assetHoldings) + } + return accountInformationMap + } + + override fun getAllAccountInformationFlow(): Flow> { + return combine( + accountInformationDao.getAllAsFlow(), + assetHoldingDao.getAllAsFlow() + ) { accountInformationEntities, _ -> + accountInformationEntities.associate { + val assetEntities = assetHoldingDao.getAssetsByAddress(it.encryptedAddress) + val assetHoldings = assetHoldingMapper(assetEntities) + val decryptedAddress = addressEncryptionManager.decrypt(it.encryptedAddress) + decryptedAddress to accountInformationMapper(it, assetHoldings) + } + }.distinctUntilChanged() + } + + override suspend fun getEarliestLastFetchedRound(): Long { + return accountInformationDao.getEarliestLastFetchedRound() ?: DEFAULT_EARLIEST_LAST_FETCHED_ROUND + } + + override suspend fun clearCache() { + accountInformationDao.clearAll() + assetHoldingDao.clearAll() + } + + override suspend fun getAccountInformation(address: String): AccountInformation? { + val encryptedAddress = addressEncryptionManager.encrypt(address) + val accountInformationEntity = accountInformationDao.get(encryptedAddress) ?: return null + + val assetEntities = assetHoldingDao.getAssetsByAddress(encryptedAddress) + val assetHoldings = assetHoldingMapper(assetEntities) + + return accountInformationMapper(accountInformationEntity, assetHoldings) + } + + override fun getAccountInformationFlow(address: String): Flow { + val encryptedAddress = addressEncryptionManager.encrypt(address) + return combine( + accountInformationDao.getAsFlow(encryptedAddress), + assetHoldingDao.getAssetsByAddressAsFlow(encryptedAddress) + ) { accountInformation, assetHoldingEntities -> + if (accountInformation == null) return@combine null + val assetHoldings = assetHoldingMapper(assetHoldingEntities) + accountInformationMapper(accountInformation, assetHoldings) + }.distinctUntilChanged() + } + + override suspend fun deleteAccountInformation(address: String) { + val encryptedAddress = addressEncryptionManager.encrypt(address) + accountInformationDao.delete(encryptedAddress) + assetHoldingDao.deleteByAddress(encryptedAddress) + } + + companion object { + private const val DEFAULT_EARLIEST_LAST_FETCHED_ROUND = 0L + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelper.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelper.kt new file mode 100644 index 00000000..527c2c88 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelper.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.model.AssetHoldingResponse +import com.algorand.common.account.info.domain.model.AssetHolding + +internal interface AssetHoldingCacheHelper { + suspend fun cacheAssetHolding(address: String, assetHoldings: List?): List +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelperImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelperImpl.kt new file mode 100644 index 00000000..1a64135b --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/AssetHoldingCacheHelperImpl.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.database.dao.AssetHoldingDao +import com.algorand.common.account.info.data.database.model.AssetHoldingEntity +import com.algorand.common.account.info.data.mapper.AssetHoldingEntityMapper +import com.algorand.common.account.info.data.mapper.AssetHoldingMapper +import com.algorand.common.account.info.data.model.AssetHoldingResponse +import com.algorand.common.account.info.domain.model.AssetHolding + +internal class AssetHoldingCacheHelperImpl ( + private val assetHoldingDao: AssetHoldingDao, + private val assetHoldingEntityMapper: AssetHoldingEntityMapper, + private val assetHoldingMapper: AssetHoldingMapper, +) : AssetHoldingCacheHelper { + + override suspend fun cacheAssetHolding( + address: String, + assetHoldings: List? + ): List { + val assetHoldingEntities = getAssetHoldingEntities(address, assetHoldings) + assetHoldingDao.updateAssetHoldings(address, assetHoldingEntities) + return assetHoldingMapper(assetHoldingEntities) + } + + private fun getAssetHoldingEntities( + address: String, + assetHoldings: List? + ): List { + return assetHoldings?.mapNotNull { + assetHoldingEntityMapper(address, it) + }.orEmpty() + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/IndexerAccountFetchRequestExcludesQueryBuilder.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/IndexerAccountFetchRequestExcludesQueryBuilder.kt new file mode 100644 index 00000000..c1da1e5d --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/repository/IndexerAccountFetchRequestExcludesQueryBuilder.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.data.repository + +import com.algorand.common.account.info.data.model.IndexerAccountFetchRequestExcludes + +internal class IndexerAccountFetchRequestExcludesQueryBuilder private constructor() { + + private val excludes = mutableListOf() + + fun addExclude(exclude: IndexerAccountFetchRequestExcludes): IndexerAccountFetchRequestExcludesQueryBuilder { + excludes.add(exclude.query) + return this + } + + fun build(): String { + return excludes.joinToString(",") + } + + companion object { + fun newBuilder(): IndexerAccountFetchRequestExcludesQueryBuilder { + return IndexerAccountFetchRequestExcludesQueryBuilder() + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiService.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/AccountInformationApiService.kt similarity index 96% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiService.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/AccountInformationApiService.kt index a36268d8..36103a34 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiService.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/AccountInformationApiService.kt @@ -17,7 +17,7 @@ import com.algorand.common.account.info.data.model.AccountInformationResponse import com.algorand.common.account.info.data.model.RekeyedAccountsResponse import com.algorand.common.foundation.PeraResult -internal interface IndexerApiService { +internal interface AccountInformationApiService { suspend fun getAccountInformation( publicKey: String, diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/AccountInformationApiServiceImpl.kt similarity index 96% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceImpl.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/AccountInformationApiServiceImpl.kt index e12f6728..8aed3e8f 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceImpl.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/AccountInformationApiServiceImpl.kt @@ -21,9 +21,9 @@ import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.request.parameter -internal class IndexerApiServiceImpl( +internal class AccountInformationApiServiceImpl( private val client: HttpClient -) : IndexerApiService { +) : AccountInformationApiService { override suspend fun getAccountInformation( publicKey: String, diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/di/AccountInformationKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/di/AccountInformationKoinModule.kt index 0ad7cd70..09ab6c34 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/di/AccountInformationKoinModule.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/di/AccountInformationKoinModule.kt @@ -28,17 +28,50 @@ import com.algorand.common.account.info.data.mapper.AssetHoldingEntityMapper import com.algorand.common.account.info.data.mapper.AssetHoldingEntityMapperImpl import com.algorand.common.account.info.data.mapper.AssetHoldingMapper import com.algorand.common.account.info.data.mapper.AssetHoldingMapperImpl -import com.algorand.common.account.info.data.service.IndexerApiService -import com.algorand.common.account.info.data.service.IndexerApiServiceImpl -import com.algorand.common.account.info.data.service.getIndexerApiHttpClient +import com.algorand.common.account.info.data.repository.AccountAssetHoldingsFetchHelper +import com.algorand.common.account.info.data.repository.AccountAssetHoldingsFetchHelperImpl +import com.algorand.common.account.info.data.repository.AccountInformationCacheHelper +import com.algorand.common.account.info.data.repository.AccountInformationCacheHelperImpl +import com.algorand.common.account.info.data.repository.AccountInformationFetchHelper +import com.algorand.common.account.info.data.repository.AccountInformationFetchHelperImpl +import com.algorand.common.account.info.data.repository.AccountInformationRepositoryImpl +import com.algorand.common.account.info.data.repository.AssetHoldingCacheHelper +import com.algorand.common.account.info.data.repository.AssetHoldingCacheHelperImpl +import com.algorand.common.account.info.data.service.AccountInformationApiService +import com.algorand.common.account.info.data.service.AccountInformationApiServiceImpl +import com.algorand.common.account.info.domain.manager.AccountCacheManager +import com.algorand.common.account.info.domain.manager.AccountCacheManagerImpl +import com.algorand.common.account.info.domain.repository.AccountInformationRepository +import com.algorand.common.account.info.domain.usecase.ClearAccountInformationCache +import com.algorand.common.account.info.domain.usecase.FetchAndCacheAccountInformation +import com.algorand.common.account.info.domain.usecase.GetAccountDetailCacheStatusFlow +import com.algorand.common.account.info.domain.usecase.GetAccountDetailCacheStatusFlowUseCase +import com.algorand.common.account.info.domain.usecase.GetAccountInformation +import com.algorand.common.account.info.domain.usecase.GetAllAccountInformation +import com.algorand.common.account.info.domain.usecase.GetAllAccountInformationFlow +import com.algorand.common.account.info.domain.usecase.GetAllAssetHoldingIds +import com.algorand.common.account.info.domain.usecase.GetCachedAccountInformationCountFlow +import com.algorand.common.account.info.domain.usecase.GetEarliestLastFetchedRound import com.algorand.common.foundation.database.PeraDatabase +import com.algorand.common.foundation.network.indexer.getIndexerApiHttpClient import org.koin.dsl.module internal val accountInformationKoinModule = module { - single { - IndexerApiServiceImpl(getIndexerApiHttpClient(get())) + single { AccountCacheManagerImpl(get(), get(), get(), get(), get(), get()) } + + single { + AccountInformationApiServiceImpl(getIndexerApiHttpClient(get())) + } + + single { + AccountInformationRepositoryImpl(get(), get(), get(), get(), get(), get(), get()) } + single { AccountInformationCacheHelperImpl(get(), get(), get(), get(), get()) } + single { AssetHoldingCacheHelperImpl(get(), get(), get()) } + single { AccountInformationFetchHelperImpl(get(), get(), get()) } + single { AccountAssetHoldingsFetchHelperImpl(get()) } + factory { AccountInformationMapperImpl(get(), get(), get()) } factory { AppStateSchemeMapperImpl() } factory { AssetHoldingMapperImpl() } @@ -52,4 +85,45 @@ internal val accountInformationKoinModule = module { factory { AccountInformationEntityMapperImpl(get()) } factory { AccountInformationErrorEntityMapperImpl(get()) } factory { AssetHoldingEntityMapperImpl(get()) } + factory { GetAccountDetailCacheStatusFlowUseCase(get(), get()) } + factory { + FetchAndCacheAccountInformation { addresses -> + get().fetchAndCacheAccountInformation(addresses) + } + } + factory { + GetEarliestLastFetchedRound { + get().getEarliestLastFetchedRound() + } + } + factory { + GetAllAccountInformation { + get().getAllAccountInformation() + } + } + factory { + GetAllAssetHoldingIds { accountAddresses -> + get().getAllAssetHoldingIds(accountAddresses) + } + } + factory { + GetCachedAccountInformationCountFlow { + get().getCachedAccountInformationCountFlow() + } + } + factory { + ClearAccountInformationCache { + get().clearCache() + } + } + factory { + GetAllAccountInformationFlow { + get().getAllAccountInformationFlow() + } + } + factory { + GetAccountInformation { address -> + get().getAccountInformation(address) + } + } } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManager.kt similarity index 68% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManager.kt index b054fecf..63cdf262 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManager.kt @@ -10,9 +10,10 @@ * limitations under the License */ -package com.algorand.common.account.info.data.service +package com.algorand.common.account.info.domain.manager -import com.algorand.common.foundation.network.IndexerInterceptorPlugin -import io.ktor.client.HttpClient +import androidx.lifecycle.Lifecycle -internal expect fun getIndexerApiHttpClient(indexerInterceptorPlugin: IndexerInterceptorPlugin): HttpClient +interface AccountCacheManager { + fun initialize(lifecycle: Lifecycle) +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManagerImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManagerImpl.kt new file mode 100644 index 00000000..d5c78878 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/manager/AccountCacheManagerImpl.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.domain.manager + +import androidx.lifecycle.Lifecycle +import com.algorand.common.account.local.domain.usecase.GetLocalAccountCountFlow +import com.algorand.common.block.domain.usecase.ClearLastKnownBlockNumber +import com.algorand.common.block.domain.usecase.ShouldUpdateAccountCache +import com.algorand.common.block.domain.usecase.UpdateLastKnownBlockNumber +import com.algorand.common.cache.LifecycleAwareCacheManager +import com.algorand.common.cache.domain.usecase.UpdateAccountCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest + +internal class AccountCacheManagerImpl( + private val cacheManager: LifecycleAwareCacheManager, + private val clearLastKnownBlockNumber: ClearLastKnownBlockNumber, + private val updateAccountCache: UpdateAccountCache, + private val updateLastKnownBlockNumber: UpdateLastKnownBlockNumber, + private val shouldUpdateAccountCache: ShouldUpdateAccountCache, + private val getLocalAccountCountFlow: GetLocalAccountCountFlow +) : AccountCacheManager { + + private val cacheManagerListener = object : LifecycleAwareCacheManager.CacheManagerListener { + override suspend fun onInitializeManager(coroutineScope: CoroutineScope) { + initialize() + } + + override suspend fun onStartJob(coroutineScope: CoroutineScope) { + runManagerJob() + } + } + + private val localAccountCollector: suspend (Int) -> Unit = { accountCount -> + if (accountCount > 0) cacheManager.startJob() else cacheManager.stopCurrentJob() + } + + override fun initialize(lifecycle: Lifecycle) { + cacheManager.setListener(cacheManagerListener) + lifecycle.addObserver(cacheManager) + } + + private suspend fun runManagerJob() { + initializeJob() + updateLastKnownBlockNumber() + while (true) { + updateCacheIfRequired() + delay(NEXT_BLOCK_DELAY_AFTER) + } + } + + private suspend fun initialize() { + getLocalAccountCountFlow().collectLatest(localAccountCollector) + } + + private suspend fun initializeJob() { + clearLastKnownBlockNumber() + } + + private suspend fun updateCacheIfRequired() { + shouldUpdateAccountCache().use( + onSuccess = { shouldUpdate -> + if (shouldUpdate) updateCacheAndLastKnownBlock() + }, + onFailed = { _, _ -> + updateCacheAndLastKnownBlock() + } + ) + } + + private suspend fun updateCacheAndLastKnownBlock() { + updateAccountCache() + updateLastKnownBlockNumber() + } + + private companion object { + const val NEXT_BLOCK_DELAY_AFTER = 3500L + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/AccountAssets.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/AccountCacheStatus.kt similarity index 86% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/AccountAssets.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/AccountCacheStatus.kt index b75721a1..beb7404c 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/AccountAssets.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/model/AccountCacheStatus.kt @@ -12,7 +12,9 @@ package com.algorand.common.account.info.domain.model -internal data class AccountAssets( - val address: String, - val assetHoldings: List -) +enum class AccountCacheStatus { + IDLE, + LOADING, + INITIALIZED, + ERROR +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/repository/AccountInformationRepository.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/repository/AccountInformationRepository.kt new file mode 100644 index 00000000..97a0fffd --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/repository/AccountInformationRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.domain.repository + +import com.algorand.common.account.info.domain.model.AccountInformation +import com.algorand.common.foundation.PeraResult +import kotlinx.coroutines.flow.Flow + +internal interface AccountInformationRepository { + + suspend fun fetchAccountInformation(address: String): PeraResult + + suspend fun getAccountInformation(address: String): AccountInformation? + + fun getCachedAccountInformationCountFlow(): Flow + + suspend fun getAllAccountInformation(): Map + + suspend fun fetchAndCacheAccountInformation(addresses: List): Map + + suspend fun getEarliestLastFetchedRound(): Long + + suspend fun clearCache() + + suspend fun deleteAccountInformation(address: String) + + suspend fun getAllAssetHoldingIds(addresses: List): List + + fun getAllAccountInformationFlow(): Flow> + + fun getAccountInformationFlow(address: String): Flow +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/AccountInformationUseCases.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/AccountInformationUseCases.kt new file mode 100644 index 00000000..18629df8 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/AccountInformationUseCases.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.domain.usecase + +import com.algorand.common.account.info.domain.model.AccountCacheStatus +import com.algorand.common.account.info.domain.model.AccountInformation +import kotlinx.coroutines.flow.Flow + +fun interface ClearAccountInformationCache { + suspend operator fun invoke() +} + +fun interface FetchAndCacheAccountInformation { + suspend operator fun invoke(addresses: List): Map +} + +fun interface GetAllAccountInformation { + suspend operator fun invoke(): Map +} + +fun interface GetAllAssetHoldingIds { + suspend operator fun invoke(accountAddresses: List): List +} + +fun interface GetCachedAccountInformationCountFlow { + operator fun invoke(): Flow +} + +fun interface GetEarliestLastFetchedRound { + suspend operator fun invoke(): Long +} + +fun interface GetAccountDetailCacheStatusFlow { + operator fun invoke(): Flow +} + +fun interface GetAllAccountInformationFlow { + operator fun invoke(): Flow> +} + +fun interface GetAccountInformation { + suspend operator fun invoke(address: String): AccountInformation? +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/GetAccountDetailCacheStatusFlowUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/GetAccountDetailCacheStatusFlowUseCase.kt new file mode 100644 index 00000000..bcd897b9 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/info/domain/usecase/GetAccountDetailCacheStatusFlowUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.account.info.domain.usecase + +import com.algorand.common.account.info.domain.model.AccountCacheStatus +import com.algorand.common.account.local.domain.usecase.GetLocalAccountCountFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged + +internal class GetAccountDetailCacheStatusFlowUseCase( + private val getLocalAccountCountFlow: GetLocalAccountCountFlow, + private val getCachedAccountInformationCountFlow: GetCachedAccountInformationCountFlow +) : GetAccountDetailCacheStatusFlow { + + override fun invoke(): Flow { + return combine( + getLocalAccountCountFlow().distinctUntilChanged(), + getCachedAccountInformationCountFlow().distinctUntilChanged() + ) { localAccountCount, cachedAccountInformationCount -> + if (cachedAccountInformationCount < localAccountCount) { + AccountCacheStatus.LOADING + } else { + AccountCacheStatus.INITIALIZED + } + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Algo25Dao.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Algo25Dao.kt index f661244a..d9539ea8 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Algo25Dao.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Algo25Dao.kt @@ -19,10 +19,10 @@ import kotlinx.coroutines.flow.Flow @Dao internal interface Algo25Dao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entity: Algo25Entity) - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(entities: List) @Query("SELECT * FROM algo_25") diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Bip39Dao.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Bip39Dao.kt index 20d252f2..50598cc6 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Bip39Dao.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/Bip39Dao.kt @@ -14,6 +14,7 @@ package com.algorand.common.account.local.data.database.dao import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import com.algorand.common.account.local.data.database.model.Bip39Entity import kotlinx.coroutines.flow.Flow @@ -21,10 +22,10 @@ import kotlinx.coroutines.flow.Flow @Dao internal interface Bip39Dao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entity: Bip39Entity) - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(entities: List) @Query("SELECT * FROM bip_39") diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/LedgerBleDao.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/LedgerBleDao.kt index ec7a299d..73405834 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/LedgerBleDao.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/LedgerBleDao.kt @@ -19,10 +19,10 @@ import kotlinx.coroutines.flow.Flow @Dao internal interface LedgerBleDao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entity: LedgerBleEntity) - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(entities: List) @Query("SELECT * FROM ledger_ble") diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/NoAuthDao.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/NoAuthDao.kt index ae3ac682..f47ef1cc 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/NoAuthDao.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/dao/NoAuthDao.kt @@ -19,10 +19,10 @@ import kotlinx.coroutines.flow.Flow @Dao internal interface NoAuthDao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entity: NoAuthEntity) - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(entities: List) @Query("SELECT * FROM no_auth") diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/model/LedgerBleEntity.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/model/LedgerBleEntity.kt index 662c4df5..4cd61a81 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/model/LedgerBleEntity.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/database/model/LedgerBleEntity.kt @@ -24,5 +24,8 @@ internal data class LedgerBleEntity( val deviceMacAddress: String, @ColumnInfo("account_index_in_ledger") - val accountIndexInLedger: Int + val accountIndexInLedger: Int, + + @ColumnInfo("bluetooth_name") + val bluetoothName: String? ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImpl.kt index 5fca720b..2ee0f55c 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImpl.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/entity/LedgerBleEntityMapperImpl.kt @@ -24,7 +24,8 @@ internal class LedgerBleEntityMapperImpl( return LedgerBleEntity( encryptedAddress = addressEncryptionManager.encrypt(localAccount.address), deviceMacAddress = localAccount.deviceMacAddress, - accountIndexInLedger = localAccount.indexInLedger + accountIndexInLedger = localAccount.indexInLedger, + bluetoothName = localAccount.bluetoothName ) } } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImpl.kt index 49c8f233..ce442603 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImpl.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/data/mapper/model/LedgerBleMapperImpl.kt @@ -24,7 +24,8 @@ internal class LedgerBleMapperImpl( return LocalAccount.LedgerBle( address = addressEncryptionManager.decrypt(entity.encryptedAddress), deviceMacAddress = entity.deviceMacAddress, - indexInLedger = entity.accountIndexInLedger + indexInLedger = entity.accountIndexInLedger, + bluetoothName = entity.bluetoothName ) } } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/di/LocalAccountsKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/di/LocalAccountsKoinModule.kt index 3aa8cf63..55a9d96c 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/di/LocalAccountsKoinModule.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/di/LocalAccountsKoinModule.kt @@ -49,6 +49,12 @@ import com.algorand.common.account.local.domain.usecase.DeleteLocalAccount import com.algorand.common.account.local.domain.usecase.DeleteLocalAccountUseCase import com.algorand.common.account.local.domain.usecase.GetAllLocalAccountAddressesAsFlow import com.algorand.common.account.local.domain.usecase.GetAllLocalAccountAddressesAsFlowUseCase +import com.algorand.common.account.local.domain.usecase.GetLocalAccountCountFlow +import com.algorand.common.account.local.domain.usecase.GetLocalAccountCountFlowUseCase +import com.algorand.common.account.local.domain.usecase.GetLocalAccounts +import com.algorand.common.account.local.domain.usecase.GetLocalAccountsUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import org.koin.dsl.module internal val localAccountsKoinModule = module { @@ -115,4 +121,11 @@ internal val localAccountsKoinModule = module { factory { GetAllLocalAccountAddressesAsFlowUseCase(get(), get(), get(), get()) } factory { DeleteLocalAccountUseCase(get(), get(), get(), get()) } + + factory { + GetLocalAccountsUseCase(get(), get(), get(), get(), Dispatchers.IO) + } + factory { + GetLocalAccountCountFlowUseCase(get(), get(), get(), get()) + } } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/domain/model/LocalAccount.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/domain/model/LocalAccount.kt index 7e0e065d..62317e6a 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/domain/model/LocalAccount.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/account/local/domain/model/LocalAccount.kt @@ -67,6 +67,7 @@ sealed interface LocalAccount { data class LedgerBle( override val address: String, val deviceMacAddress: String, + val bluetoothName: String?, val indexInLedger: Int ) : LocalAccount diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetCreatorResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetCreatorResponse.kt index 2fc6811f..041e0d3d 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetCreatorResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetCreatorResponse.kt @@ -17,7 +17,7 @@ import kotlinx.serialization.Serializable @Serializable internal data class AssetCreatorResponse( - @SerialName("id") val id: Long?, - @SerialName("address") val publicKey: String?, - @SerialName("is_verified_asset_creator") val isVerifiedAssetCreator: Boolean? + @SerialName("id") val id: Long? = null, + @SerialName("address") val publicKey: String? = null, + @SerialName("is_verified_asset_creator") val isVerifiedAssetCreator: Boolean? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetResponse.kt index cbb7d31f..5f36979e 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/AssetResponse.kt @@ -18,26 +18,26 @@ import kotlinx.serialization.Serializable @Serializable internal data class AssetResponse( - @SerialName("asset_id") val assetId: Long?, - @SerialName("name") val fullName: String?, - @SerialName("logo") val logoUri: String?, - @SerialName("unit_name") val shortName: String?, - @SerialName("fraction_decimals") val fractionDecimals: Int?, - @SerialName("usd_value") val usdValue: String?, - @SerialName("creator") val assetCreator: AssetCreatorResponse?, - @SerialName("collectible") val collectible: CollectibleResponse?, - @SerialName("total") val maxSupply: String?, - @SerialName("explorer_url") val explorerUrl: String?, - @SerialName("verification_tier") val verificationTier: VerificationTierResponse?, - @SerialName("project_url") val projectUrl: String?, - @SerialName("project_name") val projectName: String?, - @SerialName("logo_svg") val logoSvgUri: String?, - @SerialName("discord_url") val discordUrl: String?, - @SerialName("telegram_url") val telegramUrl: String?, - @SerialName("twitter_username") val twitterUsername: String?, - @SerialName("description") val description: String?, - @SerialName("url") val url: String?, - @SerialName("total_supply") val totalSupply: String?, - @SerialName("last_24_hours_algo_price_change_percentage") val last24HoursAlgoPriceChangePercentage: String?, - @SerialName("available_on_discover_mobile") val isAvailableOnDiscoverMobile: Boolean? + @SerialName("asset_id") val assetId: Long? = null, + @SerialName("name") val fullName: String? = null, + @SerialName("logo") val logoUri: String? = null, + @SerialName("unit_name") val shortName: String? = null, + @SerialName("fraction_decimals") val fractionDecimals: Int? = null, + @SerialName("usd_value") val usdValue: String? = null, + @SerialName("creator") val assetCreator: AssetCreatorResponse? = null, + @SerialName("collectible") val collectible: CollectibleResponse? = null, + @SerialName("total") val maxSupply: String? = null, + @SerialName("explorer_url") val explorerUrl: String? = null, + @SerialName("verification_tier") val verificationTier: VerificationTierResponse? = null, + @SerialName("project_url") val projectUrl: String? = null, + @SerialName("project_name") val projectName: String? = null, + @SerialName("logo_svg") val logoSvgUri: String? = null, + @SerialName("discord_url") val discordUrl: String? = null, + @SerialName("telegram_url") val telegramUrl: String? = null, + @SerialName("twitter_username") val twitterUsername: String? = null, + @SerialName("description") val description: String? = null, + @SerialName("url") val url: String? = null, + @SerialName("total_supply") val totalSupply: String? = null, + @SerialName("last_24_hours_algo_price_change_percentage") val last24HoursAlgoPriceChangePercentage: String? = null, + @SerialName("available_on_discover_mobile") val isAvailableOnDiscoverMobile: Boolean? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailParamsResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailParamsResponse.kt index 8822c7c0..cd117479 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailParamsResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailParamsResponse.kt @@ -17,8 +17,8 @@ import kotlinx.serialization.Serializable @Serializable internal data class NodeAssetDetailParamsResponse( - @SerialName("decimal") val fractionalDecimal: Int?, - @SerialName("name") val fullName: String?, - @SerialName("unit-name") val shortName: String?, - @SerialName("total") val totalSupply: String? + @SerialName("decimal") val fractionalDecimal: Int? = null, + @SerialName("name") val fullName: String? = null, + @SerialName("unit-name") val shortName: String? = null, + @SerialName("total") val totalSupply: String? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailResponse.kt index e45add35..3b6ebdcb 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/NodeAssetDetailResponse.kt @@ -17,6 +17,6 @@ import kotlinx.serialization.Serializable @Serializable internal data class NodeAssetDetailResponse( - @SerialName("index") val id: Long?, - @SerialName("params") val nodeAssetDetailParamsResponse: NodeAssetDetailParamsResponse? + @SerialName("index") val id: Long? = null, + @SerialName("params") val nodeAssetDetailParamsResponse: NodeAssetDetailParamsResponse? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleMediaResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleMediaResponse.kt index 27f41728..f43e85a2 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleMediaResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleMediaResponse.kt @@ -17,8 +17,8 @@ import kotlinx.serialization.Serializable @Serializable internal data class CollectibleMediaResponse( - @SerialName("type") val mediaType: CollectibleMediaTypeResponse?, - @SerialName("download_url") val downloadUrl: String?, - @SerialName("preview_url") val previewUrl: String?, - @SerialName("extension") val mediaTypeExtension: CollectibleMediaTypeExtensionResponse? + @SerialName("type") val mediaType: CollectibleMediaTypeResponse? = null, + @SerialName("download_url") val downloadUrl: String? = null, + @SerialName("preview_url") val previewUrl: String? = null, + @SerialName("extension") val mediaTypeExtension: CollectibleMediaTypeExtensionResponse? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleResponse.kt index 8fb8cfbf..4aaf4d79 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleResponse.kt @@ -17,12 +17,12 @@ import kotlinx.serialization.Serializable @Serializable internal data class CollectibleResponse( - @SerialName("standard") val standard: CollectibleStandardTypeResponse?, - @SerialName("media_type") val mediaType: CollectibleMediaTypeResponse?, - @SerialName("primary_image") val primaryImageUrl: String?, - @SerialName("title") val title: String?, - @SerialName("collection") val collection: CollectionResponse?, - @SerialName("media") val collectibleMedias: List?, - @SerialName("description") val description: String?, - @SerialName("traits") val traits: List? + @SerialName("standard") val standard: CollectibleStandardTypeResponse? = null, + @SerialName("media_type") val mediaType: CollectibleMediaTypeResponse? = null, + @SerialName("primary_image") val primaryImageUrl: String? = null, + @SerialName("title") val title: String? = null, + @SerialName("collection") val collection: CollectionResponse? = null, + @SerialName("media") val collectibleMedias: List? = null, + @SerialName("description") val description: String? = null, + @SerialName("traits") val traits: List? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleSearchResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleSearchResponse.kt index a28a0aab..8fab3b95 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleSearchResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleSearchResponse.kt @@ -17,6 +17,6 @@ import kotlinx.serialization.Serializable @Serializable internal data class CollectibleSearchResponse( - @SerialName("primary_image") val primaryImageUrl: String?, - @SerialName("title") val title: String? + @SerialName("primary_image") val primaryImageUrl: String? = null, + @SerialName("title") val title: String? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleTraitResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleTraitResponse.kt index e7966749..12603d3d 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleTraitResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectibleTraitResponse.kt @@ -17,6 +17,6 @@ import kotlinx.serialization.Serializable @Serializable internal data class CollectibleTraitResponse( - @SerialName("display_name") val name: String?, - @SerialName("display_value") val value: String? + @SerialName("display_name") val name: String? = null, + @SerialName("display_value") val value: String? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectionResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectionResponse.kt index 61db2ade..d960fecb 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectionResponse.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/data/model/collectible/CollectionResponse.kt @@ -17,7 +17,7 @@ import kotlinx.serialization.Serializable @Serializable internal data class CollectionResponse( - @SerialName("id") val collectionId: Long?, - @SerialName("name") val collectionName: String?, - @SerialName("description") val collectionDescription: String? + @SerialName("id") val collectionId: Long? = null, + @SerialName("name") val collectionName: String? = null, + @SerialName("description") val collectionDescription: String? = null ) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/di/AssetDetailKoinModules.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/di/AssetDetailKoinModules.kt index ba1ff7fe..b9aa2124 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/di/AssetDetailKoinModules.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/di/AssetDetailKoinModules.kt @@ -12,7 +12,6 @@ package com.algorand.common.asset.di -import com.algorand.common.account.info.data.service.getIndexerApiHttpClient import com.algorand.common.asset.data.database.di.assetDetailDatabaseModule import com.algorand.common.asset.data.repository.AssetDetailCacheHelper import com.algorand.common.asset.data.repository.AssetDetailCacheHelperImpl @@ -21,13 +20,17 @@ import com.algorand.common.asset.data.service.AssetDetailApiService import com.algorand.common.asset.data.service.AssetDetailApiServiceImpl import com.algorand.common.asset.data.service.AssetDetailNodeApiService import com.algorand.common.asset.data.service.AssetDetailNodeApiServiceImpl +import com.algorand.common.asset.domain.manager.AssetDetailCacheManager +import com.algorand.common.asset.domain.manager.AssetDetailCacheManagerImpl import com.algorand.common.asset.domain.repository.AssetRepository -import com.algorand.common.foundation.network.getAlgodHttpClient +import com.algorand.common.asset.domain.usecase.GetAssetDetailCacheStatusFlow +import com.algorand.common.foundation.network.algod.getAlgodHttpClient +import com.algorand.common.foundation.network.pera.getPeraMobileHttpClient import org.koin.dsl.module private val assetDetailKoinModule = module { single { - AssetDetailApiServiceImpl(getIndexerApiHttpClient(get())) + AssetDetailApiServiceImpl(getPeraMobileHttpClient(get())) } single { @@ -41,6 +44,13 @@ private val assetDetailKoinModule = module { single { AssetRepositoryImpl(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + + single { AssetDetailCacheManagerImpl(get(), get(), get(), get(), get(), get()) } + factory { + GetAssetDetailCacheStatusFlow { + get().cacheStatusFlow + } + } } internal val assetDetailKoinModules = listOf( diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManager.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManager.kt new file mode 100644 index 00000000..358777f0 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManager.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.asset.domain.manager + +import androidx.lifecycle.Lifecycle +import com.algorand.common.asset.domain.model.AssetCacheStatus +import kotlinx.coroutines.flow.StateFlow + +interface AssetDetailCacheManager { + val cacheStatusFlow: StateFlow + + fun initialize(lifecycle: Lifecycle) +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManagerImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManagerImpl.kt new file mode 100644 index 00000000..87d20545 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/manager/AssetDetailCacheManagerImpl.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.asset.domain.manager + +import androidx.lifecycle.Lifecycle +import com.algorand.common.account.info.domain.model.AccountCacheStatus.INITIALIZED +import com.algorand.common.account.info.domain.usecase.GetAccountDetailCacheStatusFlow +import com.algorand.common.account.info.domain.usecase.GetAllAssetHoldingIds +import com.algorand.common.account.local.domain.usecase.GetLocalAccountCountFlow +import com.algorand.common.account.local.domain.usecase.GetLocalAccounts +import com.algorand.common.asset.domain.model.AssetCacheStatus +import com.algorand.common.asset.domain.model.AssetCacheStatus.EMPTY +import com.algorand.common.asset.domain.usecase.FetchAndCacheAssets +import com.algorand.common.cache.LifecycleAwareCacheManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn + +internal class AssetDetailCacheManagerImpl( + private val cacheManager: LifecycleAwareCacheManager, + private val getAllAssetHoldingIds: GetAllAssetHoldingIds, + private val getAccountDetailCacheStatusFlow: GetAccountDetailCacheStatusFlow, + private val getLocalAccountCountFlow: GetLocalAccountCountFlow, + private val fetchAndCacheAssets: FetchAndCacheAssets, + private val getLocalAccounts: GetLocalAccounts +) : AssetDetailCacheManager { + + private val _cacheStatusFlow = MutableStateFlow(AssetCacheStatus.IDLE) + override val cacheStatusFlow = _cacheStatusFlow.asStateFlow() + + private val cacheManagerListener = object : LifecycleAwareCacheManager.CacheManagerListener { + override suspend fun onInitializeManager(coroutineScope: CoroutineScope) { + initialize(coroutineScope) + } + + override suspend fun onStartJob(coroutineScope: CoroutineScope) { + runManagerJob() + } + } + + private fun initialize(coroutineScope: CoroutineScope) { + combine( + getAccountDetailCacheStatusFlow().distinctUntilChanged(), + getLocalAccountCountFlow().distinctUntilChanged() + ) { accountDetailCacheStatus, localAccountCount -> + when { + accountDetailCacheStatus == INITIALIZED && localAccountCount == 0 -> updateCacheStatus(EMPTY) + accountDetailCacheStatus == INITIALIZED && localAccountCount > 0 -> cacheManager.startJob() + else -> cacheManager.stopCurrentJob() + } + }.launchIn(coroutineScope) + } + + override fun initialize(lifecycle: Lifecycle) { + cacheManager.setListener(cacheManagerListener) + lifecycle.addObserver(cacheManager) + } + + private suspend fun runManagerJob() { + updateCacheStatus(AssetCacheStatus.LOADING) + val localAccountAddresses = getLocalAccounts().map { it.address } + val assetIds = getAllAssetHoldingIds(localAccountAddresses) + if (assetIds.isEmpty()) { + updateCacheStatus(EMPTY) + return + } + fetchAndCacheAssets(assetIds, includeDeleted = false) + updateCacheStatus(AssetCacheStatus.INITIALIZED) + } + + private fun updateCacheStatus(newStatus: AssetCacheStatus) { + if (newStatus.ordinal > _cacheStatusFlow.value.ordinal) { + _cacheStatusFlow.value = newStatus + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/model/AssetCacheStatus.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/model/AssetCacheStatus.kt new file mode 100644 index 00000000..5f117160 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/model/AssetCacheStatus.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.asset.domain.model + +enum class AssetCacheStatus { + IDLE, + LOADING, + EMPTY, + INITIALIZED; + + infix fun isAtLeast(status: AssetCacheStatus): Boolean { + return ordinal >= status.ordinal + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/usecase/GetAssetDetailCacheStatusFlow.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/usecase/GetAssetDetailCacheStatusFlow.kt new file mode 100644 index 00000000..e3bae1dc --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/asset/domain/usecase/GetAssetDetailCacheStatusFlow.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.asset.domain.usecase + +import com.algorand.common.asset.domain.model.AssetCacheStatus +import kotlinx.coroutines.flow.Flow + +fun interface GetAssetDetailCacheStatusFlow { + operator fun invoke(): Flow +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshRequestBody.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshRequestBody.kt new file mode 100644 index 00000000..7fc5765f --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshRequestBody.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ShouldRefreshRequestBody( + @SerialName("account_addresses") val accountAddresses: List, + @SerialName("last_known_round") val lastKnownRound: Long? +) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshResponse.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshResponse.kt new file mode 100644 index 00000000..8fa3b0b6 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/model/ShouldRefreshResponse.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ShouldRefreshResponse( + @SerialName("refresh") val shouldRefresh: Boolean? +) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/repository/BlockPollingRepositoryImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/repository/BlockPollingRepositoryImpl.kt new file mode 100644 index 00000000..dfb3fcad --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/repository/BlockPollingRepositoryImpl.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.data.repository + +import com.algorand.common.block.data.model.ShouldRefreshRequestBody +import com.algorand.common.block.data.service.BlockPollingApiService +import com.algorand.common.block.domain.repository.BlockPollingRepository +import com.algorand.common.foundation.PeraResult +import com.algorand.common.foundation.cache.CacheResult +import com.algorand.common.foundation.cache.SingleInMemoryLocalCache +import com.algorand.common.utils.date.TimeProvider + +internal class BlockPollingRepositoryImpl( + private val blockPollingApiService: BlockPollingApiService, + private val blockPollingLocalCache: SingleInMemoryLocalCache, + private val timeProvider: TimeProvider +) : BlockPollingRepository { + + override suspend fun clearLastKnownBlockNumber() { + blockPollingLocalCache.clear() + } + + override suspend fun updateLastKnownBlockNumber(blockNumber: Long) { + blockPollingLocalCache.put(CacheResult.Success(blockNumber, timeProvider.getCurrentTimeMillis())) + } + + override suspend fun getLastKnownAccountBlockNumber(): Long? { + return blockPollingLocalCache.getOrNull()?.getDataOrNull() + } + + override suspend fun shouldUpdateAccountCache(localAccountAddresses: List): PeraResult { + val body = ShouldRefreshRequestBody(localAccountAddresses, getLastKnownAccountBlockNumber()) + return blockPollingApiService.shouldRefresh(body).map { + it.shouldRefresh ?: false + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiService.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiService.kt new file mode 100644 index 00000000..e9dab64b --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiService.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.data.service + +import com.algorand.common.block.data.model.ShouldRefreshRequestBody +import com.algorand.common.block.data.model.ShouldRefreshResponse +import com.algorand.common.foundation.PeraResult + +internal interface BlockPollingApiService { + suspend fun shouldRefresh(body: ShouldRefreshRequestBody): PeraResult +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiServiceImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiServiceImpl.kt new file mode 100644 index 00000000..221bcf64 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/data/service/BlockPollingApiServiceImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.data.service + +import com.algorand.common.block.data.model.ShouldRefreshRequestBody +import com.algorand.common.block.data.model.ShouldRefreshResponse +import com.algorand.common.foundation.PeraResult +import com.algorand.common.foundation.network.safeRequest +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.setBody + +internal class BlockPollingApiServiceImpl( + private val httpClient: HttpClient +) : BlockPollingApiService { + + override suspend fun shouldRefresh(body: ShouldRefreshRequestBody): PeraResult { + return safeRequest { + httpClient.post("v1/algorand-indexer/should-refresh/") { + setBody(body) + } + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/di/BlockPollingKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/di/BlockPollingKoinModule.kt new file mode 100644 index 00000000..dc8bd0f9 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/di/BlockPollingKoinModule.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.di + +import com.algorand.common.block.data.repository.BlockPollingRepositoryImpl +import com.algorand.common.block.data.service.BlockPollingApiService +import com.algorand.common.block.data.service.BlockPollingApiServiceImpl +import com.algorand.common.block.domain.repository.BlockPollingRepository +import com.algorand.common.block.domain.usecase.ClearLastKnownBlockNumber +import com.algorand.common.block.domain.usecase.GetLastKnownBlockNumber +import com.algorand.common.block.domain.usecase.ShouldUpdateAccountCache +import com.algorand.common.block.domain.usecase.ShouldUpdateAccountCacheUseCase +import com.algorand.common.block.domain.usecase.UpdateLastKnownBlockNumber +import com.algorand.common.block.domain.usecase.UpdateLastKnownBlockNumberUseCase +import com.algorand.common.foundation.cache.SingleInMemoryLocalCache +import com.algorand.common.foundation.network.pera.getPeraMobileHttpClient +import org.koin.dsl.module + +internal val blockPollingKoinModule = module { + single { BlockPollingApiServiceImpl(getPeraMobileHttpClient(get())) } + single { BlockPollingRepositoryImpl(get(), SingleInMemoryLocalCache(), get()) } + + factory { + ClearLastKnownBlockNumber { + get().clearLastKnownBlockNumber() + } + } + + factory { + GetLastKnownBlockNumber { + get().getLastKnownAccountBlockNumber() + } + } + + factory { + ShouldUpdateAccountCacheUseCase(get(), get(), get()) + } + + factory { + UpdateLastKnownBlockNumberUseCase(get(), get()) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/repository/BlockPollingRepository.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/repository/BlockPollingRepository.kt new file mode 100644 index 00000000..fa2a782c --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/repository/BlockPollingRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.domain.repository + +import com.algorand.common.foundation.PeraResult + +internal interface BlockPollingRepository { + + suspend fun clearLastKnownBlockNumber() + + suspend fun updateLastKnownBlockNumber(blockNumber: Long) + + suspend fun getLastKnownAccountBlockNumber(): Long? + + suspend fun shouldUpdateAccountCache(localAccountAddresses: List): PeraResult +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/BlockPollingUseCases.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/BlockPollingUseCases.kt new file mode 100644 index 00000000..f7087547 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/BlockPollingUseCases.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.domain.usecase + +import com.algorand.common.foundation.PeraResult + +fun interface ClearLastKnownBlockNumber { + suspend operator fun invoke() +} + +fun interface GetLastKnownBlockNumber { + suspend operator fun invoke() +} + +fun interface UpdateLastKnownBlockNumber { + suspend operator fun invoke() +} + +fun interface ShouldUpdateAccountCache { + suspend operator fun invoke(): PeraResult +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/ShouldUpdateAccountCacheUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/ShouldUpdateAccountCacheUseCase.kt new file mode 100644 index 00000000..2740bb50 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/ShouldUpdateAccountCacheUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.domain.usecase + +import com.algorand.common.account.info.domain.usecase.GetAllAccountInformation +import com.algorand.common.account.local.domain.usecase.GetLocalAccounts +import com.algorand.common.block.domain.repository.BlockPollingRepository +import com.algorand.common.foundation.PeraResult + +internal class ShouldUpdateAccountCacheUseCase( + private val getLocalAccounts: GetLocalAccounts, + private val blockPollingRepository: BlockPollingRepository, + private val getAllAccountInformation: GetAllAccountInformation +) : ShouldUpdateAccountCache { + + override suspend fun invoke(): PeraResult { + val localAccountAddresses = getLocalAccounts().map { it.address } + val cachedAccounts = getAllAccountInformation() + if (localAccountAddresses.size > cachedAccounts.size) { + return PeraResult.Success(true) + } + return blockPollingRepository.shouldUpdateAccountCache(localAccountAddresses) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/UpdateLastKnownBlockNumberUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/UpdateLastKnownBlockNumberUseCase.kt new file mode 100644 index 00000000..5e20c25a --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/block/domain/usecase/UpdateLastKnownBlockNumberUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.block.domain.usecase + +import com.algorand.common.account.info.domain.usecase.GetEarliestLastFetchedRound +import com.algorand.common.block.domain.repository.BlockPollingRepository + +internal class UpdateLastKnownBlockNumberUseCase( + private val getEarliestLastFetchedRound: GetEarliestLastFetchedRound, + private val blockPollingRepository: BlockPollingRepository +) : UpdateLastKnownBlockNumber { + + override suspend fun invoke() { + val earliestFetchedRound = getEarliestLastFetchedRound() + return blockPollingRepository.updateLastKnownBlockNumber(earliestFetchedRound) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManager.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManager.kt new file mode 100644 index 00000000..fca53533 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManager.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache + +import androidx.lifecycle.DefaultLifecycleObserver +import kotlinx.coroutines.CoroutineScope + +internal interface LifecycleAwareCacheManager : DefaultLifecycleObserver { + fun stopCurrentJob() + fun startJob() + fun setListener(listener: CacheManagerListener) + fun launchScope(action: suspend CoroutineScope.() -> Unit) + + interface CacheManagerListener { + suspend fun onInitializeManager(coroutineScope: CoroutineScope) {} + suspend fun onStartJob(coroutineScope: CoroutineScope) {} + fun onClearResources() {} + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManagerImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManagerImpl.kt new file mode 100644 index 00000000..6b472dcf --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/LifecycleAwareCacheManagerImpl.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache + +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch + +internal class LifecycleAwareCacheManagerImpl : LifecycleAwareCacheManager { + + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var currentJob: Job? = null + private var initializationJob: Job? = null + + private var listener: LifecycleAwareCacheManager.CacheManagerListener? = null + + override fun onResume(owner: LifecycleOwner) { + initializationJob = coroutineScope.launch(Dispatchers.IO) { + listener?.onInitializeManager(this) + } + } + + override fun onPause(owner: LifecycleOwner) { + stop() + } + + override fun onDestroy(owner: LifecycleOwner) { + clearResources() + listener?.onClearResources() + } + + override fun launchScope(action: suspend CoroutineScope.() -> Unit) { + coroutineScope.launch(Dispatchers.IO) { + action() + } + } + + override fun startJob() { + currentJob = coroutineScope.launch(Dispatchers.IO) { + listener?.onStartJob(this) + } + } + + override fun setListener(listener: LifecycleAwareCacheManager.CacheManagerListener) { + this.listener = listener + } + + override fun stopCurrentJob() { + if (currentJob?.isActive == true) { + currentJob?.cancel() + } + } + + private fun stop() { + currentJob?.cancel() + currentJob = null + initializationJob?.cancel() + initializationJob = null + coroutineScope.coroutineContext.cancelChildren() + } + + private fun clearResources() { + currentJob = null + initializationJob = null + coroutineScope.coroutineContext.cancelChildren() + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/di/CacheKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/di/CacheKoinModule.kt new file mode 100644 index 00000000..3d77108e --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/di/CacheKoinModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.di + +import com.algorand.common.cache.LifecycleAwareCacheManager +import com.algorand.common.cache.LifecycleAwareCacheManagerImpl +import com.algorand.common.cache.domain.usecase.ClearPreviousSessionCache +import com.algorand.common.cache.domain.usecase.ClearPreviousSessionCacheUseCase +import com.algorand.common.cache.domain.usecase.GetAppCacheStatusFlow +import com.algorand.common.cache.domain.usecase.GetAppCacheStatusFlowUseCase +import com.algorand.common.cache.domain.usecase.InitializeAppCache +import com.algorand.common.cache.domain.usecase.InitializeAppCacheImpl +import com.algorand.common.cache.domain.usecase.UpdateAccountCache +import com.algorand.common.cache.domain.usecase.UpdateAccountCacheUseCase +import org.koin.dsl.module + +internal val cacheKoinModule = module { + factory { LifecycleAwareCacheManagerImpl() } + factory { ClearPreviousSessionCacheUseCase(get(), get()) } + single { InitializeAppCacheImpl(get(), get(), get()) } + single { GetAppCacheStatusFlowUseCase(get(), get()) } + factory { UpdateAccountCacheUseCase(get(), get(), get()) } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/model/AppCacheStatus.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/model/AppCacheStatus.kt new file mode 100644 index 00000000..d03c68dd --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/model/AppCacheStatus.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.domain.model + +enum class AppCacheStatus { + IDLE, + LOADING, + INITIALIZED, + FAILED +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/CacheUseCases.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/CacheUseCases.kt new file mode 100644 index 00000000..3f24997b --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/CacheUseCases.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.domain.usecase + +import androidx.lifecycle.Lifecycle +import com.algorand.common.cache.domain.model.AppCacheStatus +import kotlinx.coroutines.flow.Flow + +fun interface InitializeAppCache { + suspend operator fun invoke(lifecycle: Lifecycle) +} + +internal fun interface ClearPreviousSessionCache { + suspend operator fun invoke() +} + +fun interface GetAppCacheStatusFlow { + operator fun invoke(): Flow +} + +fun interface UpdateAccountCache { + suspend operator fun invoke() +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt new file mode 100644 index 00000000..fb187f4b --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.domain.usecase + +import com.algorand.common.account.info.domain.usecase.ClearAccountInformationCache +import com.algorand.common.asset.domain.usecase.ClearAssetCache + +internal class ClearPreviousSessionCacheUseCase( + private val clearAccountInformationCache: ClearAccountInformationCache, + private val clearAssetCache: ClearAssetCache +) : ClearPreviousSessionCache { + + override suspend fun invoke() { + clearAccountInformationCache() + clearAssetCache() + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/GetAppCacheStatusFlowUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/GetAppCacheStatusFlowUseCase.kt new file mode 100644 index 00000000..8a630e3b --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/GetAppCacheStatusFlowUseCase.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.domain.usecase + +import com.algorand.common.account.info.domain.model.AccountCacheStatus +import com.algorand.common.account.info.domain.usecase.GetAccountDetailCacheStatusFlow +import com.algorand.common.asset.domain.model.AssetCacheStatus +import com.algorand.common.asset.domain.usecase.GetAssetDetailCacheStatusFlow +import com.algorand.common.cache.domain.model.AppCacheStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +internal class GetAppCacheStatusFlowUseCase( + private val getAccountDetailCacheStatusFlow: GetAccountDetailCacheStatusFlow, + private val getAssetDetailCacheStatusFlow: GetAssetDetailCacheStatusFlow +) : GetAppCacheStatusFlow { + + override fun invoke(): Flow { + return combine( + getAccountDetailCacheStatusFlow(), + getAssetDetailCacheStatusFlow() + ) { accountCacheStatus, assetCacheStatus -> + when { + !isInitializationStarted(accountCacheStatus) -> AppCacheStatus.IDLE + isCacheInitialized(accountCacheStatus, assetCacheStatus) -> AppCacheStatus.INITIALIZED + else -> AppCacheStatus.LOADING + } + } + } + + private fun isCacheInitialized(accountStatus: AccountCacheStatus, assetStatus: AssetCacheStatus): Boolean { + return accountStatus == AccountCacheStatus.INITIALIZED && assetStatus isAtLeast AssetCacheStatus.EMPTY + } + + private fun isInitializationStarted(accountStatus: AccountCacheStatus): Boolean { + return accountStatus != AccountCacheStatus.IDLE + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/InitializeAppCacheImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/InitializeAppCacheImpl.kt new file mode 100644 index 00000000..f914268f --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/InitializeAppCacheImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.domain.usecase + +import androidx.lifecycle.Lifecycle +import com.algorand.common.account.info.domain.manager.AccountCacheManager +import com.algorand.common.asset.domain.manager.AssetDetailCacheManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal class InitializeAppCacheImpl( + private val accountCacheManager: AccountCacheManager, + private val assetDetailCacheManager: AssetDetailCacheManager, + private val clearPreviousSessionCache: ClearPreviousSessionCache +) : InitializeAppCache { + + override suspend fun invoke(lifecycle: Lifecycle) { + clearPreviousSessionCache() + withContext(Dispatchers.Main) { + accountCacheManager.initialize(lifecycle) + assetDetailCacheManager.initialize(lifecycle) + } + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/UpdateAccountCacheUseCase.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/UpdateAccountCacheUseCase.kt new file mode 100644 index 00000000..c95d58ff --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/cache/domain/usecase/UpdateAccountCacheUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.cache.domain.usecase + +import com.algorand.common.account.info.domain.usecase.FetchAndCacheAccountInformation +import com.algorand.common.account.local.domain.usecase.GetLocalAccounts +import com.algorand.common.asset.domain.usecase.FetchAndCacheAssets + +internal class UpdateAccountCacheUseCase( + private val getLocalAccounts: GetLocalAccounts, + private val fetchAndCacheAccountInformation: FetchAndCacheAccountInformation, + private val fetchAndCacheAssets: FetchAndCacheAssets +) : UpdateAccountCache { + + override suspend fun invoke() { + val localAccountAddresses = getLocalAccounts().map { it.address } + val assetIds = fetchAndCacheAccountInformation(localAccountAddresses).mapNotNull { + if (it.value == null) return@mapNotNull null + it.value?.assetHoldings?.map { assetHolding -> + assetHolding.assetId + } + }.flatten() + fetchAndCacheAssets(assetIds, false) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/CommonModuleKoinModules.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/CommonModuleKoinModules.kt index f9d9610d..e824229d 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/CommonModuleKoinModules.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/CommonModuleKoinModules.kt @@ -12,15 +12,23 @@ package com.algorand.common.di +import com.algorand.common.account.detail.di.accountDetailKoinModule import com.algorand.common.account.info.di.accountInformationKoinModule import com.algorand.common.account.local.di.localAccountsKoinModule import com.algorand.common.asset.di.assetDetailKoinModules +import com.algorand.common.block.di.blockPollingKoinModule +import com.algorand.common.cache.di.cacheKoinModule import com.algorand.common.encryption.di.encryptionModule +import com.algorand.common.utils.date.dateKoinModule val commonModuleKoinModules = listOf( localAccountsKoinModule, encryptionModule, platformKoinModule(), accountInformationKoinModule, - networkKoinModule + networkKoinModule, + dateKoinModule, + blockPollingKoinModule, + cacheKoinModule, + accountDetailKoinModule ) + assetDetailKoinModules diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/NetworkKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/NetworkKoinModule.kt index b27f4167..95bfd08b 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/NetworkKoinModule.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/di/NetworkKoinModule.kt @@ -12,8 +12,9 @@ package com.algorand.common.di -import com.algorand.common.foundation.network.AlgodInterceptorPlugin -import com.algorand.common.foundation.network.IndexerInterceptorPlugin +import com.algorand.common.foundation.network.algod.AlgodInterceptorPlugin +import com.algorand.common.foundation.network.indexer.IndexerInterceptorPlugin +import com.algorand.common.foundation.network.pera.PeraMobileInterceptorPlugin import org.koin.dsl.module internal val networkKoinModule = module { @@ -23,4 +24,7 @@ internal val networkKoinModule = module { single { AlgodInterceptorPlugin(get()) } + single { + PeraMobileInterceptorPlugin(get()) + } } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/CacheResult.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/CacheResult.kt new file mode 100644 index 00000000..0306edf5 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/CacheResult.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.cache + +sealed class CacheResult { + + data class Success( + val data: T, + val creationTimestamp: Long + ) : CacheResult() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other != null && this::class != other::class) return false + + other as Success<*> + + if (data != other.data) return false + return creationTimestamp == other.creationTimestamp + } + + override fun hashCode(): Int { + return data.hashCode() + creationTimestamp.hashCode() + } + } + + class Error private constructor( + val exception: Throwable, + val previouslyCachedData: T? = null, + val previouslyCachedDataCreationTimestamp: Long? = null, + val code: Int? = null + ) : CacheResult() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other != null && this::class != other::class) return false + + other as Error<*> + + if (exception == other.exception) return false + if (previouslyCachedData != other.previouslyCachedData) return false + return previouslyCachedDataCreationTimestamp == other.previouslyCachedDataCreationTimestamp + } + + override fun hashCode(): Int { + return exception.hashCode() + previouslyCachedData.hashCode() + previouslyCachedData.hashCode() + } + + companion object { + fun create( + exception: Throwable, + previousData: Success? = null, + code: Int? = null + ): Error { + return Error( + exception, + previousData?.data, + previousData?.creationTimestamp, + code = code + ) + } + } + } + + fun getDataOrNull(): T? = (this as? Success)?.data +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/SingleInMemoryLocalCache.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/SingleInMemoryLocalCache.kt new file mode 100644 index 00000000..9c875214 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/cache/SingleInMemoryLocalCache.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.cache + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class SingleInMemoryLocalCache { + + val cacheFlow: StateFlow?> + get() = _cacheFlow + private val _cacheFlow = MutableStateFlow?>(null) + + fun getOrNull(): CacheResult? = cacheFlow.value + + fun remove(): CacheResult? { + return _cacheFlow.value.also { + clear() + } + } + + fun clear() { + _cacheFlow.value = null + } + + fun put(value: CacheResult) { + _cacheFlow.value = value + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientExtensions.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientExtensions.kt index 64ac812f..84b91865 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientExtensions.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientExtensions.kt @@ -13,8 +13,14 @@ package com.algorand.common.foundation.network import com.algorand.common.foundation.PeraResult +import io.ktor.client.HttpClientConfig import io.ktor.client.call.body +import io.ktor.client.plugins.DefaultRequest +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json import kotlinx.io.IOException suspend inline fun safeRequest( @@ -23,7 +29,7 @@ suspend inline fun safeRequest( return try { request().toPeraResult() } catch (exception: Exception) { - PeraResult.Error(IOException("API request is failed on the client"), 99) + PeraResult.Error(IOException("API request is failed on the client - $exception"), 99) } } @@ -34,3 +40,12 @@ suspend inline fun HttpResponse.toPeraResult(): PeraResult PeraResult.Error(Exception("Error"), this.status.value) } } + +internal fun HttpClientConfig<*>.installDefaultConfigs() { + install(DefaultRequest) { + contentType(ContentType.Application.Json) + } + install(ContentNegotiation) { + json(PeraJsonNegotiation) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.kt similarity index 87% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.kt index 0203d89a..c6aa6a1b 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.kt @@ -14,4 +14,4 @@ package com.algorand.common.foundation.network import io.ktor.client.HttpClient -internal expect fun getAlgodHttpClient(algodInterceptorPlugin: AlgodInterceptorPlugin): HttpClient +internal expect fun createHttpClient(): HttpClient diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/PeraContentNegotiation.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/PeraContentNegotiation.kt index ae868a82..d0e539e1 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/PeraContentNegotiation.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/PeraContentNegotiation.kt @@ -20,6 +20,8 @@ import kotlinx.serialization.modules.contextual internal val PeraJsonNegotiation = Json { ignoreUnknownKeys = true isLenient = true + encodeDefaults = true + prettyPrint = true this.serializersModule = SerializersModule { contextual(LongAsStringSerializer) } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodHttpClientProvider.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodHttpClientProvider.kt new file mode 100644 index 00000000..8a4c5f96 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodHttpClientProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.network.algod + +import com.algorand.common.foundation.network.createHttpClient +import com.algorand.common.foundation.network.installDefaultConfigs +import io.ktor.client.HttpClient + +internal fun getAlgodHttpClient(algodInterceptorPlugin: AlgodInterceptorPlugin): HttpClient { + return createHttpClient().config { + installDefaultConfigs() + install(algodInterceptorPlugin) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/AlgodInterceptorPlugin.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodInterceptorPlugin.kt similarity index 97% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/AlgodInterceptorPlugin.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodInterceptorPlugin.kt index ac8fac01..1d237c48 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/AlgodInterceptorPlugin.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/AlgodInterceptorPlugin.kt @@ -10,7 +10,7 @@ * limitations under the License */ -package com.algorand.common.foundation.network +package com.algorand.common.foundation.network.algod import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpClientPlugin diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/GetAlgodInterceptorConfig.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/GetAlgodInterceptorConfig.kt similarity index 92% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/GetAlgodInterceptorConfig.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/GetAlgodInterceptorConfig.kt index 2799ed7d..2d256b9c 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/GetAlgodInterceptorConfig.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/algod/GetAlgodInterceptorConfig.kt @@ -10,7 +10,7 @@ * limitations under the License */ -package com.algorand.common.foundation.network +package com.algorand.common.foundation.network.algod fun interface GetAlgodInterceptorConfig { suspend operator fun invoke(): AlgodInterceptorPluginConfig diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/GetIndexerInterceptorConfig.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/GetIndexerInterceptorConfig.kt similarity index 92% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/GetIndexerInterceptorConfig.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/GetIndexerInterceptorConfig.kt index 902afe22..e2da9865 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/GetIndexerInterceptorConfig.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/GetIndexerInterceptorConfig.kt @@ -10,7 +10,7 @@ * limitations under the License */ -package com.algorand.common.foundation.network +package com.algorand.common.foundation.network.indexer fun interface GetIndexerInterceptorConfig { suspend operator fun invoke(): IndexerInterceptorPluginConfig diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerApiServiceProvider.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerApiServiceProvider.kt new file mode 100644 index 00000000..7f3cfbed --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerApiServiceProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.network.indexer + +import com.algorand.common.foundation.network.createHttpClient +import com.algorand.common.foundation.network.installDefaultConfigs +import io.ktor.client.HttpClient + +internal fun getIndexerApiHttpClient(indexerInterceptorPlugin: IndexerInterceptorPlugin): HttpClient { + return createHttpClient().config { + installDefaultConfigs() + install(indexerInterceptorPlugin) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/IndexerInterceptorPlugin.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerInterceptorPlugin.kt similarity index 97% rename from wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/IndexerInterceptorPlugin.kt rename to wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerInterceptorPlugin.kt index abbd8787..1fdd129f 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/IndexerInterceptorPlugin.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/indexer/IndexerInterceptorPlugin.kt @@ -10,7 +10,7 @@ * limitations under the License */ -package com.algorand.common.foundation.network +package com.algorand.common.foundation.network.indexer import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpClientPlugin diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/GetPeraMobileInterceptorConfig.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/GetPeraMobileInterceptorConfig.kt new file mode 100644 index 00000000..9edcace8 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/GetPeraMobileInterceptorConfig.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.network.pera + +fun interface GetPeraMobileInterceptorConfig { + suspend operator fun invoke(): PeraMobileInterceptorPluginConfig +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileHttpClientProvider.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileHttpClientProvider.kt new file mode 100644 index 00000000..8d6926cf --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileHttpClientProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.network.pera + +import com.algorand.common.foundation.network.createHttpClient +import com.algorand.common.foundation.network.installDefaultConfigs +import io.ktor.client.HttpClient + +internal fun getPeraMobileHttpClient(peraMobileInterceptorPlugin: PeraMobileInterceptorPlugin): HttpClient { + return createHttpClient().config { + installDefaultConfigs() + install(peraMobileInterceptorPlugin) + } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileInterceptorPlugin.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileInterceptorPlugin.kt new file mode 100644 index 00000000..f4c5815a --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileInterceptorPlugin.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.network.pera + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpClientPlugin +import io.ktor.client.request.HttpSendPipeline +import io.ktor.client.request.headers +import io.ktor.http.HttpMessageBuilder +import io.ktor.http.URLBuilder +import io.ktor.http.set +import io.ktor.http.takeFrom +import io.ktor.util.AttributeKey +import io.ktor.util.appendAll + +internal class PeraMobileInterceptorPlugin( + private val getPeraMobileInterceptorConfig: GetPeraMobileInterceptorConfig +) : HttpClientPlugin { + + override val key: AttributeKey = AttributeKey("PeraMobileInterceptor") + + override fun prepare(block: Unit.() -> Unit): PeraMobileInterceptorPlugin { + return PeraMobileInterceptorPlugin(getPeraMobileInterceptorConfig) + } + + override fun install(plugin: PeraMobileInterceptorPlugin, scope: HttpClient) { + scope.sendPipeline.intercept(HttpSendPipeline.Before) { + val config = plugin.getPeraMobileInterceptorConfig() + context.url.setNodeAwareUrl(config) + context.setNodeAwareHeaders(config) + context.setUserAgentHeaders(config.userAgent) + proceed() + } + } + + private fun URLBuilder.setNodeAwareUrl(config: PeraMobileInterceptorPluginConfig) { + val newUrl = URLBuilder(config.baseUrl) + newUrl.pathSegments = pathSegments + newUrl.parameters.appendAll(parameters) + set { + takeFrom(newUrl) + } + } + + private fun HttpMessageBuilder.setNodeAwareHeaders(config: PeraMobileInterceptorPluginConfig) { + headers { + append("X-API-Key", config.apiKey) + } + } + + private fun HttpMessageBuilder.setUserAgentHeaders(userAgent: PeraMobileUserAgent) { + headers { + append("App-Name", userAgent.appName) + append("Client-Type", userAgent.clientType) + append("Device-OS-Version", userAgent.osVersion) + append("App-Package-Name", userAgent.packageName) + append("App-Version", userAgent.appVersion) + append("Device-Model", userAgent.deviceModel) + append("Accept-Language", userAgent.languageTag) + } + } +} + +data class PeraMobileInterceptorPluginConfig( + val baseUrl: String, + val apiKey: String, + val userAgent: PeraMobileUserAgent +) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileUserAgent.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileUserAgent.kt new file mode 100644 index 00000000..8f736b46 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/foundation/network/pera/PeraMobileUserAgent.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.foundation.network.pera + +data class PeraMobileUserAgent( + val packageName: String, + val appVersion: String, + val appName: String, + val osVersion: String, + val deviceModel: String, + val languageTag: String, + val clientType: String +) diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/DateKoinModule.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/DateKoinModule.kt new file mode 100644 index 00000000..71778574 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/DateKoinModule.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.utils.date + +import org.koin.dsl.module + +internal val dateKoinModule = module { + single { TimeProviderImpl() } +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProvider.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProvider.kt new file mode 100644 index 00000000..551bd976 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProvider.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.utils.date + +interface TimeProvider { + fun getCurrentTimeMillis(): Long +} diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProviderImpl.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProviderImpl.kt new file mode 100644 index 00000000..4bfa1ab7 --- /dev/null +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/utils/date/TimeProviderImpl.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.common.utils.date + +import kotlinx.datetime.Clock + +internal class TimeProviderImpl : TimeProvider { + + override fun getCurrentTimeMillis(): Long { + return Clock.System.now().toEpochMilliseconds() + } +} diff --git a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.ios.kt b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.ios.kt deleted file mode 100644 index 861bbe73..00000000 --- a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/account/info/data/service/IndexerApiServiceProvider.ios.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2022 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.common.account.info.data.service - -import com.algorand.common.foundation.network.IndexerInterceptorPlugin -import com.algorand.common.foundation.network.PeraJsonNegotiation -import io.ktor.client.HttpClient -import io.ktor.client.engine.darwin.Darwin -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.plugins.logging.SIMPLE -import io.ktor.serialization.kotlinx.json.json - -internal actual fun getIndexerApiHttpClient(indexerInterceptorPlugin: IndexerInterceptorPlugin): HttpClient { - return HttpClient(Darwin) { - install(Logging) { - logger = Logger.SIMPLE - level = LogLevel.BODY - } - install(ContentNegotiation) { - json(PeraJsonNegotiation) - } - install(indexerInterceptorPlugin) - } -} diff --git a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/database/PeraDatabase.kt b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/database/PeraDatabase.kt index 5cdcdcb5..7583848e 100644 --- a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/database/PeraDatabase.kt +++ b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/database/PeraDatabase.kt @@ -14,6 +14,7 @@ package com.algorand.common.foundation.database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.sqlite.driver.bundled.BundledSQLiteDriver import kotlinx.cinterop.ExperimentalForeignApi import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSFileManager @@ -27,7 +28,7 @@ internal fun getPeraDatabaseBuilder(): RoomDatabase.Builder { val dbFilePath = documentDirectory() + "/${PeraDatabase.DATABASE_NAME}.db" return Room.databaseBuilder( name = dbFilePath, - ) + ).setDriver(BundledSQLiteDriver()) } @OptIn(ExperimentalForeignApi::class) diff --git a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.ios.kt b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.ios.kt similarity index 75% rename from wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.ios.kt rename to wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.ios.kt index 068d59a1..b909f8be 100644 --- a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/AlgodHttpClientProvider.ios.kt +++ b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/foundation/network/HttpClientProvider.ios.kt @@ -14,22 +14,16 @@ package com.algorand.common.foundation.network import io.ktor.client.HttpClient import io.ktor.client.engine.darwin.Darwin -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.SIMPLE -import io.ktor.serialization.kotlinx.json.json -internal actual fun getAlgodHttpClient(algodInterceptorPlugin: AlgodInterceptorPlugin): HttpClient { +internal actual fun createHttpClient(): HttpClient { return HttpClient(Darwin) { install(Logging) { logger = Logger.SIMPLE level = LogLevel.BODY } - install(ContentNegotiation) { - json(PeraJsonNegotiation) - } - install(algodInterceptorPlugin) } }