diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt index ffd087f5e4..18319b12c1 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/RootRoute.kt @@ -8,7 +8,8 @@ import androidx.compose.runtime.remember import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import com.gemwallet.android.features.asset_select.presents.navigation.AssetsManageRoute -import com.gemwallet.android.features.asset_select.presents.navigation.AssetsSearchRoute +import com.gemwallet.android.ui.navigation.routes.AssetsResultsRoute +import com.gemwallet.android.ui.navigation.routes.WalletSearchRoute import com.gemwallet.android.features.create_wallet.navigation.CreateWalletAlertRoute import com.gemwallet.android.features.create_wallet.navigation.CreateWalletRoute import com.gemwallet.android.features.import_wallet.navigation.ImportChainWalletRoute @@ -74,6 +75,7 @@ import com.gemwallet.android.ui.navigation.routes.WalletSecurityReminderRoute import com.gemwallet.android.ui.navigation.routes.WalletsRoute import com.gemwallet.android.ext.toIdentifier import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetTag import com.wallet.core.primitives.NFTAssetId import com.wallet.core.primitives.TransactionId import com.wallet.core.primitives.WalletId @@ -170,7 +172,8 @@ class WalletNavigator( fun openWallets() = push(WalletsRoute) fun openAcceptTerms(destination: AcceptTermsDestination) = push(AcceptTermsRoute(destination)) fun openAssetsManage() = push(AssetsManageRoute) - fun openAssetsSearch() = push(AssetsSearchRoute) + fun openAssetsSearch() = push(WalletSearchRoute) + fun openAssetsResults(query: String, tag: AssetTag?) = push(AssetsResultsRoute(query, tag)) fun openCreateWalletRules() = push(CreateWalletAlertRoute) fun openCreateWallet() = push(CreateWalletRoute) fun openImportWallet() = push(ImportSelectTypeRoute) diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt index 074ac3e34b..fb7cc3530f 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt @@ -22,6 +22,7 @@ import com.gemwallet.android.ui.models.actions.AmountTransactionAction import com.gemwallet.android.ui.models.actions.ConfirmTransactionAction import com.gemwallet.android.features.activities.presents.details.TransactionDetailsAction import com.gemwallet.android.features.asset_select.presents.navigation.assetsManageScreen +import com.gemwallet.android.features.assets.views.WalletSearchAction import com.gemwallet.android.features.create_wallet.navigation.createWalletScreen import com.gemwallet.android.features.import_wallet.navigation.importWalletScreen import com.gemwallet.android.features.onboarding.OnboardingRoute @@ -51,6 +52,7 @@ import com.gemwallet.android.ui.navigation.routes.swap import com.gemwallet.android.ui.navigation.routes.swapSelect import com.gemwallet.android.ui.navigation.routes.transactionDetailsScreen import com.gemwallet.android.ui.navigation.routes.walletScreen +import com.gemwallet.android.ui.navigation.routes.walletSearchScreen import com.gemwallet.android.ui.navigation.routes.walletsScreen import com.wallet.core.primitives.WalletId @@ -86,6 +88,19 @@ fun WalletNavGraph( onCancel = onCancel, ) + walletSearchScreen( + onAction = { action -> + when (action) { + WalletSearchAction.AddAsset -> navigator.openAddAsset() + WalletSearchAction.Cancel -> onCancel() + WalletSearchAction.OpenPerpetuals -> navigator.openPerpetuals() + is WalletSearchAction.OpenAsset -> navigator.openAsset(action.assetId) + is WalletSearchAction.OpenPerpetual -> navigator.openPerpetualDetails(action.assetId) + is WalletSearchAction.ShowAllAssets -> navigator.openAssetsResults(action.query, action.tag) + } + }, + ) + assetScreen( onCancel = onCancel, onTransfer = navigator::openRecipient, diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/WalletSearch.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/WalletSearch.kt new file mode 100644 index 0000000000..a14bf7dea0 --- /dev/null +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/WalletSearch.kt @@ -0,0 +1,36 @@ +package com.gemwallet.android.ui.navigation.routes + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.gemwallet.android.features.assets.views.AssetsResultsScreen +import com.gemwallet.android.features.assets.views.WalletSearchAction +import com.gemwallet.android.features.assets.views.WalletSearchScreen +import com.gemwallet.android.ui.models.navigation.RouteArgument +import com.gemwallet.android.ui.navigation.routeArguments +import com.wallet.core.primitives.AssetTag +import kotlinx.serialization.Serializable + +@Serializable +data object WalletSearchRoute : NavKey + +@Serializable +data class AssetsResultsRoute(val query: String, val tag: AssetTag?) : NavKey + +fun EntryProviderScope.walletSearchScreen( + onAction: (WalletSearchAction) -> Unit, +) { + entry { + WalletSearchScreen(onAction = onAction) + } + + entry( + metadata = { key -> + routeArguments( + RouteArgument.Query to key.query, + RouteArgument.Tag to key.tag?.string, + ) + }, + ) { + AssetsResultsScreen(onAction = onAction) + } +} diff --git a/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImpl.kt b/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImpl.kt index 79746fd49b..1d7d404463 100644 --- a/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImpl.kt +++ b/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImpl.kt @@ -1,21 +1,23 @@ package com.gemwallet.android.data.coordinators.asset +import com.gemwallet.android.application.assets.coordinators.GemSearch import com.gemwallet.android.application.assets.coordinators.SearchAssets import com.gemwallet.android.data.services.gemapi.GemApiClient import com.wallet.core.primitives.AssetBasic import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetTag import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.SearchResponse class SearchAssetsImpl( private val gemApiClient: GemApiClient, -) : SearchAssets { +) : SearchAssets, GemSearch { override suspend fun search( query: String, chains: List, tags: List, - ): List { + ): SearchResponse { return gemApiClient.search( query = query, chains = chains.joinToString(",") { it.string }, @@ -23,6 +25,18 @@ class SearchAssetsImpl( ) } + override suspend fun searchAssets( + query: String, + chains: List, + tags: List, + ): List { + return gemApiClient.searchAssets( + query = query, + chains = chains.joinToString(",") { it.string }, + tags = tags.joinToString(",") { it.string }, + ) + } + override suspend fun getAssets(assetIds: List): List { return gemApiClient.getAssets(assetIds) } diff --git a/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/di/AssetModule.kt b/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/di/AssetModule.kt index b475f171f7..327354c7f1 100644 --- a/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/di/AssetModule.kt +++ b/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/di/AssetModule.kt @@ -1,6 +1,7 @@ package com.gemwallet.android.data.coordinators.di import com.gemwallet.android.application.assets.coordinators.EnableAsset +import com.gemwallet.android.application.assets.coordinators.GemSearch import com.gemwallet.android.application.assets.coordinators.GetActiveAssetsInfo import com.gemwallet.android.application.assets.coordinators.GetAssetById import com.gemwallet.android.application.assets.coordinators.GetAssetChartData @@ -64,12 +65,20 @@ import javax.inject.Singleton object AssetModule { @Provides @Singleton - fun provideSearchAssets( + fun provideSearchAssetsImpl( gemApiClient: GemApiClient, - ): SearchAssets = SearchAssetsImpl( + ): SearchAssetsImpl = SearchAssetsImpl( gemApiClient = gemApiClient, ) + @Provides + @Singleton + fun provideSearchAssets(impl: SearchAssetsImpl): SearchAssets = impl + + @Provides + @Singleton + fun provideGemSearch(impl: SearchAssetsImpl): GemSearch = impl + @Provides @Singleton fun provideGetActiveAssetsInfo(assetsRepository: AssetsRepository): GetActiveAssetsInfo = diff --git a/android/data/coordinators/src/test/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImplTest.kt b/android/data/coordinators/src/test/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImplTest.kt index ad7e26f557..1eceef9419 100644 --- a/android/data/coordinators/src/test/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImplTest.kt +++ b/android/data/coordinators/src/test/kotlin/com/gemwallet/android/data/coordinators/asset/SearchAssetsImplTest.kt @@ -4,6 +4,7 @@ import com.gemwallet.android.data.services.gemapi.GemApiClient import com.gemwallet.android.testkit.mockAssetBasic import com.wallet.core.primitives.AssetTag import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.SearchResponse import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk @@ -22,13 +23,14 @@ class SearchAssetsImplTest { @Test fun search_formatsChainsAndTagsForGemApi() = runTest { val asset = mockAssetBasic() + val response = SearchResponse(assets = listOf(asset), perpetuals = emptyList(), nfts = emptyList()) coEvery { gemApiClient.search( query = "usd", chains = "bitcoin,ethereum", tags = "trending,stablecoins", ) - } returns listOf(asset) + } returns response val result = subject.search( query = "usd", @@ -36,7 +38,7 @@ class SearchAssetsImplTest { tags = listOf(AssetTag.Trending, AssetTag.Stablecoins), ) - assertEquals(listOf(asset), result) + assertEquals(response, result) } @Test diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/assets/AssetsSearchService.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/assets/AssetsSearchService.kt index 934a4c13e5..b67f1a6384 100644 --- a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/assets/AssetsSearchService.kt +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/assets/AssetsSearchService.kt @@ -3,16 +3,18 @@ package com.gemwallet.android.data.repositories.assets import com.gemwallet.android.data.repositories.session.SessionRepository import com.gemwallet.android.data.repositories.tokens.toPriorityQuery import com.gemwallet.android.data.service.store.database.AssetsDao -import com.gemwallet.android.data.service.store.database.AssetsPriorityDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import com.gemwallet.android.data.service.store.database.entities.toAssetInfoModel import com.gemwallet.android.ext.toIdentifier import com.gemwallet.android.model.AssetInfo import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetTag import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.SearchItemType import com.wallet.core.primitives.Wallet import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -22,14 +24,14 @@ import javax.inject.Singleton @Singleton class AssetsSearchService @Inject constructor( private val assetsDao: AssetsDao, - private val assetsPriorityDao: AssetsPriorityDao, + private val searchPriorityDao: SearchPriorityDao, private val sessionRepository: SessionRepository, ) { fun search(query: String, tags: List, byAllWallets: Boolean): Flow> { val query = tags.toPriorityQuery(query) return sessionRepository.currentWalletId().flatMapLatest { walletId -> - assetsPriorityDao.hasPriorities(query).map { it > 0 }.flatMapLatest { hasPriority -> + searchPriorityDao.hasPriorities(query, SearchItemType.Asset.string).map { it > 0 }.distinctUntilChanged().flatMapLatest { hasPriority -> when { byAllWallets && hasPriority -> assetsDao.searchByAllWalletsWithPriority(walletId, query) byAllWallets -> assetsDao.searchByAllWallets(walletId, query) @@ -46,7 +48,7 @@ class AssetsSearchService @Inject constructor( val walletChains = wallet.accounts.map { it.chain } val includeChains = byChains.filter { walletChains.contains(it) } val includeAssetIds = byAssets.filter { walletChains.contains(it.chain) } - return assetsPriorityDao.hasPriorities(query).map { it > 0 }.flatMapLatest { hasPriority -> + return searchPriorityDao.hasPriorities(query, SearchItemType.Asset.string).map { it > 0 }.distinctUntilChanged().flatMapLatest { hasPriority -> if (hasPriority) { assetsDao.swapSearchWithPriority(wallet.id.id, query, includeChains, includeAssetIds.map { it.toIdentifier() }) } else { diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/config/UserConfigExt.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/config/UserConfigExt.kt new file mode 100644 index 0000000000..1dbd96f267 --- /dev/null +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/config/UserConfigExt.kt @@ -0,0 +1,11 @@ +package com.gemwallet.android.data.repositories.config + +import com.gemwallet.android.ext.hasPerpetualsSupport +import com.gemwallet.android.model.Session +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +fun UserConfig.showPerpetuals(session: Flow): Flow = + combine(session, isPerpetualEnabled()) { current, enabled -> + enabled && current?.wallet?.hasPerpetualsSupport == true + } diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/PerpetualModule.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/PerpetualModule.kt index 4aebdfd6b9..c814c8aee5 100644 --- a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/PerpetualModule.kt +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/PerpetualModule.kt @@ -6,6 +6,7 @@ import com.gemwallet.android.data.service.store.database.AssetsDao import com.gemwallet.android.data.service.store.database.BalancesDao import com.gemwallet.android.data.service.store.database.PerpetualDao import com.gemwallet.android.data.service.store.database.PerpetualPositionDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,12 +24,14 @@ object PerpetualModule { perpetualPositionDao: PerpetualPositionDao, assetsDao: AssetsDao, balancesDao: BalancesDao, + searchPriorityDao: SearchPriorityDao, ): PerpetualRepository { return PerpetualRepositoryImpl( perpetualDao = perpetualDao, perpetualPositionDao = perpetualPositionDao, assetsDao = assetsDao, balancesDao = balancesDao, + searchPriorityDao = searchPriorityDao, ) } } diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/TokensModule.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/TokensModule.kt index f68e0e15c6..94056d69ea 100644 --- a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/TokensModule.kt +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/di/TokensModule.kt @@ -1,13 +1,17 @@ package com.gemwallet.android.data.repositories.di +import com.gemwallet.android.application.assets.coordinators.GemSearch import com.gemwallet.android.application.assets.coordinators.SearchAssets import com.gemwallet.android.blockchain.services.TokenService import com.gemwallet.android.cases.tokens.SearchTokensCase import com.gemwallet.android.cases.tokens.SyncAssetPrices +import com.gemwallet.android.data.repositories.perpetual.PerpetualRepository import com.gemwallet.android.data.repositories.tokens.TokensRepository +import com.gemwallet.android.data.repositories.tokens.WalletSearch +import com.gemwallet.android.data.repositories.tokens.WalletSearchTokens import com.gemwallet.android.data.service.store.database.AssetsDao -import com.gemwallet.android.data.service.store.database.AssetsPriorityDao import com.gemwallet.android.data.service.store.database.PricesDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,13 +27,13 @@ object TokensModule { fun provideTokensRepository( assetsDao: AssetsDao, pricesDao: PricesDao, - assetsPriorityDao: AssetsPriorityDao, + searchPriorityDao: SearchPriorityDao, gateway: GemGateway, searchAssets: SearchAssets, ): TokensRepository = TokensRepository( assetsDao = assetsDao, pricesDao = pricesDao, - assetsPriorityDao = assetsPriorityDao, + searchPriorityDao = searchPriorityDao, searchAssets = searchAssets, tokenService = TokenService( gateway = gateway, @@ -40,6 +44,16 @@ object TokensModule { @Singleton fun provideSearchTokensCase(tokensRepository: TokensRepository): SearchTokensCase = tokensRepository + @Provides + @Singleton + @WalletSearch + fun provideWalletSearchTokensCase( + tokensRepository: TokensRepository, + gemSearch: GemSearch, + perpetualRepository: PerpetualRepository, + searchPriorityDao: SearchPriorityDao, + ): SearchTokensCase = WalletSearchTokens(tokensRepository, gemSearch, perpetualRepository, searchPriorityDao) + @Provides @Singleton fun provideSyncAssetPrices(tokensRepository: TokensRepository): SyncAssetPrices = tokensRepository diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/perpetual/PerpetualRepositoryImpl.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/perpetual/PerpetualRepositoryImpl.kt index 85da0ff320..07756f9e2b 100644 --- a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/perpetual/PerpetualRepositoryImpl.kt +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/perpetual/PerpetualRepositoryImpl.kt @@ -4,10 +4,12 @@ import com.gemwallet.android.data.service.store.database.AssetsDao import com.gemwallet.android.data.service.store.database.BalancesDao import com.gemwallet.android.data.service.store.database.PerpetualDao import com.gemwallet.android.data.service.store.database.PerpetualPositionDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import com.gemwallet.android.data.service.store.database.entities.toDB import com.gemwallet.android.data.service.store.database.entities.toDTO import com.gemwallet.android.data.service.store.database.entities.toDto import com.gemwallet.android.data.service.store.database.entities.DbBalance +import com.gemwallet.android.data.service.store.database.entities.DbPerpetualData import com.gemwallet.android.data.service.store.database.entities.toRecord import com.gemwallet.android.ext.toIdentifier import com.wallet.core.primitives.Asset @@ -18,8 +20,12 @@ import com.wallet.core.primitives.PerpetualData import com.wallet.core.primitives.PerpetualId import com.wallet.core.primitives.PerpetualPosition import com.wallet.core.primitives.PerpetualPositionData +import com.wallet.core.primitives.SearchItemType import com.wallet.core.primitives.WalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map class PerpetualRepositoryImpl( @@ -27,6 +33,7 @@ class PerpetualRepositoryImpl( private val perpetualPositionDao: PerpetualPositionDao, private val assetsDao: AssetsDao, private val balancesDao: BalancesDao, + private val searchPriorityDao: SearchPriorityDao, ) : PerpetualRepository { override suspend fun putPerpetuals(items: List) { @@ -34,17 +41,31 @@ class PerpetualRepositoryImpl( perpetualDao.upsert(items.map { it.perpetual.toDB() }) } + @OptIn(ExperimentalCoroutinesApi::class) override fun getPerpetuals(query: String?): Flow> { - val needle = query?.trim().orEmpty() - return perpetualDao.getPerpetualsData().map { items -> - items.mapNotNull { it.toDTO() }.filter { needle.isEmpty() || it.matches(needle) } + val searchQuery = query?.trim().orEmpty() + if (searchQuery.isEmpty()) { + return perpetualDao.getPerpetualsData().toPerpetualData() } + return searchPriorityDao.hasPriorities(searchQuery, SearchItemType.Perpetual.string) + .map { it > 0 } + .distinctUntilChanged() + .flatMapLatest { hasPriority -> + if (hasPriority) { + perpetualDao.searchWithPriority(searchQuery).toPerpetualData() + } else { + perpetualDao.getPerpetualsData().toPerpetualData() + .map { items -> items.filter { it.matches(searchQuery) } } + } + } } - private fun PerpetualData.matches(needle: String): Boolean = - perpetual.name.contains(needle, ignoreCase = true) || - asset.symbol.contains(needle, ignoreCase = true) || - asset.name.contains(needle, ignoreCase = true) + private fun Flow>.toPerpetualData(): Flow> = + map { items -> items.mapNotNull { it.toDTO() } } + + private fun PerpetualData.matches(query: String): Boolean = + perpetual.name.contains(query, ignoreCase = true) || + asset.symbol.contains(query, ignoreCase = true) override fun getPerpetual(perpetualId: PerpetualId): Flow { return perpetualDao.getPerpetual(perpetualId.toIdentifier()).map { it?.toDTO() } diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepository.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepository.kt index 9f67456b30..ecd3a49ed2 100644 --- a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepository.kt +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepository.kt @@ -5,12 +5,12 @@ import com.gemwallet.android.blockchain.services.TokenService import com.gemwallet.android.cases.tokens.SearchTokensCase import com.gemwallet.android.cases.tokens.SyncAssetPrices import com.gemwallet.android.data.service.store.database.AssetsDao -import com.gemwallet.android.data.service.store.database.AssetsPriorityDao import com.gemwallet.android.data.service.store.database.PricesDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import com.gemwallet.android.data.service.store.database.entities.toDTO import com.gemwallet.android.data.service.store.database.entities.toRecord import com.gemwallet.android.data.service.store.database.entities.toPriceRecord -import com.gemwallet.android.data.service.store.database.entities.toRecordPriority +import com.gemwallet.android.data.service.store.database.entities.toSearchPriority import com.gemwallet.android.data.service.store.database.entities.toUpdateRecord import com.gemwallet.android.domains.asset.defaultBasic import com.gemwallet.android.ext.toIdentifier @@ -19,6 +19,7 @@ import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetTag import com.wallet.core.primitives.Chain import com.wallet.core.primitives.Currency +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext @@ -26,7 +27,7 @@ import kotlinx.coroutines.withContext class TokensRepository ( private val assetsDao: AssetsDao, private val pricesDao: PricesDao, - private val assetsPriorityDao: AssetsPriorityDao, + private val searchPriorityDao: SearchPriorityDao, private val searchAssets: SearchAssets, private val tokenService: TokenService, ) : SearchTokensCase, SyncAssetPrices { @@ -36,24 +37,29 @@ class TokensRepository ( return@withContext false } val tokens = try { - searchAssets.search( + searchAssets.searchAssets( query = query, chains = chains, tags = tags, ) + } catch (err: CancellationException) { + throw err } catch (_: Throwable) { return@withContext false } - val assets = if (tokens.isEmpty()) { + storeAssets(query, tokens, currency, tags.toPriorityQuery(query)) + } + + internal suspend fun storeAssets(query: String, tokens: List, currency: Currency, priorityQuery: String): Boolean { + return if (tokens.isEmpty()) { val assets = tokenService.search(query) runCatching { assetsDao.insert(assets.map { it.toRecord() }) } - assets + assets.isNotEmpty() } else { updateAssets(tokens, currency) - assetsPriorityDao.put(tokens.toRecordPriority(tags.toPriorityQuery(query))) - tokens + searchPriorityDao.put(tokens.toSearchPriority(priorityQuery)) + true } - assets.isNotEmpty() } override suspend fun search(assetIds: List, currency: Currency): Boolean { diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearch.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearch.kt new file mode 100644 index 0000000000..caffd5503b --- /dev/null +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearch.kt @@ -0,0 +1,7 @@ +package com.gemwallet.android.data.repositories.tokens + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class WalletSearch diff --git a/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearchTokens.kt b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearchTokens.kt new file mode 100644 index 0000000000..6bff5faaaa --- /dev/null +++ b/android/data/repositories/src/main/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearchTokens.kt @@ -0,0 +1,51 @@ +package com.gemwallet.android.data.repositories.tokens + +import com.gemwallet.android.application.assets.coordinators.GemSearch +import com.gemwallet.android.cases.tokens.SearchTokensCase +import com.gemwallet.android.data.repositories.perpetual.PerpetualRepository +import com.gemwallet.android.data.service.store.database.SearchPriorityDao +import com.gemwallet.android.data.service.store.database.entities.toSearchPriority +import com.wallet.core.primitives.AssetTag +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.Currency +import com.wallet.core.primitives.PerpetualData +import com.wallet.core.primitives.PerpetualMetadata +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class WalletSearchTokens( + private val tokensRepository: TokensRepository, + private val gemSearch: GemSearch, + private val perpetualRepository: PerpetualRepository, + private val searchPriorityDao: SearchPriorityDao, +) : SearchTokensCase by tokensRepository { + + override suspend fun search(query: String, currency: Currency, chains: List, tags: List): Boolean = withContext(Dispatchers.IO) { + if (query.isEmpty() && tags.isEmpty()) { + return@withContext false + } + val response = try { + gemSearch.search(query = query, chains = chains, tags = tags) + } catch (err: CancellationException) { + throw err + } catch (_: Throwable) { + return@withContext false + } + val priorityQuery = tags.toPriorityQuery(query) + val hasAssets = tokensRepository.storeAssets(query, response.assets, currency, priorityQuery) + val perpetuals = if (tags.isEmpty()) response.perpetuals else emptyList() + if (perpetuals.isNotEmpty()) { + try { + perpetualRepository.putPerpetuals( + perpetuals.map { PerpetualData(perpetual = it.perpetual, asset = it.asset, metadata = PerpetualMetadata(isPinned = false)) } + ) + searchPriorityDao.put(perpetuals.toSearchPriority(priorityQuery)) + } catch (err: CancellationException) { + throw err + } catch (_: Throwable) { + } + } + hasAssets || perpetuals.isNotEmpty() + } +} diff --git a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/assets/AssetsRepositoryTest.kt b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/assets/AssetsRepositoryTest.kt index 5b5706b71b..004ce33ecf 100644 --- a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/assets/AssetsRepositoryTest.kt +++ b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/assets/AssetsRepositoryTest.kt @@ -5,9 +5,9 @@ import com.gemwallet.android.data.repositories.session.SessionRepository import com.gemwallet.android.data.repositories.stream.StreamSubscriptionService import com.gemwallet.android.cases.tokens.SearchTokensCase import com.gemwallet.android.data.service.store.database.AssetsDao -import com.gemwallet.android.data.service.store.database.AssetsPriorityDao import com.gemwallet.android.data.service.store.database.BalancesDao import com.gemwallet.android.data.service.store.database.PricesDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import com.gemwallet.android.data.service.store.database.entities.DbAsset import com.gemwallet.android.data.service.store.database.entities.DbAssetBasicUpdate import com.gemwallet.android.data.service.store.database.entities.DbFiatRate @@ -28,12 +28,12 @@ import com.gemwallet.android.testkit.mockAssetLink import com.gemwallet.android.testkit.mockAssetEthereum import com.gemwallet.android.testkit.mockAssetMarket import com.gemwallet.android.testkit.mockWalletId +import com.gemwallet.android.testkit.mockWalletId import com.gemwallet.android.testkit.mockAssetProperties import com.gemwallet.android.testkit.mockAssetSolana import com.gemwallet.android.testkit.mockAssetSolanaUSDC import com.gemwallet.android.testkit.mockSession import com.gemwallet.android.testkit.mockWallet -import com.gemwallet.android.testkit.mockWalletId import com.gemwallet.android.testkit.mockChartValuePercentage import com.gemwallet.android.testkit.mockPrice import com.wallet.core.primitives.AssetBasic @@ -63,7 +63,7 @@ import uniffi.gemstone.assetDefaultRank class AssetsRepositoryTest { private val assetsDao = mockk(relaxed = true) - private val assetsPriorityDao = mockk(relaxed = true) + private val searchPriorityDao = mockk(relaxed = true) private val balancesDao = mockk(relaxed = true) private val pricesDao = mockk(relaxed = true) private val sessionRepository = mockk() @@ -84,7 +84,7 @@ class AssetsRepositoryTest { streamSubscriptionService = streamSubscriptionService, availabilityService = AssetsAvailabilityService(assetsDao), currencyRatesService = CurrencyRatesService(pricesDao), - searchService = AssetsSearchService(assetsDao, assetsPriorityDao, sessionRepository), + searchService = AssetsSearchService(assetsDao, searchPriorityDao, sessionRepository), recentAssetsService = RecentAssetsService(assetsDao, sessionRepository), updateBalances = updateBalances, scope = scope, @@ -397,7 +397,7 @@ class AssetsRepositoryTest { @Test fun swapSearch_includesEnabledHiddenAndUnlinkedAssets() = runBlocking { every { sessionRepository.session() } returns sessionFlow - every { assetsPriorityDao.hasPriorities("") } returns flowOf(0) + every { searchPriorityDao.hasPriorities("", "asset") } returns flowOf(0) val wallet = mockWallet( id = "wallet-1", @@ -463,7 +463,7 @@ class AssetsRepositoryTest { @Test fun swapSearch_usesPriorityDaoAndPreservesOrderWhenPrioritiesExist() = runBlocking { every { sessionRepository.session() } returns sessionFlow - every { assetsPriorityDao.hasPriorities("usd") } returns flowOf(2) + every { searchPriorityDao.hasPriorities("usd", "asset") } returns flowOf(2) val wallet = mockWallet( id = "wallet-1", diff --git a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepositoryTest.kt b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepositoryTest.kt index be3e4565da..28f975250b 100644 --- a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepositoryTest.kt +++ b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/TokensRepositoryTest.kt @@ -3,11 +3,11 @@ package com.gemwallet.android.data.repositories.tokens import com.gemwallet.android.application.assets.coordinators.SearchAssets import com.gemwallet.android.blockchain.services.TokenService import com.gemwallet.android.data.service.store.database.AssetsDao -import com.gemwallet.android.data.service.store.database.AssetsPriorityDao import com.gemwallet.android.data.service.store.database.PricesDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import com.gemwallet.android.data.service.store.database.entities.DbAssetBasicUpdate -import com.gemwallet.android.data.service.store.database.entities.DbAssetPriority import com.gemwallet.android.data.service.store.database.entities.DbFiatRate +import com.gemwallet.android.data.service.store.database.entities.DbSearchPriority import com.gemwallet.android.data.service.store.database.entities.DbPrice import com.gemwallet.android.ext.toIdentifier import com.gemwallet.android.testkit.mockAsset @@ -31,14 +31,14 @@ class TokensRepositoryTest { private val assetsDao = mockk(relaxed = true) private val pricesDao = mockk(relaxed = true) - private val assetsPriorityDao = mockk(relaxed = true) + private val searchPriorityDao = mockk(relaxed = true) private val searchAssets = mockk() private val tokenService = mockk(relaxed = true) private val subject = TokensRepository( assetsDao = assetsDao, pricesDao = pricesDao, - assetsPriorityDao = assetsPriorityDao, + searchPriorityDao = searchPriorityDao, searchAssets = searchAssets, tokenService = tokenService, ) @@ -47,7 +47,7 @@ class TokensRepositoryTest { fun search_usesSearchAssetsAndStoresPriority() = runTest { val asset = mockAssetBasic() coEvery { - searchAssets.search( + searchAssets.searchAssets( query = "btc", chains = listOf(Chain.Bitcoin), tags = listOf(AssetTag.Trending), @@ -61,17 +61,17 @@ class TokensRepositoryTest { chains = listOf(Chain.Bitcoin), tags = listOf(AssetTag.Trending), ) - val priorities = slot>() + val priorities = slot>() assertTrue(result) coVerify { - searchAssets.search( + searchAssets.searchAssets( query = "btc", chains = listOf(Chain.Bitcoin), tags = listOf(AssetTag.Trending), ) } - coVerify { assetsPriorityDao.put(capture(priorities)) } + coVerify { searchPriorityDao.put(capture(priorities)) } assertEquals("btc::trending", priorities.captured.single().query) } @@ -80,7 +80,7 @@ class TokensRepositoryTest { val firstResult = mockAssetBasic(asset = mockAssetEthereum(), rank = 10) val secondResult = mockAssetBasic(asset = mockAsset(), rank = 999) coEvery { - searchAssets.search( + searchAssets.searchAssets( query = "usdt arbitrum", chains = emptyList(), tags = emptyList(), @@ -95,11 +95,11 @@ class TokensRepositoryTest { tags = emptyList(), ) - val priorities = slot>() - coVerify { assetsPriorityDao.put(capture(priorities)) } + val priorities = slot>() + coVerify { searchPriorityDao.put(capture(priorities)) } val captured = priorities.captured - assertEquals(listOf(firstResult.asset.id.toIdentifier(), secondResult.asset.id.toIdentifier()), captured.map { it.assetId }) + assertEquals(listOf(firstResult.asset.id.toIdentifier(), secondResult.asset.id.toIdentifier()), captured.map { it.itemId }) assertTrue("first response item must outrank later items", captured[0].priority < captured[1].priority) } diff --git a/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearchTokensTest.kt b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearchTokensTest.kt new file mode 100644 index 0000000000..784bd284b8 --- /dev/null +++ b/android/data/repositories/src/test/kotlin/com/gemwallet/android/data/repositories/tokens/WalletSearchTokensTest.kt @@ -0,0 +1,84 @@ +package com.gemwallet.android.data.repositories.tokens + +import com.gemwallet.android.application.assets.coordinators.GemSearch +import com.gemwallet.android.data.repositories.perpetual.PerpetualRepository +import com.gemwallet.android.data.service.store.database.SearchPriorityDao +import com.gemwallet.android.testkit.mockAsset +import com.gemwallet.android.testkit.mockAssetBasic +import com.wallet.core.primitives.Currency +import com.wallet.core.primitives.Perpetual +import com.wallet.core.primitives.PerpetualId +import com.wallet.core.primitives.PerpetualProvider +import com.wallet.core.primitives.PerpetualSearchData +import com.wallet.core.primitives.SearchResponse +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Test + +class WalletSearchTokensTest { + + private val tokensRepository = mockk(relaxed = true) + private val gemSearch = mockk() + private val perpetualRepository = mockk(relaxed = true) + private val searchPriorityDao = mockk(relaxed = true) + + private val subject = WalletSearchTokens( + tokensRepository = tokensRepository, + gemSearch = gemSearch, + perpetualRepository = perpetualRepository, + searchPriorityDao = searchPriorityDao, + ) + + @Test + fun search_ingestsPerpetualsAndStoresPerpPriority() = runTest { + val perpAsset = mockAsset() + val perpetual = Perpetual( + id = PerpetualId(provider = PerpetualProvider.Hypercore, symbol = "BTC"), + name = "Bitcoin", + provider = PerpetualProvider.Hypercore, + assetId = perpAsset.id, + identifier = "0", + price = 1.0, + pricePercentChange24h = 0.0, + openInterest = 0.0, + volume24h = 1.0, + funding = 0.0, + maxLeverage = 1u, + isIsolatedOnly = false, + ) + coEvery { + gemSearch.search(query = "btc", chains = emptyList(), tags = emptyList()) + } returns SearchResponse( + assets = listOf(mockAssetBasic()), + perpetuals = listOf(PerpetualSearchData(perpetual = perpetual, asset = perpAsset)), + nfts = emptyList(), + ) + + val result = subject.search( + query = "btc", + currency = Currency.USD, + chains = emptyList(), + tags = emptyList(), + ) + + assertTrue(result) + coVerify { perpetualRepository.putPerpetuals(any()) } + coVerify { searchPriorityDao.put(match { priorities -> priorities.any { it.type == "perpetual" } }) } + } + + @Test + fun search_rethrowsCancellationWithoutStoring() = runTest { + coEvery { gemSearch.search(any(), any(), any()) } throws CancellationException("cancelled") + + val result = runCatching { + subject.search(query = "btc", currency = Currency.USD, chains = emptyList(), tags = emptyList()) + } + + assertTrue(result.exceptionOrNull() is CancellationException) + coVerify(exactly = 0) { tokensRepository.storeAssets(any(), any(), any(), any()) } + } +} diff --git a/android/data/services/remote-gem/src/main/kotlin/com/gemwallet/android/data/services/gemapi/GemApiClient.kt b/android/data/services/remote-gem/src/main/kotlin/com/gemwallet/android/data/services/gemapi/GemApiClient.kt index dbbb5ae6b3..f76332c50f 100644 --- a/android/data/services/remote-gem/src/main/kotlin/com/gemwallet/android/data/services/gemapi/GemApiClient.kt +++ b/android/data/services/remote-gem/src/main/kotlin/com/gemwallet/android/data/services/gemapi/GemApiClient.kt @@ -5,6 +5,7 @@ import com.wallet.core.primitives.AssetFull import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.Charts import com.wallet.core.primitives.ConfigResponse +import com.wallet.core.primitives.SearchResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -27,9 +28,16 @@ interface GemApiClient { ): List @GET("/v1/assets/search") - suspend fun search( + suspend fun searchAssets( @Query("query") query: String, @Query("chains") chains: String, @Query("tags") tags: String, ): List + + @GET("/v1/search") + suspend fun search( + @Query("query") query: String, + @Query("chains") chains: String, + @Query("tags") tags: String, + ): SearchResponse } diff --git a/android/data/services/store/schemas/com.gemwallet.android.data.service.store.database.GemDatabase/80.json b/android/data/services/store/schemas/com.gemwallet.android.data.service.store.database.GemDatabase/80.json new file mode 100644 index 0000000000..74d57638b2 --- /dev/null +++ b/android/data/services/store/schemas/com.gemwallet.android.data.service.store.database.GemDatabase/80.json @@ -0,0 +1,2494 @@ +{ + "formatVersion": 1, + "database": { + "version": 80, + "identityHash": "115e24b41923a5b39db8ef8b34dd3953", + "entities": [ + { + "tableName": "wallets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `domain_name` TEXT, `type` TEXT NOT NULL, `position` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `index` INTEGER NOT NULL, `source` TEXT NOT NULL DEFAULT 'Import', `imageUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domainName", + "columnName": "domain_name", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Import'" + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `derivation_path` TEXT NOT NULL, `address` TEXT NOT NULL, `chain` TEXT NOT NULL, `extendedPublicKey` TEXT, PRIMARY KEY(`wallet_id`, `chain`), FOREIGN KEY(`chain`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`wallet_id`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "derivationPath", + "columnName": "derivation_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extendedPublicKey", + "columnName": "extendedPublicKey", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wallet_id", + "chain" + ] + }, + "indices": [ + { + "name": "index_accounts_chain", + "unique": false, + "columnNames": [ + "chain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_accounts_chain` ON `${TABLE_NAME}` (`chain`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "wallet_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "addresses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain` TEXT NOT NULL, `address` TEXT NOT NULL, `walletId` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`chain`, `address`), FOREIGN KEY(`chain`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`walletId`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "chain", + "address" + ] + }, + "indices": [ + { + "name": "index_addresses_chain", + "unique": false, + "columnNames": [ + "chain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_addresses_chain` ON `${TABLE_NAME}` (`chain`)" + }, + { + "name": "index_addresses_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_addresses_walletId` ON `${TABLE_NAME}` (`walletId`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "walletId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "contacts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "contacts_addresses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `contactId` TEXT NOT NULL, `address` TEXT NOT NULL, `chain` TEXT NOT NULL, `memo` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`contactId`) REFERENCES `contacts`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contactId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "memo", + "columnName": "memo", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_contacts_addresses_contactId", + "unique": false, + "columnNames": [ + "contactId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contacts_addresses_contactId` ON `${TABLE_NAME}` (`contactId`)" + } + ], + "foreignKeys": [ + { + "table": "contacts", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "contactId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "asset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `type` TEXT NOT NULL, `chain` TEXT NOT NULL, `is_enabled` INTEGER NOT NULL, `is_buy_enabled` INTEGER NOT NULL, `is_sell_enabled` INTEGER NOT NULL, `is_swap_enabled` INTEGER NOT NULL, `is_stake_enabled` INTEGER NOT NULL, `staking_apr` REAL, `rank` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "decimals", + "columnName": "decimals", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBuyEnabled", + "columnName": "is_buy_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSellEnabled", + "columnName": "is_sell_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSwapEnabled", + "columnName": "is_swap_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStakeEnabled", + "columnName": "is_stake_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stakingApr", + "columnName": "staking_apr", + "affinity": "REAL" + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "balances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `available` TEXT NOT NULL, `available_amount` REAL NOT NULL, `frozen` TEXT NOT NULL, `frozen_amount` REAL NOT NULL, `locked` TEXT NOT NULL, `locked_amount` REAL NOT NULL, `staked` TEXT NOT NULL, `staked_amount` REAL NOT NULL, `pending` TEXT NOT NULL, `pending_amount` REAL NOT NULL, `rewards` TEXT NOT NULL, `rewards_amount` REAL NOT NULL, `reserved` TEXT NOT NULL, `reserved_amount` REAL NOT NULL, `withdrawable` TEXT NOT NULL, `withdrawableAmount` REAL NOT NULL, `total_amount` REAL NOT NULL, `is_active` INTEGER NOT NULL, `is_pinned` INTEGER NOT NULL, `is_visible` INTEGER NOT NULL, `list_position` INTEGER NOT NULL, `votes` INTEGER NOT NULL DEFAULT 0, `energy_available` INTEGER NOT NULL DEFAULT 0, `energy_total` INTEGER NOT NULL DEFAULT 0, `bandwidth_available` INTEGER NOT NULL DEFAULT 0, `bandwidth_total` INTEGER NOT NULL DEFAULT 0, `updated_at` INTEGER, PRIMARY KEY(`asset_id`, `wallet_id`), FOREIGN KEY(`asset_id`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`wallet_id`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "availableAmount", + "columnName": "available_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "frozen", + "columnName": "frozen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "frozenAmount", + "columnName": "frozen_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lockedAmount", + "columnName": "locked_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "staked", + "columnName": "staked", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stakedAmount", + "columnName": "staked_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pending", + "columnName": "pending", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pendingAmount", + "columnName": "pending_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rewards", + "columnName": "rewards", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rewardsAmount", + "columnName": "rewards_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "reserved", + "columnName": "reserved", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reservedAmount", + "columnName": "reserved_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "withdrawable", + "columnName": "withdrawable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "withdrawableAmount", + "columnName": "withdrawableAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalAmount", + "columnName": "total_amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "is_active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPinned", + "columnName": "is_pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVisible", + "columnName": "is_visible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "listPosition", + "columnName": "list_position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "votes", + "columnName": "votes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "energyAvailable", + "columnName": "energy_available", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "energyTotal", + "columnName": "energy_total", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "bandwidthAvailable", + "columnName": "bandwidth_available", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "bandwidthTotal", + "columnName": "bandwidth_total", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "asset_id", + "wallet_id" + ] + }, + "indices": [ + { + "name": "index_balances_wallet_id", + "unique": false, + "columnNames": [ + "wallet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_balances_wallet_id` ON `${TABLE_NAME}` (`wallet_id`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "asset_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "wallet_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "prices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` TEXT NOT NULL, `value` REAL, `usd_value` REAL, `day_changed` REAL, `currency` TEXT NOT NULL, PRIMARY KEY(`asset_id`))", + "fields": [ + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL" + }, + { + "fieldPath": "usdValue", + "columnName": "usd_value", + "affinity": "REAL" + }, + { + "fieldPath": "dayChanged", + "columnName": "day_changed", + "affinity": "REAL" + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "asset_id" + ] + } + }, + { + "tableName": "transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `walletId` TEXT NOT NULL, `hash` TEXT NOT NULL, `assetId` TEXT NOT NULL, `feeAssetId` TEXT NOT NULL, `owner` TEXT NOT NULL, `recipient` TEXT NOT NULL, `contract` TEXT, `metadata` TEXT, `state` TEXT NOT NULL, `type` TEXT NOT NULL, `blockNumber` TEXT NOT NULL, `sequence` TEXT NOT NULL, `fee` TEXT NOT NULL, `value` TEXT NOT NULL, `payload` TEXT, `direction` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`, `walletId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feeAssetId", + "columnName": "feeAssetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contract", + "columnName": "contract", + "affinity": "TEXT" + }, + { + "fieldPath": "metadata", + "columnName": "metadata", + "affinity": "TEXT" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockNumber", + "columnName": "blockNumber", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sequence", + "columnName": "sequence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fee", + "columnName": "fee", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payload", + "columnName": "payload", + "affinity": "TEXT" + }, + { + "fieldPath": "direction", + "columnName": "direction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "walletId" + ] + } + }, + { + "tableName": "tx_swap_metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `from_asset_id` TEXT NOT NULL, `to_asset_id` TEXT NOT NULL, `from_amount` TEXT NOT NULL, `to_amount` TEXT NOT NULL, PRIMARY KEY(`tx_id`))", + "fields": [ + { + "fieldPath": "txId", + "columnName": "tx_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromAssetId", + "columnName": "from_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "toAssetId", + "columnName": "to_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromAmount", + "columnName": "from_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "toAmount", + "columnName": "to_amount", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tx_id" + ] + } + }, + { + "tableName": "wallets_connections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `session_id` TEXT NOT NULL, `state` TEXT NOT NULL, `chains` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `expire_at` INTEGER NOT NULL, `app_name` TEXT NOT NULL, `app_description` TEXT NOT NULL, `app_url` TEXT NOT NULL, `app_icon` TEXT NOT NULL, `redirect_native` TEXT, `redirect_universal` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`wallet_id`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chains", + "columnName": "chains", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expireAt", + "columnName": "expire_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appName", + "columnName": "app_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appDescription", + "columnName": "app_description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appUrl", + "columnName": "app_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appIcon", + "columnName": "app_icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "redirectNative", + "columnName": "redirect_native", + "affinity": "TEXT" + }, + { + "fieldPath": "redirectUniversal", + "columnName": "redirect_universal", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_wallets_connections_wallet_id", + "unique": false, + "columnNames": [ + "wallet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_connections_wallet_id` ON `${TABLE_NAME}` (`wallet_id`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "wallet_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "stake_validators", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assetId` TEXT NOT NULL, `validatorId` TEXT NOT NULL, `name` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `commission` REAL NOT NULL, `apr` REAL NOT NULL, `providerType` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assetId`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "validatorId", + "columnName": "validatorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "commission", + "columnName": "commission", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "apr", + "columnName": "apr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "providerType", + "columnName": "providerType", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_stake_validators_assetId", + "unique": false, + "columnNames": [ + "assetId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stake_validators_assetId` ON `${TABLE_NAME}` (`assetId`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "assetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "stake_delegations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `walletId` TEXT NOT NULL, `assetId` TEXT NOT NULL, `validatorId` TEXT NOT NULL, `state` TEXT NOT NULL, `delegationId` TEXT NOT NULL, `balance` TEXT NOT NULL, `shares` TEXT NOT NULL, `rewards` TEXT NOT NULL, `completionDate` INTEGER, PRIMARY KEY(`walletId`, `id`), FOREIGN KEY(`walletId`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`assetId`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`validatorId`) REFERENCES `stake_validators`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "validatorId", + "columnName": "validatorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "delegationId", + "columnName": "delegationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shares", + "columnName": "shares", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rewards", + "columnName": "rewards", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "completionDate", + "columnName": "completionDate", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId", + "id" + ] + }, + "indices": [ + { + "name": "index_stake_delegations_assetId", + "unique": false, + "columnNames": [ + "assetId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stake_delegations_assetId` ON `${TABLE_NAME}` (`assetId`)" + }, + { + "name": "index_stake_delegations_validatorId", + "unique": false, + "columnNames": [ + "validatorId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stake_delegations_validatorId` ON `${TABLE_NAME}` (`validatorId`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "walletId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "assetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "stake_validators", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "validatorId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `status` TEXT NOT NULL, `priority` INTEGER NOT NULL, `chain` TEXT NOT NULL, PRIMARY KEY(`url`), FOREIGN KEY(`chain`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_nodes_chain", + "unique": false, + "columnNames": [ + "chain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_chain` ON `${TABLE_NAME}` (`chain`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `wallet_id` TEXT NOT NULL, `currency` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "banners", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `asset_id` TEXT NOT NULL, `chain` TEXT, `state` TEXT NOT NULL, `event` TEXT NOT NULL, PRIMARY KEY(`wallet_id`, `asset_id`), FOREIGN KEY(`chain`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "event", + "columnName": "event", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wallet_id", + "asset_id" + ] + }, + "indices": [ + { + "name": "index_banners_event", + "unique": false, + "columnNames": [ + "event" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_banners_event` ON `${TABLE_NAME}` (`event`)" + }, + { + "name": "index_banners_wallet_id", + "unique": false, + "columnNames": [ + "wallet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_banners_wallet_id` ON `${TABLE_NAME}` (`wallet_id`)" + }, + { + "name": "index_banners_chain", + "unique": false, + "columnNames": [ + "chain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_banners_chain` ON `${TABLE_NAME}` (`chain`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "price_alerts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assetId` TEXT NOT NULL, `currency` TEXT NOT NULL, `price` REAL, `pricePercentChange` REAL, `priceDirection` TEXT, `lastNotifiedAt` INTEGER, `enabled` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "REAL" + }, + { + "fieldPath": "pricePercentChange", + "columnName": "pricePercentChange", + "affinity": "REAL" + }, + { + "fieldPath": "priceDirection", + "columnName": "priceDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "lastNotifiedAt", + "columnName": "lastNotifiedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "nft_collections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `chain` TEXT NOT NULL, `contractAddress` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `previewImageUrl` TEXT NOT NULL, `originalSourceUrl` TEXT NOT NULL, `status` TEXT, `links` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`chain`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contractAddress", + "columnName": "contractAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "previewImageUrl", + "columnName": "previewImageUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalSourceUrl", + "columnName": "originalSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "links", + "columnName": "links", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nft_collections_chain", + "unique": false, + "columnNames": [ + "chain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nft_collections_chain` ON `${TABLE_NAME}` (`chain`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nft_assets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `collection_id` TEXT NOT NULL, `token_id` TEXT NOT NULL, `token_type` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `chain` TEXT NOT NULL, `contract_address` TEXT, `image_url` TEXT NOT NULL, `preview_image_url` TEXT NOT NULL, `original_image_url` TEXT NOT NULL, `attributes` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`collection_id`) REFERENCES `nft_collections`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`chain`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collection_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tokenId", + "columnName": "token_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tokenType", + "columnName": "token_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "chain", + "columnName": "chain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contractAddress", + "columnName": "contract_address", + "affinity": "TEXT" + }, + { + "fieldPath": "imageUrl", + "columnName": "image_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "previewImageUrl", + "columnName": "preview_image_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalSourceUrl", + "columnName": "original_image_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nft_assets_collection_id", + "unique": false, + "columnNames": [ + "collection_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nft_assets_collection_id` ON `${TABLE_NAME}` (`collection_id`)" + }, + { + "name": "index_nft_assets_chain", + "unique": false, + "columnNames": [ + "chain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nft_assets_chain` ON `${TABLE_NAME}` (`chain`)" + } + ], + "foreignKeys": [ + { + "table": "nft_collections", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "collection_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nft_assets_associations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `asset_id` TEXT NOT NULL, PRIMARY KEY(`wallet_id`, `asset_id`), FOREIGN KEY(`asset_id`) REFERENCES `nft_assets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`wallet_id`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wallet_id", + "asset_id" + ] + }, + "indices": [ + { + "name": "index_nft_assets_associations_asset_id", + "unique": false, + "columnNames": [ + "asset_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nft_assets_associations_asset_id` ON `${TABLE_NAME}` (`asset_id`)" + } + ], + "foreignKeys": [ + { + "table": "nft_assets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "asset_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "wallet_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "asset_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` TEXT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`asset_id`, `name`), FOREIGN KEY(`asset_id`) REFERENCES `asset`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "asset_id", + "name" + ] + }, + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "asset_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "asset_market", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` TEXT NOT NULL, `marketCap` REAL, `marketCapFdv` REAL, `marketCapRank` INTEGER, `totalVolume` REAL, `circulatingSupply` REAL, `totalSupply` REAL, `maxSupply` REAL, `allTimeHigh` REAL, `allTimeHighDate` INTEGER, `allTimeHighChangePercentage` REAL, `allTimeLow` REAL, `allTimeLowDate` INTEGER, `allTimeLowChangePercentage` REAL, PRIMARY KEY(`asset_id`), FOREIGN KEY(`asset_id`) REFERENCES `asset`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketCap", + "columnName": "marketCap", + "affinity": "REAL" + }, + { + "fieldPath": "marketCapFdv", + "columnName": "marketCapFdv", + "affinity": "REAL" + }, + { + "fieldPath": "marketCapRank", + "columnName": "marketCapRank", + "affinity": "INTEGER" + }, + { + "fieldPath": "totalVolume", + "columnName": "totalVolume", + "affinity": "REAL" + }, + { + "fieldPath": "circulatingSupply", + "columnName": "circulatingSupply", + "affinity": "REAL" + }, + { + "fieldPath": "totalSupply", + "columnName": "totalSupply", + "affinity": "REAL" + }, + { + "fieldPath": "maxSupply", + "columnName": "maxSupply", + "affinity": "REAL" + }, + { + "fieldPath": "allTimeHigh", + "columnName": "allTimeHigh", + "affinity": "REAL" + }, + { + "fieldPath": "allTimeHighDate", + "columnName": "allTimeHighDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "allTimeHighChangePercentage", + "columnName": "allTimeHighChangePercentage", + "affinity": "REAL" + }, + { + "fieldPath": "allTimeLow", + "columnName": "allTimeLow", + "affinity": "REAL" + }, + { + "fieldPath": "allTimeLowDate", + "columnName": "allTimeLowDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "allTimeLowChangePercentage", + "columnName": "allTimeLowChangePercentage", + "affinity": "REAL" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "asset_id" + ] + }, + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "asset_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "search_priority", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, `type` TEXT NOT NULL, `item_id` TEXT NOT NULL, `priority` INTEGER NOT NULL, PRIMARY KEY(`query`, `type`, `item_id`))", + "fields": [ + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "item_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "query", + "type", + "item_id" + ] + } + }, + { + "tableName": "currency_rates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`currency` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`currency`))", + "fields": [ + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rate", + "columnName": "rate", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "currency" + ] + } + }, + { + "tableName": "fiat_transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `walletId` TEXT NOT NULL, `assetId` TEXT NOT NULL, `transactionType` TEXT NOT NULL, `provider` TEXT NOT NULL, `status` TEXT NOT NULL, `fiatAmount` REAL NOT NULL, `fiatCurrency` TEXT NOT NULL, `value` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `detailsUrl` TEXT, PRIMARY KEY(`id`, `walletId`), FOREIGN KEY(`walletId`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`assetId`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionType", + "columnName": "transactionType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fiatAmount", + "columnName": "fiatAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "fiatCurrency", + "columnName": "fiatCurrency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "detailsUrl", + "columnName": "detailsUrl", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "walletId" + ] + }, + "indices": [ + { + "name": "index_fiat_transactions_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_fiat_transactions_walletId` ON `${TABLE_NAME}` (`walletId`)" + }, + { + "name": "index_fiat_transactions_assetId", + "unique": false, + "columnNames": [ + "assetId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_fiat_transactions_assetId` ON `${TABLE_NAME}` (`assetId`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "walletId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "assetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "recent_assets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `to_asset_id` TEXT, `type` TEXT NOT NULL, `addedAt` INTEGER NOT NULL, PRIMARY KEY(`asset_id`, `wallet_id`, `type`), FOREIGN KEY(`asset_id`) REFERENCES `asset`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`wallet_id`) REFERENCES `wallets`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "toAssetId", + "columnName": "to_asset_id", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "asset_id", + "wallet_id", + "type" + ] + }, + "indices": [ + { + "name": "index_recent_assets_wallet_id", + "unique": false, + "columnNames": [ + "wallet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recent_assets_wallet_id` ON `${TABLE_NAME}` (`wallet_id`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "asset_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "wallet_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "perpetuals", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `provider` TEXT NOT NULL, `assetId` TEXT NOT NULL, `identifier` TEXT NOT NULL, `price` REAL NOT NULL, `pricePercentChange24h` REAL NOT NULL, `openInterest` REAL NOT NULL, `volume24h` REAL NOT NULL, `funding` REAL NOT NULL, `maxLeverage` INTEGER NOT NULL, `isIsolatedOnly` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assetId`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "identifier", + "columnName": "identifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pricePercentChange24h", + "columnName": "pricePercentChange24h", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "openInterest", + "columnName": "openInterest", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "volume24h", + "columnName": "volume24h", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "funding", + "columnName": "funding", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxLeverage", + "columnName": "maxLeverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIsolatedOnly", + "columnName": "isIsolatedOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPinned", + "columnName": "isPinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "perpetuals_asset_id_idx", + "unique": false, + "columnNames": [ + "assetId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `perpetuals_asset_id_idx` ON `${TABLE_NAME}` (`assetId`)" + } + ], + "foreignKeys": [ + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "assetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "perpetuals_positions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `walletId` TEXT NOT NULL, `perpetualId` TEXT NOT NULL, `assetId` TEXT NOT NULL, `size` REAL NOT NULL, `sizeValue` REAL NOT NULL, `leverage` INTEGER NOT NULL, `entryPrice` REAL, `liquidationPrice` REAL, `marginType` TEXT NOT NULL, `direction` TEXT NOT NULL, `marginAmount` REAL NOT NULL, `takeProfitPrice` REAL, `takeProfitType` TEXT, `takeProfitOrderId` TEXT, `stopLossPrice` REAL, `stopLossType` TEXT, `stopLossOrderId` TEXT, `pnl` REAL NOT NULL, `funding` REAL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`, `walletId`), FOREIGN KEY(`walletId`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`perpetualId`) REFERENCES `perpetuals`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`assetId`) REFERENCES `asset`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "perpetualId", + "columnName": "perpetualId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "sizeValue", + "columnName": "sizeValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryPrice", + "columnName": "entryPrice", + "affinity": "REAL" + }, + { + "fieldPath": "liquidationPrice", + "columnName": "liquidationPrice", + "affinity": "REAL" + }, + { + "fieldPath": "marginType", + "columnName": "marginType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "direction", + "columnName": "direction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marginAmount", + "columnName": "marginAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "takeProfitPrice", + "columnName": "takeProfitPrice", + "affinity": "REAL" + }, + { + "fieldPath": "takeProfitType", + "columnName": "takeProfitType", + "affinity": "TEXT" + }, + { + "fieldPath": "takeProfitOrderId", + "columnName": "takeProfitOrderId", + "affinity": "TEXT" + }, + { + "fieldPath": "stopLossPrice", + "columnName": "stopLossPrice", + "affinity": "REAL" + }, + { + "fieldPath": "stopLossType", + "columnName": "stopLossType", + "affinity": "TEXT" + }, + { + "fieldPath": "stopLossOrderId", + "columnName": "stopLossOrderId", + "affinity": "TEXT" + }, + { + "fieldPath": "pnl", + "columnName": "pnl", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "funding", + "columnName": "funding", + "affinity": "REAL" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "walletId" + ] + }, + "indices": [ + { + "name": "perpetuals_positions_wallet_id_idx", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `perpetuals_positions_wallet_id_idx` ON `${TABLE_NAME}` (`walletId`)" + }, + { + "name": "perpetuals_positions_perpetual_id_idx", + "unique": false, + "columnNames": [ + "perpetualId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `perpetuals_positions_perpetual_id_idx` ON `${TABLE_NAME}` (`perpetualId`)" + }, + { + "name": "perpetuals_positions_asset_id_idx", + "unique": false, + "columnNames": [ + "assetId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `perpetuals_positions_asset_id_idx` ON `${TABLE_NAME}` (`assetId`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "walletId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "perpetuals", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "perpetualId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "asset", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "assetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "in_app_notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `read_at` INTEGER, `created_at` INTEGER NOT NULL, `item` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`wallet_id`) REFERENCES `wallets`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readAt", + "columnName": "read_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "item", + "columnName": "item", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_in_app_notifications_wallet_id", + "unique": false, + "columnNames": [ + "wallet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_in_app_notifications_wallet_id` ON `${TABLE_NAME}` (`wallet_id`)" + }, + { + "name": "index_in_app_notifications_created_at", + "unique": false, + "columnNames": [ + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_in_app_notifications_created_at` ON `${TABLE_NAME}` (`created_at`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "wallet_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '115e24b41923a5b39db8ef8b34dd3953')" + ] + } +} \ No newline at end of file diff --git a/android/data/services/store/src/androidTest/kotlin/com/gemwallet/android/service/store/Migration_79_80Test.kt b/android/data/services/store/src/androidTest/kotlin/com/gemwallet/android/service/store/Migration_79_80Test.kt new file mode 100644 index 0000000000..aa5a6e3a7f --- /dev/null +++ b/android/data/services/store/src/androidTest/kotlin/com/gemwallet/android/service/store/Migration_79_80Test.kt @@ -0,0 +1,88 @@ +package com.gemwallet.android.service.store + +import android.content.Context +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.gemwallet.android.data.service.store.database.GemDatabase +import com.gemwallet.android.data.service.store.database.di.Migration_79_80 +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Migration_79_80Test { + + private val testDb = "migration-79-80-test" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + GemDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory() + ) + + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + context.deleteDatabase(testDb) + } + + @Test + fun migrate79To80_dropsAssetsPriorityAndCreatesUnifiedSearchPriority() = runBlocking { + helper.createDatabase(testDb, 79).apply { + execSQL("INSERT INTO assets_priority (`query`, asset_id, priority) VALUES ('btc', 'bitcoin', 0)") + close() + } + + val migratedDb = helper.runMigrationsAndValidate(testDb, 80, true, Migration_79_80) + + assertFalse(migratedDb.hasTable("assets_priority")) + assertTrue(migratedDb.hasTable("search_priority")) + assertTrue(migratedDb.hasColumn("search_priority", "query")) + assertTrue(migratedDb.hasColumn("search_priority", "type")) + assertTrue(migratedDb.hasColumn("search_priority", "item_id")) + assertTrue(migratedDb.hasColumn("search_priority", "priority")) + + migratedDb.execSQL("INSERT INTO search_priority (`query`, type, item_id, priority) VALUES ('btc', 'asset', 'bitcoin', 0)") + migratedDb.execSQL("INSERT INTO search_priority (`query`, type, item_id, priority) VALUES ('btc', 'perpetual', 'hypercore_perpetual::BTC', 0)") + assertEquals(2, migratedDb.longForQuery("SELECT COUNT(*) FROM search_priority")) + migratedDb.close() + } + + private fun SupportSQLiteDatabase.longForQuery(query: String): Long { + val cursor = query(query) + return cursor.use { + assertTrue(it.moveToFirst()) + it.getLong(0) + } + } + + private fun SupportSQLiteDatabase.hasTable(name: String): Boolean { + val cursor = query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = '$name'") + return cursor.use { it.moveToFirst() } + } + + private fun SupportSQLiteDatabase.hasColumn(table: String, column: String): Boolean { + val cursor = query("PRAGMA table_info($table)") + return cursor.use { + while (it.moveToNext()) { + if (it.getString(1) == column) { + return@use true + } + } + false + } + } +} diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsDao.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsDao.kt index f7bbd47c1f..1b1409d275 100644 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsDao.kt +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsDao.kt @@ -260,7 +260,8 @@ interface AssetsDao { AND (walletId = :walletId OR walletId IS NULL) AND assetRank > 0 AND (symbol LIKE '%' || :query || '%' - OR name LIKE '%' || :query || '%' COLLATE NOCASE) + OR name LIKE '%' || :query || '%' COLLATE NOCASE + OR SUBSTR(id, INSTR(id, '_') + 1) LIKE '%' || :query || '%' COLLATE NOCASE) ORDER BY balanceFiatTotalAmount DESC, assetRank DESC """) fun search(walletId: String, query: String, exclude: List = emptyList()): Flow> @@ -268,14 +269,14 @@ interface AssetsDao { @Query(""" SELECT asset_info.* FROM $ASSET_INFO - JOIN assets_priority ON asset_info.id = assets_priority.asset_id + JOIN search_priority ON asset_info.id = search_priority.item_id AND search_priority.type = 'asset' WHERE asset_info.id NOT IN (:exclude) AND chain IN (SELECT chain FROM accounts WHERE wallet_id = :walletId) AND (walletId = :walletId OR walletId IS NULL) AND assetRank > 0 - AND assets_priority.`query` = :query - ORDER BY balanceFiatTotalAmount DESC, assets_priority.priority ASC, assetRank DESC + AND search_priority.`query` = :query + ORDER BY balanceFiatTotalAmount DESC, search_priority.priority ASC, assetRank DESC """) fun searchWithPriority(walletId: String, query: String, exclude: List = emptyList()): Flow> @@ -284,7 +285,9 @@ interface AssetsDao { FROM $ASSET_INFO WHERE assetRank > 0 AND - (symbol LIKE '%' || :query || '%' OR name LIKE '%' || :query || '%' COLLATE NOCASE) + (symbol LIKE '%' || :query || '%' + OR name LIKE '%' || :query || '%' COLLATE NOCASE + OR SUBSTR(id, INSTR(id, '_') + 1) LIKE '%' || :query || '%' COLLATE NOCASE) ORDER BY balanceFiatTotalAmount DESC, assetRank DESC """) fun searchByAllWallets(walletId: String, query: String): Flow> @@ -292,12 +295,12 @@ interface AssetsDao { @Query(""" SELECT asset_info.* FROM $ASSET_INFO - JOIN assets_priority ON asset_info.id = assets_priority.asset_id + JOIN search_priority ON asset_info.id = search_priority.item_id AND search_priority.type = 'asset' WHERE assetRank > 0 AND - assets_priority.`query` = :query - ORDER BY balanceFiatTotalAmount DESC, assets_priority.priority ASC, assetRank DESC + search_priority.`query` = :query + ORDER BY balanceFiatTotalAmount DESC, search_priority.priority ASC, assetRank DESC """) fun searchByAllWalletsWithPriority(walletId: String, query: String): Flow> @@ -315,12 +318,12 @@ interface AssetsDao { @Query(""" SELECT asset_info.* FROM $ASSET_INFO - JOIN assets_priority ON asset_info.id = assets_priority.asset_id + JOIN search_priority ON asset_info.id = search_priority.item_id AND search_priority.type = 'asset' WHERE (chain IN (:byChains) OR id IN (:byAssets) ) AND assetRank > 0 - AND assets_priority.`query` = :query - ORDER BY assets_priority.priority ASC, assetRank DESC + AND search_priority.`query` = :query + ORDER BY search_priority.priority ASC, assetRank DESC """) fun swapSearchWithPriority(walletId: String, query: String, byChains: List, byAssets: List): Flow> diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsPriorityDao.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsPriorityDao.kt deleted file mode 100644 index 9a88304a15..0000000000 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/AssetsPriorityDao.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.gemwallet.android.data.service.store.database - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import androidx.room.Transaction -import com.gemwallet.android.data.service.store.database.entities.DbAssetPriority -import kotlinx.coroutines.flow.Flow - -@Dao -interface AssetsPriorityDao { - - @Insert(onConflict = REPLACE) - suspend fun insert(priorities: List) - - @Query("DELETE FROM assets_priority WHERE `query` = :query") - suspend fun deleteByQuery(query: String) - - @Transaction - suspend fun put(priorities: List) { - priorities.firstOrNull()?.query?.let { deleteByQuery(it) } - insert(priorities) - } - - @Query(""" - SELECT COUNT(asset_id) FROM assets_priority WHERE `query` = :query - """) - fun hasPriorities(query: String): Flow -} diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/GemDatabase.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/GemDatabase.kt index b740b2a2a4..71cc9ca880 100644 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/GemDatabase.kt +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/GemDatabase.kt @@ -8,7 +8,6 @@ import com.gemwallet.android.data.service.store.database.entities.DbAddress import com.gemwallet.android.data.service.store.database.entities.DbAsset import com.gemwallet.android.data.service.store.database.entities.DbAssetLink import com.gemwallet.android.data.service.store.database.entities.DbAssetMarket -import com.gemwallet.android.data.service.store.database.entities.DbAssetPriority import com.gemwallet.android.data.service.store.database.entities.DbBalance import com.gemwallet.android.data.service.store.database.entities.DbBanner import com.gemwallet.android.data.service.store.database.entities.DbConnection @@ -28,13 +27,14 @@ import com.gemwallet.android.data.service.store.database.entities.DbPerpetualPos import com.gemwallet.android.data.service.store.database.entities.DbPrice import com.gemwallet.android.data.service.store.database.entities.DbPriceAlert import com.gemwallet.android.data.service.store.database.entities.DbRecentActivity +import com.gemwallet.android.data.service.store.database.entities.DbSearchPriority import com.gemwallet.android.data.service.store.database.entities.DbSession import com.gemwallet.android.data.service.store.database.entities.DbTransaction import com.gemwallet.android.data.service.store.database.entities.DbTxSwapMetadata import com.gemwallet.android.data.service.store.database.entities.DbWallet @Database( - version = 79, + version = 80, entities = [ DbWallet::class, DbAccount::class, @@ -58,7 +58,7 @@ import com.gemwallet.android.data.service.store.database.entities.DbWallet DbNFTAssociation::class, DbAssetLink::class, DbAssetMarket::class, - DbAssetPriority::class, + DbSearchPriority::class, DbFiatRate::class, DbFiatTransaction::class, DbRecentActivity::class, @@ -99,7 +99,7 @@ abstract class GemDatabase : RoomDatabase() { abstract fun nftDao(): NftDao - abstract fun assetsPriorityDao(): AssetsPriorityDao + abstract fun searchPriorityDao(): SearchPriorityDao abstract fun perpetualDao(): PerpetualDao diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/PerpetualDao.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/PerpetualDao.kt index f9c6b28413..aaa0141786 100644 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/PerpetualDao.kt +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/PerpetualDao.kt @@ -34,6 +34,15 @@ interface PerpetualDao { @Query("SELECT * FROM perpetuals WHERE volume24h > 0 ORDER BY volume24h DESC") fun getPerpetualsData(): Flow> + @Transaction + @Query(""" + SELECT perpetuals.* FROM perpetuals + JOIN search_priority ON perpetuals.id = search_priority.item_id AND search_priority.type = 'perpetual' + WHERE search_priority.`query` = :query + ORDER BY search_priority.priority ASC, perpetuals.volume24h DESC + """) + fun searchWithPriority(query: String): Flow> + @Transaction @Query("SELECT * FROM perpetuals WHERE id = :perpetualId") fun getPerpetual(perpetualId: String): Flow diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/SearchPriorityDao.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/SearchPriorityDao.kt new file mode 100644 index 0000000000..d6a5cc1007 --- /dev/null +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/SearchPriorityDao.kt @@ -0,0 +1,28 @@ +package com.gemwallet.android.data.service.store.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import androidx.room.Transaction +import com.gemwallet.android.data.service.store.database.entities.DbSearchPriority +import kotlinx.coroutines.flow.Flow + +@Dao +interface SearchPriorityDao { + + @Insert(onConflict = REPLACE) + suspend fun insert(priorities: List) + + @Query("DELETE FROM search_priority WHERE `query` = :query AND type = :type") + suspend fun deleteByQuery(query: String, type: String) + + @Transaction + suspend fun put(priorities: List) { + priorities.firstOrNull()?.let { deleteByQuery(it.query, it.type) } + insert(priorities) + } + + @Query("SELECT COUNT(item_id) FROM search_priority WHERE `query` = :query AND type = :type") + fun hasPriorities(query: String, type: String): Flow +} diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/DatabaseModule.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/DatabaseModule.kt index 470dcf192d..c51b5ad8cc 100644 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/DatabaseModule.kt +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/DatabaseModule.kt @@ -6,7 +6,6 @@ import com.gemwallet.android.application.PasswordStore import com.gemwallet.android.data.service.store.database.AccountsDao import com.gemwallet.android.data.service.store.database.AddressesDao import com.gemwallet.android.data.service.store.database.AssetsDao -import com.gemwallet.android.data.service.store.database.AssetsPriorityDao import com.gemwallet.android.data.service.store.database.BalancesDao import com.gemwallet.android.data.service.store.database.BannersDao import com.gemwallet.android.data.service.store.database.ConnectionsDao @@ -19,6 +18,7 @@ import com.gemwallet.android.data.service.store.database.NodesDao import com.gemwallet.android.data.service.store.database.PerpetualDao import com.gemwallet.android.data.service.store.database.PerpetualPositionDao import com.gemwallet.android.data.service.store.database.PriceAlertsDao +import com.gemwallet.android.data.service.store.database.SearchPriorityDao import com.gemwallet.android.data.service.store.database.PricesDao import com.gemwallet.android.data.service.store.database.RoomStoreTransactionRunner import com.gemwallet.android.data.service.store.database.SessionDao @@ -81,6 +81,7 @@ object DatabaseModule { .addMigrations(Migration_76_77) .addMigrations(Migration_77_78) .addMigrations(Migration_78_79) + .addMigrations(Migration_79_80) .build() @Singleton @@ -149,7 +150,7 @@ object DatabaseModule { @Singleton @Provides - fun provideAssetsPriorityDao(db: GemDatabase): AssetsPriorityDao = db.assetsPriorityDao() + fun provideSearchPriorityDao(db: GemDatabase): SearchPriorityDao = db.searchPriorityDao() @Singleton @Provides diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/Migration_79_80.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/Migration_79_80.kt new file mode 100644 index 0000000000..15d9fde75c --- /dev/null +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/di/Migration_79_80.kt @@ -0,0 +1,18 @@ +package com.gemwallet.android.data.service.store.database.di + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +object Migration_79_80 : Migration(79, 80) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS `assets_priority`") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `search_priority` (" + + "`query` TEXT NOT NULL, " + + "`type` TEXT NOT NULL, " + + "`item_id` TEXT NOT NULL, " + + "`priority` INTEGER NOT NULL, " + + "PRIMARY KEY(`query`, `type`, `item_id`))" + ) + } +} diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/entities/DbAssetPriority.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/entities/DbAssetPriority.kt deleted file mode 100644 index eac751b47a..0000000000 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/entities/DbAssetPriority.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.gemwallet.android.data.service.store.database.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import com.gemwallet.android.ext.toIdentifier -import com.wallet.core.primitives.AssetBasic - -@Entity( - tableName = "assets_priority", - primaryKeys = ["query", "asset_id"], -) -data class DbAssetPriority( - val query: String, - @ColumnInfo(name = "asset_id") val assetId: String, - val priority: Int, -) - -fun List.toRecordPriority(query: String): List = mapIndexed { index, basic -> - DbAssetPriority( - query = query, - assetId = basic.asset.id.toIdentifier(), - priority = index, - ) -} diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/entities/DbSearchPriority.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/entities/DbSearchPriority.kt new file mode 100644 index 0000000000..d760ad8ded --- /dev/null +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/service/store/database/entities/DbSearchPriority.kt @@ -0,0 +1,39 @@ +package com.gemwallet.android.data.service.store.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.primitives.AssetBasic +import com.wallet.core.primitives.PerpetualSearchData +import com.wallet.core.primitives.SearchItemType + +@Entity( + tableName = "search_priority", + primaryKeys = ["query", "type", "item_id"], +) +data class DbSearchPriority( + val query: String, + val type: String, + @ColumnInfo(name = "item_id") val itemId: String, + val priority: Int, +) + +@JvmName("assetsToSearchPriority") +fun List.toSearchPriority(query: String): List = mapIndexed { index, basic -> + DbSearchPriority( + query = query, + type = SearchItemType.Asset.string, + itemId = basic.asset.id.toIdentifier(), + priority = index, + ) +} + +@JvmName("perpetualsToSearchPriority") +fun List.toSearchPriority(query: String): List = mapIndexed { index, data -> + DbSearchPriority( + query = query, + type = SearchItemType.Perpetual.string, + itemId = data.perpetual.id.toIdentifier(), + priority = index, + ) +} diff --git a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/navigation/AssetsManageNavigation.kt b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/navigation/AssetsManageNavigation.kt index b691f072e9..6a3a48a069 100644 --- a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/navigation/AssetsManageNavigation.kt +++ b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/navigation/AssetsManageNavigation.kt @@ -3,16 +3,12 @@ package com.gemwallet.android.features.asset_select.presents.navigation import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import com.gemwallet.android.features.asset_select.presents.views.AssetsManageScreen -import com.gemwallet.android.features.asset_select.presents.views.AssetsSearchScreen import com.wallet.core.primitives.AssetId import kotlinx.serialization.Serializable @Serializable data object AssetsManageRoute : NavKey -@Serializable -data object AssetsSearchRoute : NavKey - fun EntryProviderScope.assetsManageScreen( onAddAsset: () -> Unit, onAssetClick: (AssetId) -> Unit, @@ -25,12 +21,4 @@ fun EntryProviderScope.assetsManageScreen( onCancel = onCancel, ) } - - entry { - AssetsSearchScreen( - onAddAsset = onAddAsset, - onAssetClick = onAssetClick, - onCancel = onCancel, - ) - } } diff --git a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetBadge.kt b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetBadge.kt new file mode 100644 index 0000000000..3ba5109661 --- /dev/null +++ b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetBadge.kt @@ -0,0 +1,7 @@ +package com.gemwallet.android.features.asset_select.presents.views + +import com.gemwallet.android.ui.components.list_item.AssetItemUIModel + +fun getAssetBadge(item: AssetItemUIModel): String { + return if (item.asset.symbol == item.asset.name) "" else item.asset.symbol +} diff --git a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetSelectScene.kt b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetSelectScene.kt index ff5d708faa..e8ec360d5e 100644 --- a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetSelectScene.kt +++ b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetSelectScene.kt @@ -173,6 +173,10 @@ fun AssetSelectScene( onSelectRecent: ((AssetId) -> Unit)? = null, onOpenRecentsSheet: (() -> Unit)? = null, contextActions: AssetContextActions = AssetContextActions.Empty, + pinnedPerpetualRows: List<@Composable (ListPosition) -> Unit> = emptyList(), + perpetualsContent: (LazyListScope.() -> Unit)? = null, + assetsHeaderRes: Int? = null, + onAssetsHeaderClick: (() -> Unit)? = null, ) { val listState = rememberLazyListState() var isReturnToTop by remember { mutableStateOf(false) } @@ -242,7 +246,30 @@ fun AssetSelectScene( } recent(recent, onSelectRecent, onOpenRecentsSheet) assets(popular, AssetsGroupType.Popular, onSelect, support, titleBadge, itemTrailing, longPressedAsset, contextActions) - assets(pinned, AssetsGroupType.Pined, onSelect, support, titleBadge, itemTrailing, longPressedAsset, contextActions) + if (pinned.isNotEmpty() || pinnedPerpetualRows.isNotEmpty()) { + item { PinnedAssetsHeaderItem(AssetsGroupType.Pined) } + val pinnedTotal = pinnedPerpetualRows.size + pinned.size + itemsPositioned(pinnedPerpetualRows, totalCount = pinnedTotal) { position, row -> + row(position) + } + assetRows( + pinned, + onSelect, + support, + titleBadge, + itemTrailing, + longPressedAsset, + contextActions, + indexOffset = pinnedPerpetualRows.size, + totalCount = pinnedTotal, + ) + } + perpetualsContent?.invoke(this) + if (assetsHeaderRes != null && unpinned.isNotEmpty()) { + item { + SubheaderItem(assetsHeaderRes, onAssetsHeaderClick) + } + } assets(unpinned, AssetsGroupType.None, onSelect, support, titleBadge, itemTrailing, longPressedAsset, contextActions) loading(state) notFound(state = state, onAddAsset = onAddAsset, isAddAvailable = isAddAvailable) @@ -276,7 +303,21 @@ private fun LazyListScope.assets( item { PinnedAssetsHeaderItem(group) } - itemsPositioned(items) { position, item -> + assetRows(items, onSelect, support, titleBadge, itemTrailing, longPressedAsset, contextActions) +} + +fun LazyListScope.assetRows( + items: List, + onSelect: ((AssetId) -> Unit)?, + support: ((AssetItemUIModel) -> (@Composable () -> Unit)?)?, + titleBadge: (AssetItemUIModel) -> String?, + itemTrailing: (@Composable (AssetItemUIModel) -> Unit)?, + longPressedAsset: MutableState, + contextActions: AssetContextActions, + indexOffset: Int = 0, + totalCount: Int = items.size, +) { + itemsPositioned(items, indexOffset = indexOffset, totalCount = totalCount) { position, item -> AssetSelectRow( position = position, item = item, @@ -291,7 +332,7 @@ private fun LazyListScope.assets( } @Composable -private fun AssetSelectRow( +fun AssetSelectRow( position: ListPosition, item: AssetItemUIModel, support: ((AssetItemUIModel) -> (@Composable () -> Unit)?)?, @@ -364,11 +405,7 @@ private fun LazyListScope.recent( return } item { - if (onOpenRecentsSheet == null) { - SubheaderItem(R.string.recent_activity_title) - } else { - SubheaderItem(R.string.recent_activity_title, onClick = onOpenRecentsSheet) - } + SubheaderItem(R.string.recent_activity_title, onOpenRecentsSheet) } item { LazyRow( diff --git a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsManageScreen.kt b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsManageScreen.kt index 8a49976712..0e71dcd80d 100644 --- a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsManageScreen.kt +++ b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsManageScreen.kt @@ -90,7 +90,3 @@ fun AssetsManageScreen( contextActions = AssetContextActions.Empty, ) } - -fun getAssetBadge(item: AssetItemUIModel): String { - return if (item.asset.symbol == item.asset.name) "" else item.asset.symbol -} diff --git a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsSearchScreen.kt b/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsSearchScreen.kt deleted file mode 100644 index 6ce50dc891..0000000000 --- a/android/features/asset_select/presents/src/main/kotlin/com/gemwallet/android/features/asset_select/presents/views/AssetsSearchScreen.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.gemwallet.android.features.asset_select.presents.views - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.gemwallet.android.ui.components.SearchBar -import com.gemwallet.android.ui.components.list_item.AssetContextActions -import com.gemwallet.android.ui.components.list_item.AssetItemUIModel -import com.gemwallet.android.ui.components.list_item.assetPriceSupport -import com.gemwallet.android.ui.components.list_item.getBalanceInfo -import com.gemwallet.android.ui.components.list_item.listItem -import com.gemwallet.android.ui.models.ListPosition -import com.gemwallet.android.features.asset_select.viewmodels.AssetSelectViewModel -import com.gemwallet.android.features.asset_select.viewmodels.RecentsSheetViewModel -import com.gemwallet.android.model.RecentType -import com.wallet.core.primitives.AssetId -import kotlinx.collections.immutable.toImmutableList - -@Composable -fun AssetsSearchScreen( - onAddAsset: () -> Unit, - onAssetClick: (AssetId) -> Unit, - onCancel: () -> Unit, - viewModel: AssetSelectViewModel = hiltViewModel(), - recentsViewModel: RecentsSheetViewModel = hiltViewModel(), -) { - val isAddAssetAvailable by viewModel.isAddAssetAvailable.collectAsStateWithLifecycle() - val uiStates by viewModel.uiState.collectAsStateWithLifecycle() - val pinned by viewModel.pinned.collectAsStateWithLifecycle() - val unpinned by viewModel.unpinned.collectAsStateWithLifecycle() - val recent by viewModel.recent.collectAsStateWithLifecycle() - - val selectedTag by viewModel.selectedTag.collectAsStateWithLifecycle() - - val contextActions = remember(viewModel) { - AssetContextActions( - onTogglePin = viewModel::onTogglePin, - onAddToWallet = { id -> viewModel.onChangeVisibility(id, true) }, - ) - } - - val selectAsset: (AssetId) -> Unit = { id -> - viewModel.updateRecent(id, RecentType.Search) - onAssetClick(id) - } - - AssetSelectScene( - title = { - SearchBar( - query = viewModel.queryState, - modifier = Modifier.listItem(ListPosition.Single, paddingHorizontal = 0.dp), - ) - }, - titleBadge = ::getAssetBadge, - support = { assetPriceSupport(it.price) }, - query = viewModel.queryState, - selectedTag = selectedTag, - tags = viewModel.getTags(), - pinned = pinned, - popular = emptyList().toImmutableList(), - unpinned = unpinned, - recent = recent, - state = uiStates, - isAddAvailable = isAddAssetAvailable, - availableChains = emptyList(), - chainsFilter = emptyList(), - balanceFilter = false, - searchable = false, - onChainFilter = {}, - onBalanceFilter = {}, - onClearFilters = {}, - onCancel = onCancel, - onAddAsset = if (isAddAssetAvailable) onAddAsset else null, - onSelect = selectAsset, - onSelectRecent = onAssetClick, - onOpenRecentsSheet = { recentsViewModel.show(filters = viewModel.assetFilters()) }, - onTagSelect = viewModel::onTagSelect, - itemTrailing = { asset -> getBalanceInfo(asset)() }, - contextActions = contextActions, - ) - - RecentsSheetHost(viewModel = recentsViewModel, onSelect = onAssetClick) -} diff --git a/android/features/asset_select/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset_select/viewmodels/BaseAssetSelectViewModel.kt b/android/features/asset_select/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset_select/viewmodels/BaseAssetSelectViewModel.kt index 02f1dd80c2..b3ee75a6e4 100644 --- a/android/features/asset_select/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset_select/viewmodels/BaseAssetSelectViewModel.kt +++ b/android/features/asset_select/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset_select/viewmodels/BaseAssetSelectViewModel.kt @@ -15,7 +15,6 @@ import com.gemwallet.android.cases.tokens.SearchTokensCase import com.gemwallet.android.ext.assetType import com.gemwallet.android.ext.getAccount import com.gemwallet.android.model.RecentType -import com.gemwallet.android.model.Session import com.gemwallet.android.ui.components.list_item.AssetInfoUIModel import com.gemwallet.android.ui.components.list_item.AssetItemUIModel import com.gemwallet.android.features.asset_select.viewmodels.models.SelectAssetFilters @@ -35,12 +34,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -54,6 +54,7 @@ open class BaseAssetSelectViewModel( private val toggleAssetPin: ToggleAssetPin, private val searchTokensCase: SearchTokensCase, val search: SelectSearch, + private val remoteSearch: Boolean = true, ) : ViewModel() { val queryState = TextFieldState() @@ -70,9 +71,13 @@ open class BaseAssetSelectViewModel( .map { session -> session?.wallet?.accounts?.map { it.chain } ?: emptyList() } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - private val currentQuery = snapshotFlow { queryState.text.toString() } + protected val currentQuery = snapshotFlow { queryState.text.toString() } .stateIn(viewModelScope, SharingStarted.Eagerly, "") + private val searchRequests = combine(currentQuery, selectedTag, session) { query, tag, session -> + SearchRequest(query, tag, session?.currency ?: Currency.USD, walletSearchChains(session?.wallet)) + }.distinctUntilChanged() + private val filters = combine( session, currentQuery, @@ -81,7 +86,7 @@ open class BaseAssetSelectViewModel( balanceFilter, ) { session, query, tag, chainFilter, hasBalance -> SelectAssetFilters(session = session, query = query, chainFilter = chainFilter, hasBalance = hasBalance, tag = tag) - }.onEach { request(it.query, it.tag, it.session) } + } .stateIn(viewModelScope, SharingStarted.Eagerly, null) private val assets = combine( @@ -196,15 +201,23 @@ open class BaseAssetSelectViewModel( return session.value?.wallet?.getAccount(assetId) } - private fun request(query: String, tags: AssetTag?, session: Session?) = viewModelScope.launch(Dispatchers.IO) { - delay(SEARCH_DEBOUNCE_MS) - val ok = searchTokensCase.search( - query = query, - currency = session?.currency ?: Currency.USD, - chains = walletSearchChains(session?.wallet), - tags = tags?.let { listOf(it) } ?: emptyList(), - ) - noResultsQuery.value = if (ok) null else query + init { + viewModelScope.launch { + currentQuery.collect { query -> + if (query.isNotEmpty() && selectedTag.value != null) { + selectedTag.value = null + } + } + } + if (remoteSearch) { + viewModelScope.launch(Dispatchers.IO) { + searchRequests.collectLatest { (query, tag, currency, chains) -> + delay(SEARCH_DEBOUNCE_MS) + val ok = searchTokensCase.search(query, currency, chains, tag?.let { listOf(it) }.orEmpty()) + noResultsQuery.value = if (ok) null else query + } + } + } } private fun walletSearchChains(wallet: Wallet?): List = when (wallet?.type) { @@ -223,6 +236,13 @@ open class BaseAssetSelectViewModel( open fun assetFilters(): Set = emptySet() + private data class SearchRequest( + val query: String, + val tag: AssetTag?, + val currency: Currency, + val chains: List, + ) + private companion object { private const val SEARCH_DEBOUNCE_MS = 250L } diff --git a/android/features/assets/presents/build.gradle.kts b/android/features/assets/presents/build.gradle.kts index bb1b2c3828..fc65ecafbf 100644 --- a/android/features/assets/presents/build.gradle.kts +++ b/android/features/assets/presents/build.gradle.kts @@ -54,6 +54,8 @@ android { dependencies { implementation(project(":ui")) implementation(project(":features:assets:viewmodels")) + implementation(project(":features:asset_select:presents")) + implementation(project(":features:asset_select:viewmodels")) implementation(project(":features:update_app:presents")) implementation(project(":features:banner:presents")) implementation(project(":features:perpetual:presents")) diff --git a/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/AssetsResultsScreen.kt b/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/AssetsResultsScreen.kt new file mode 100644 index 0000000000..4437781781 --- /dev/null +++ b/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/AssetsResultsScreen.kt @@ -0,0 +1,69 @@ +package com.gemwallet.android.features.assets.views + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.gemwallet.android.features.asset_select.presents.views.assetRows +import com.gemwallet.android.features.asset_select.presents.views.getAssetBadge +import com.gemwallet.android.features.assets.viewmodels.AssetsResultsViewModel +import com.gemwallet.android.ui.R +import com.gemwallet.android.ui.components.list_item.AssetContextActions +import com.gemwallet.android.ui.components.list_item.PinnedAssetsHeaderItem +import com.gemwallet.android.ui.components.list_item.assetPriceSupport +import com.gemwallet.android.ui.components.list_item.getBalanceInfo +import com.gemwallet.android.ui.components.screen.Scene +import com.gemwallet.android.ui.models.AssetsGroupType +import com.wallet.core.primitives.AssetId + +@Composable +fun AssetsResultsScreen( + onAction: (WalletSearchAction) -> Unit, + viewModel: AssetsResultsViewModel = hiltViewModel(), +) { + val pinned by viewModel.pinned.collectAsStateWithLifecycle() + val cappedAssets by viewModel.cappedAssets.collectAsStateWithLifecycle() + val longPressedAsset = remember { mutableStateOf(null) } + val onAssetClick: (AssetId) -> Unit = { onAction(WalletSearchAction.OpenAsset(it)) } + val contextActions = remember(viewModel) { + AssetContextActions( + onTogglePin = viewModel::onTogglePin, + onAddToWallet = { id -> viewModel.onChangeVisibility(id, true) }, + ) + } + + Scene( + title = stringResource(id = R.string.assets_title), + onClose = { onAction(WalletSearchAction.Cancel) }, + ) { + LazyColumn(modifier = Modifier.fillMaxWidth()) { + if (pinned.isNotEmpty()) { + item { PinnedAssetsHeaderItem(AssetsGroupType.Pined) } + assetRows( + items = pinned, + onSelect = onAssetClick, + support = { assetPriceSupport(it.price) }, + titleBadge = ::getAssetBadge, + itemTrailing = { getBalanceInfo(it)() }, + longPressedAsset = longPressedAsset, + contextActions = contextActions, + ) + } + assetRows( + items = cappedAssets, + onSelect = onAssetClick, + support = { assetPriceSupport(it.price) }, + titleBadge = ::getAssetBadge, + itemTrailing = { getBalanceInfo(it)() }, + longPressedAsset = longPressedAsset, + contextActions = contextActions, + ) + } + } +} diff --git a/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/WalletSearchAction.kt b/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/WalletSearchAction.kt new file mode 100644 index 0000000000..6d2254175d --- /dev/null +++ b/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/WalletSearchAction.kt @@ -0,0 +1,13 @@ +package com.gemwallet.android.features.assets.views + +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetTag + +sealed interface WalletSearchAction { + data object AddAsset : WalletSearchAction + data object Cancel : WalletSearchAction + data object OpenPerpetuals : WalletSearchAction + data class OpenAsset(val assetId: AssetId) : WalletSearchAction + data class OpenPerpetual(val assetId: AssetId) : WalletSearchAction + data class ShowAllAssets(val query: String, val tag: AssetTag?) : WalletSearchAction +} diff --git a/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/WalletSearchScreen.kt b/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/WalletSearchScreen.kt new file mode 100644 index 0000000000..fed077941b --- /dev/null +++ b/android/features/assets/presents/src/main/kotlin/com/gemwallet/android/features/assets/views/WalletSearchScreen.kt @@ -0,0 +1,147 @@ +package com.gemwallet.android.features.assets.views + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.gemwallet.android.ext.toIdentifier +import com.gemwallet.android.features.asset_select.presents.views.AssetSelectScene +import com.gemwallet.android.features.asset_select.presents.views.RecentsSheetHost +import com.gemwallet.android.features.asset_select.presents.views.getAssetBadge +import com.gemwallet.android.features.asset_select.viewmodels.RecentsSheetViewModel +import com.gemwallet.android.features.assets.viewmodels.WalletSearchViewModel +import com.gemwallet.android.features.perpetual.views.components.PerpetualItem +import com.gemwallet.android.model.RecentType +import com.gemwallet.android.ui.R +import com.gemwallet.android.ui.components.SearchBar +import com.gemwallet.android.ui.components.list_item.AssetContextActions +import com.gemwallet.android.ui.components.list_item.AssetItemUIModel +import com.gemwallet.android.ui.components.list_item.SubheaderItem +import com.gemwallet.android.ui.components.list_item.assetPriceSupport +import com.gemwallet.android.ui.components.list_item.getBalanceInfo +import com.gemwallet.android.ui.components.list_item.listItem +import com.gemwallet.android.ui.components.list_item.property.itemsPositioned +import com.gemwallet.android.ui.models.ListPosition +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.PerpetualId +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun WalletSearchScreen( + onAction: (WalletSearchAction) -> Unit, + viewModel: WalletSearchViewModel = hiltViewModel(), + recentsViewModel: RecentsSheetViewModel = hiltViewModel(), +) { + val isAddAssetAvailable by viewModel.isAddAssetAvailable.collectAsStateWithLifecycle() + val state by viewModel.state.collectAsStateWithLifecycle() + val pinned by viewModel.pinned.collectAsStateWithLifecycle() + val previewAssets by viewModel.previewAssets.collectAsStateWithLifecycle() + val hasMoreAssets by viewModel.hasMoreAssets.collectAsStateWithLifecycle() + val recent by viewModel.recent.collectAsStateWithLifecycle() + val selectedTag by viewModel.selectedTag.collectAsStateWithLifecycle() + val previewPerpetuals by viewModel.previewPerpetuals.collectAsStateWithLifecycle() + val hasMorePerpetuals by viewModel.hasMorePerpetuals.collectAsStateWithLifecycle() + val pinnedPerpetuals by viewModel.pinnedPerpetuals.collectAsStateWithLifecycle() + val perpetualRecentIds by viewModel.perpetualRecentIds.collectAsStateWithLifecycle() + + val longPressedPerpetual = remember { mutableStateOf(null) } + + val onRecentClick: (AssetId) -> Unit = { assetId -> + onAction(if (assetId.toIdentifier() in perpetualRecentIds) WalletSearchAction.OpenPerpetual(assetId) else WalletSearchAction.OpenAsset(assetId)) + } + + val contextActions = remember(viewModel) { + AssetContextActions( + onTogglePin = viewModel::onPinAsset, + onAddToWallet = { id -> viewModel.onChangeVisibility(id, true) }, + ) + } + + val selectAsset: (AssetId) -> Unit = { id -> + viewModel.updateRecent(id, RecentType.Search) + onAction(WalletSearchAction.OpenAsset(id)) + } + + val onPerpetualSelect: (AssetId) -> Unit = { assetId -> + viewModel.onOpenPerpetual(assetId) + onAction(WalletSearchAction.OpenPerpetual(assetId)) + } + + val pinnedPerpetualRows: List<@Composable (ListPosition) -> Unit> = pinnedPerpetuals.map { item -> + @Composable { position: ListPosition -> + PerpetualItem( + item = item, + listPosition = position, + longPressState = longPressedPerpetual, + onTogglePin = viewModel::onTogglePerpetualPin, + onClick = onPerpetualSelect, + ) + } + } + + val perpetualsContent: (LazyListScope.() -> Unit)? = if (previewPerpetuals.isNotEmpty()) { + { + item { + SubheaderItem(R.string.perpetuals_title, if (hasMorePerpetuals) ({ onAction(WalletSearchAction.OpenPerpetuals) }) else null) + } + itemsPositioned(previewPerpetuals) { position, item -> + PerpetualItem( + item = item, + listPosition = position, + longPressState = longPressedPerpetual, + onTogglePin = viewModel::onTogglePerpetualPin, + onClick = onPerpetualSelect, + ) + } + } + } else { + null + } + + AssetSelectScene( + title = { + SearchBar( + query = viewModel.queryState, + modifier = Modifier.listItem(ListPosition.Single, paddingHorizontal = 0.dp), + ) + }, + titleBadge = ::getAssetBadge, + support = { assetPriceSupport(it.price) }, + query = viewModel.queryState, + selectedTag = selectedTag, + tags = viewModel.getTags(), + pinned = pinned, + popular = emptyList().toImmutableList(), + unpinned = previewAssets.toImmutableList(), + recent = recent, + state = state, + isAddAvailable = isAddAssetAvailable, + searchable = false, + onChainFilter = {}, + onBalanceFilter = {}, + onClearFilters = {}, + onCancel = { onAction(WalletSearchAction.Cancel) }, + onAddAsset = if (isAddAssetAvailable) ({ onAction(WalletSearchAction.AddAsset) }) else null, + onSelect = selectAsset, + onSelectRecent = onRecentClick, + onOpenRecentsSheet = { recentsViewModel.show(filters = viewModel.assetFilters()) }, + onTagSelect = viewModel::onTagSelect, + itemTrailing = { asset -> getBalanceInfo(asset)() }, + contextActions = contextActions, + pinnedPerpetualRows = pinnedPerpetualRows, + perpetualsContent = perpetualsContent, + assetsHeaderRes = R.string.assets_title, + onAssetsHeaderClick = if (hasMoreAssets) { + { onAction(WalletSearchAction.ShowAllAssets(viewModel.queryState.text.toString(), selectedTag)) } + } else { + null + }, + ) + + RecentsSheetHost(viewModel = recentsViewModel, onSelect = onRecentClick) +} diff --git a/android/features/assets/viewmodels/build.gradle.kts b/android/features/assets/viewmodels/build.gradle.kts index d258016feb..2bf3ab05b8 100644 --- a/android/features/assets/viewmodels/build.gradle.kts +++ b/android/features/assets/viewmodels/build.gradle.kts @@ -50,6 +50,8 @@ android { dependencies { api(project(":ui-models")) implementation(project(":ui")) + implementation(project(":data:repositories")) + implementation(project(":features:asset_select:viewmodels")) implementation(libs.hilt.android) ksp(libs.hilt.compiler) diff --git a/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/AssetsResultsViewModel.kt b/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/AssetsResultsViewModel.kt new file mode 100644 index 0000000000..ac7a3564f3 --- /dev/null +++ b/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/AssetsResultsViewModel.kt @@ -0,0 +1,59 @@ +package com.gemwallet.android.features.assets.viewmodels + +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.gemwallet.android.application.asset_select.coordinators.GetRecentAssets +import com.gemwallet.android.application.asset_select.coordinators.SearchSelectAssets +import com.gemwallet.android.application.asset_select.coordinators.SwitchAssetVisibility +import com.gemwallet.android.application.asset_select.coordinators.ToggleAssetPin +import com.gemwallet.android.application.asset_select.coordinators.UpdateRecentAsset +import com.gemwallet.android.application.session.coordinators.GetSession +import com.gemwallet.android.cases.tokens.SearchTokensCase +import com.gemwallet.android.features.asset_select.viewmodels.BaseAssetSelectViewModel +import com.gemwallet.android.features.asset_select.viewmodels.models.BaseSelectSearch +import com.gemwallet.android.ui.components.list_item.AssetItemUIModel +import com.gemwallet.android.ui.models.navigation.RouteArgument +import com.wallet.core.primitives.AssetTag +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltViewModel +class AssetsResultsViewModel @Inject constructor( + getSession: GetSession, + searchSelectAssets: SearchSelectAssets, + getRecentAssets: GetRecentAssets, + updateRecentAsset: UpdateRecentAsset, + switchAssetVisibility: SwitchAssetVisibility, + toggleAssetPin: ToggleAssetPin, + searchTokensCase: SearchTokensCase, + savedStateHandle: SavedStateHandle, +) : BaseAssetSelectViewModel( + getSession, + getRecentAssets, + updateRecentAsset, + switchAssetVisibility, + toggleAssetPin, + searchTokensCase, + BaseSelectSearch(searchSelectAssets), + remoteSearch = false, +) { + + init { + val tag = savedStateHandle.get(RouteArgument.Tag.key) + ?.let { value -> AssetTag.entries.firstOrNull { it.string == value } } + selectedTag.value = tag + queryState.setTextAndPlaceCursorAtEnd(savedStateHandle.get(RouteArgument.Query.key).orEmpty()) + } + + val cappedAssets: StateFlow> = combine(pinned, unpinned) { pinned, unpinned -> + unpinned.take((WalletSearchLimits.RESULTS - pinned.size).coerceAtLeast(0)) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) +} diff --git a/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/WalletSearchLimits.kt b/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/WalletSearchLimits.kt new file mode 100644 index 0000000000..95b8457ab0 --- /dev/null +++ b/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/WalletSearchLimits.kt @@ -0,0 +1,9 @@ +package com.gemwallet.android.features.assets.viewmodels + +object WalletSearchLimits { + const val ASSETS_INITIAL = 12 + const val ASSETS_TAG = 18 + const val ASSETS_SEARCH = 25 + const val PERPETUALS_PREVIEW = 3 + const val RESULTS = 100 +} diff --git a/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/WalletSearchViewModel.kt b/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/WalletSearchViewModel.kt new file mode 100644 index 0000000000..960b664e76 --- /dev/null +++ b/android/features/assets/viewmodels/src/main/kotlin/com/gemwallet/android/features/assets/viewmodels/WalletSearchViewModel.kt @@ -0,0 +1,135 @@ +package com.gemwallet.android.features.assets.viewmodels + +import androidx.lifecycle.viewModelScope +import com.gemwallet.android.application.asset_select.coordinators.GetRecentAssets +import com.gemwallet.android.application.asset_select.coordinators.SearchSelectAssets +import com.gemwallet.android.application.asset_select.coordinators.SwitchAssetVisibility +import com.gemwallet.android.application.asset_select.coordinators.ToggleAssetPin +import com.gemwallet.android.application.asset_select.coordinators.UpdateRecentAsset +import com.gemwallet.android.application.perpetual.coordinators.GetPerpetuals +import com.gemwallet.android.application.perpetual.coordinators.TogglePerpetualPin +import com.gemwallet.android.application.session.coordinators.GetSession +import com.gemwallet.android.cases.tokens.SearchTokensCase +import com.gemwallet.android.data.repositories.config.UserConfig +import com.gemwallet.android.data.repositories.config.showPerpetuals +import com.gemwallet.android.data.repositories.tokens.WalletSearch +import com.gemwallet.android.domains.perpetual.aggregates.PerpetualDataAggregate +import com.gemwallet.android.ext.toIdentifier +import com.gemwallet.android.features.asset_select.viewmodels.BaseAssetSelectViewModel +import com.gemwallet.android.features.asset_select.viewmodels.models.BaseSelectSearch +import com.gemwallet.android.features.asset_select.viewmodels.models.UIState +import com.gemwallet.android.model.RecentAssetsRequest +import com.gemwallet.android.model.RecentType +import com.gemwallet.android.ui.components.list_item.AssetItemUIModel +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetTag +import com.wallet.core.primitives.PerpetualId +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltViewModel +class WalletSearchViewModel @Inject constructor( + getSession: GetSession, + searchSelectAssets: SearchSelectAssets, + getRecentAssets: GetRecentAssets, + updateRecentAsset: UpdateRecentAsset, + switchAssetVisibility: SwitchAssetVisibility, + toggleAssetPin: ToggleAssetPin, + @WalletSearch searchTokensCase: SearchTokensCase, + getPerpetuals: GetPerpetuals, + userConfig: UserConfig, + private val togglePerpetualPin: TogglePerpetualPin, +) : BaseAssetSelectViewModel( + getSession, + getRecentAssets, + updateRecentAsset, + switchAssetVisibility, + toggleAssetPin, + searchTokensCase, + BaseSelectSearch(searchSelectAssets), +) { + + private val showPerpetuals = userConfig.showPerpetuals(getSession()) + + private val visiblePerpetuals = combine( + getPerpetuals.getPerpetuals(currentQuery.map { it.takeIf(String::isNotEmpty) }), + showPerpetuals, + selectedTag, + ) { items, show, tag -> + if (show && tag == null) items else emptyList() + } + .flowOn(Dispatchers.IO) + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val pinnedPerpetuals: StateFlow> = visiblePerpetuals + .map { items -> items.filter { it.isPinned } } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + private val perpetuals: StateFlow> = visiblePerpetuals + .map { items -> items.filter { !it.isPinned } } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val previewPerpetuals: StateFlow> = perpetuals + .map { items -> items.take(WalletSearchLimits.PERPETUALS_PREVIEW) } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val hasMorePerpetuals: StateFlow = visiblePerpetuals + .map { items -> items.size > WalletSearchLimits.PERPETUALS_PREVIEW } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + val perpetualRecentIds: StateFlow> = + getRecentAssets(RecentAssetsRequest(types = listOf(RecentType.Perpetual))) + .map { items -> items.mapTo(HashSet()) { it.asset.id.toIdentifier() } } + .flowOn(Dispatchers.IO) + .stateIn(viewModelScope, SharingStarted.Eagerly, emptySet()) + + val previewAssets: StateFlow> = combine( + unpinned, currentQuery, selectedTag, + ) { items, query, tag -> + items.take(assetsLimit(query, tag)) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val hasMoreAssets: StateFlow = combine( + pinned, unpinned, currentQuery, selectedTag, + ) { pinned, unpinned, query, tag -> + (pinned.size + unpinned.size) > assetsLimit(query, tag) + } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + val state: StateFlow = combine( + uiState, previewPerpetuals, pinnedPerpetuals, + ) { base, preview, pinnedPerps -> + if (preview.isNotEmpty() || pinnedPerps.isNotEmpty()) UIState.Idle else base + } + .stateIn(viewModelScope, SharingStarted.Eagerly, UIState.Idle) + + private fun assetsLimit(query: String, tag: AssetTag?): Int = when { + query.isNotEmpty() -> WalletSearchLimits.ASSETS_SEARCH + tag != null -> WalletSearchLimits.ASSETS_TAG + else -> WalletSearchLimits.ASSETS_INITIAL + } + + fun onPinAsset(assetId: AssetId) { + val willPin = (pinned.value + unpinned.value).firstOrNull { it.asset.id == assetId }?.metadata?.isPinned != true + onTogglePin(assetId) + if (willPin) onChangeVisibility(assetId, true) + } + + fun onTogglePerpetualPin(perpetualId: PerpetualId) { + togglePerpetualPin.togglePin(perpetualId) + } + + fun onOpenPerpetual(assetId: AssetId) { + updateRecent(assetId, RecentType.Perpetual) + } +} diff --git a/android/features/perpetual/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetual/viewmodels/PerpetualsPreviewViewModel.kt b/android/features/perpetual/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetual/viewmodels/PerpetualsPreviewViewModel.kt index 607ce8b9f8..4ae4528ff3 100644 --- a/android/features/perpetual/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetual/viewmodels/PerpetualsPreviewViewModel.kt +++ b/android/features/perpetual/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetual/viewmodels/PerpetualsPreviewViewModel.kt @@ -5,10 +5,9 @@ import androidx.lifecycle.viewModelScope import com.gemwallet.android.application.perpetual.coordinators.GetPerpetualPositions import com.gemwallet.android.application.session.coordinators.GetSession import com.gemwallet.android.data.repositories.config.UserConfig -import com.gemwallet.android.ext.hasPerpetualsSupport +import com.gemwallet.android.data.repositories.config.showPerpetuals import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -19,12 +18,8 @@ class PerpetualsPreviewViewModel @Inject constructor( getPositions: GetPerpetualPositions, ) : ViewModel() { - val showPerpetuals = combine( - getSession(), - userConfig.isPerpetualEnabled(), - ) { session, enabled -> - enabled && (session?.wallet?.hasPerpetualsSupport == true) - }.stateIn(viewModelScope, SharingStarted.Eagerly, false) + val showPerpetuals = userConfig.showPerpetuals(getSession()) + .stateIn(viewModelScope, SharingStarted.Eagerly, false) val positions = getPositions.getPerpetualPositions() .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) diff --git a/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/GemSearch.kt b/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/GemSearch.kt new file mode 100644 index 0000000000..786a9dbf9b --- /dev/null +++ b/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/GemSearch.kt @@ -0,0 +1,13 @@ +package com.gemwallet.android.application.assets.coordinators + +import com.wallet.core.primitives.AssetTag +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.SearchResponse + +interface GemSearch { + suspend fun search( + query: String, + chains: List = emptyList(), + tags: List = emptyList(), + ): SearchResponse +} diff --git a/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/SearchAssets.kt b/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/SearchAssets.kt index 5408c941d2..74aaed4e39 100644 --- a/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/SearchAssets.kt +++ b/android/gemcore/src/main/kotlin/com/gemwallet/android/application/assets/coordinators/SearchAssets.kt @@ -6,7 +6,7 @@ import com.wallet.core.primitives.AssetTag import com.wallet.core.primitives.Chain interface SearchAssets { - suspend fun search( + suspend fun searchAssets( query: String, chains: List = emptyList(), tags: List = emptyList(), diff --git a/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/ListPosition.kt b/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/ListPosition.kt index e39ce3ea71..b342393fe3 100644 --- a/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/ListPosition.kt +++ b/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/ListPosition.kt @@ -17,6 +17,4 @@ enum class ListPosition { else -> Middle } } -} - -fun List<*>.getListPosition(index: Int) = ListPosition.getPosition(index, size = size) \ No newline at end of file +} \ No newline at end of file diff --git a/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/navigation/RouteArgument.kt b/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/navigation/RouteArgument.kt index 13a6a6aba9..eade77965e 100644 --- a/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/navigation/RouteArgument.kt +++ b/android/ui-models/src/main/kotlin/com/gemwallet/android/ui/models/navigation/RouteArgument.kt @@ -10,7 +10,9 @@ enum class RouteArgument(val key: String) { NftAssetId("nftAssetId"), NftCollectionId("nftCollectionId"), Params("params"), + Query("query"), SwapItemType("swapItemType"), + Tag("tag"), ToAssetId("toAssetId"), TransactionId("transactionId"), Type("type"), diff --git a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/SubheaderItem.kt b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/SubheaderItem.kt index 80c3b4a2bd..dce0514381 100644 --- a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/SubheaderItem.kt +++ b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/SubheaderItem.kt @@ -42,8 +42,9 @@ fun SubheaderItem(title: String, modifier: Modifier = Modifier) { } @Composable -fun SubheaderItem(@StringRes title: Int, onClick: () -> Unit) { - SubheaderItem(stringResource(title), onClick) +fun SubheaderItem(@StringRes title: Int, onClick: (() -> Unit)?) { + val text = stringResource(title) + if (onClick == null) SubheaderItem(text) else SubheaderItem(text, onClick) } @Composable @@ -52,8 +53,7 @@ fun SubheaderItem(title: String, onClick: () -> Unit) { Row( modifier = Modifier .clip(RoundedCornerShape(paddingHalfSmall)) - .clickable(onClick = onClick) - .padding(paddingHalfSmall), + .clickable(onClick = onClick), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/itemsPositioned.kt b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/itemsPositioned.kt index e08c3c4267..dc96565aca 100644 --- a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/itemsPositioned.kt +++ b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/itemsPositioned.kt @@ -5,16 +5,17 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import com.gemwallet.android.ui.models.ListPosition -import com.gemwallet.android.ui.models.getListPosition inline fun LazyListScope.itemsPositioned( items: List, + indexOffset: Int = 0, + totalCount: Int = items.size, noinline key: ((index: Int, item: T) -> Any)? = null, crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null }, crossinline itemContent: @Composable LazyItemScope.(position: ListPosition, item: T) -> Unit, ) { itemsIndexed(items, key, contentType) { index, item -> - val position = items.getListPosition(index) + val position = ListPosition.getPosition(indexOffset + index, totalCount) itemContent(position, item) } } \ No newline at end of file