From af6944e8f4e469cf93f8920eff8c25bde8381b57 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 18 Aug 2026 16:07:28 +0800 Subject: [PATCH 1/3] feat(market): sort trending contracts by score --- .../one.mixin.android.db.PerpsDatabase/8.json | 489 ++++++++++++++++++ .../mixin/android/db/PerpsMigrationTest.kt | 23 + .../android/api/response/perps/PerpsMarket.kt | 4 + .../one/mixin/android/db/PerpsDatabase.kt | 12 +- .../mixin/android/db/perps/PerpsMarketDao.kt | 26 +- .../repository/PerpsMarketRepository.kt | 26 +- .../ui/home/web3/market/MarketPageModels.kt | 3 +- .../android/db/perps/PerpsMarketDaoTest.kt | 101 ++++ .../home/web3/market/MarketPageModelsTest.kt | 12 +- 9 files changed, 674 insertions(+), 22 deletions(-) create mode 100644 app/schemas/one.mixin.android.db.PerpsDatabase/8.json create mode 100644 app/src/test/java/one/mixin/android/db/perps/PerpsMarketDaoTest.kt diff --git a/app/schemas/one.mixin.android.db.PerpsDatabase/8.json b/app/schemas/one.mixin.android.db.PerpsDatabase/8.json new file mode 100644 index 0000000000..83d380752f --- /dev/null +++ b/app/schemas/one.mixin.android.db.PerpsDatabase/8.json @@ -0,0 +1,489 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "37cac3de0d8a5f685d464158ffc1f3d9", + "entities": [ + { + "tableName": "positions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position_id` TEXT NOT NULL, `market_id` TEXT NOT NULL, `side` TEXT NOT NULL, `quantity` TEXT NOT NULL, `entry_price` TEXT NOT NULL, `margin` TEXT NOT NULL, `leverage` INTEGER NOT NULL, `state` TEXT NOT NULL, `mark_price` TEXT NOT NULL, `unrealized_pnl` TEXT NOT NULL, `roe` TEXT NOT NULL, `settle_asset_id` TEXT NOT NULL, `open_pay_amount` TEXT NOT NULL, `open_pay_asset_id` TEXT NOT NULL, `take_profit_price` TEXT, `stop_loss_price` TEXT, `liquidation_price` TEXT, `bot_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`position_id`))", + "fields": [ + { + "fieldPath": "positionId", + "columnName": "position_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entryPrice", + "columnName": "entry_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "margin", + "columnName": "margin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markPrice", + "columnName": "mark_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unrealizedPnl", + "columnName": "unrealized_pnl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roe", + "columnName": "roe", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "settleAssetId", + "columnName": "settle_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openPayAmount", + "columnName": "open_pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openPayAssetId", + "columnName": "open_pay_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "takeProfitPrice", + "columnName": "take_profit_price", + "affinity": "TEXT" + }, + { + "fieldPath": "stopLossPrice", + "columnName": "stop_loss_price", + "affinity": "TEXT" + }, + { + "fieldPath": "liquidationPrice", + "columnName": "liquidation_price", + "affinity": "TEXT" + }, + { + "fieldPath": "botId", + "columnName": "bot_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "position_id" + ] + } + }, + { + "tableName": "perps_orders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order_id` TEXT NOT NULL, `position_id` TEXT NOT NULL, `market_id` TEXT NOT NULL, `side` TEXT NOT NULL, `order_type` TEXT NOT NULL, `status` TEXT NOT NULL, `leverage` INTEGER NOT NULL, `quantity` TEXT NOT NULL, `pay_amount` TEXT NOT NULL, `entry_price` TEXT NOT NULL, `close_price` TEXT NOT NULL, `realized_pnl` TEXT NOT NULL, `roe` TEXT NOT NULL, `close_reason` TEXT, `trigger_price` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`order_id`))", + "fields": [ + { + "fieldPath": "orderId", + "columnName": "order_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionId", + "columnName": "position_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderType", + "columnName": "order_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payAmount", + "columnName": "pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entryPrice", + "columnName": "entry_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "closePrice", + "columnName": "close_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realizedPnl", + "columnName": "realized_pnl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roe", + "columnName": "roe", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "closeReason", + "columnName": "close_reason", + "affinity": "TEXT" + }, + { + "fieldPath": "triggerPrice", + "columnName": "trigger_price", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "order_id" + ] + } + }, + { + "tableName": "markets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `display_symbol` TEXT NOT NULL, `token_symbol` TEXT NOT NULL, `quote_symbol` TEXT NOT NULL, `mark_price` TEXT NOT NULL, `price_scale` INTEGER NOT NULL DEFAULT 2, `leverage` INTEGER NOT NULL, `icon_url` TEXT NOT NULL, `category` TEXT NOT NULL, `tags` TEXT NOT NULL, `funding_rate` TEXT NOT NULL, `min_amount` TEXT NOT NULL, `max_amount` TEXT NOT NULL, `last` TEXT NOT NULL, `volume` TEXT NOT NULL, `trade_volume_score_1d` INTEGER NOT NULL DEFAULT 0, `high` TEXT NOT NULL, `low` TEXT NOT NULL, `open` TEXT NOT NULL, `change` TEXT NOT NULL, `bid_price` TEXT NOT NULL, `ask_price` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `descriptions` TEXT, PRIMARY KEY(`market_id`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displaySymbol", + "columnName": "display_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tokenSymbol", + "columnName": "token_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quoteSymbol", + "columnName": "quote_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markPrice", + "columnName": "mark_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priceScale", + "columnName": "price_scale", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "2" + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fundingRate", + "columnName": "funding_rate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "minAmount", + "columnName": "min_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxAmount", + "columnName": "max_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "last", + "columnName": "last", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tradeVolumeScore1D", + "columnName": "trade_volume_score_1d", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "high", + "columnName": "high", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "low", + "columnName": "low", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "open", + "columnName": "open", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "change", + "columnName": "change", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bidPrice", + "columnName": "bid_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "askPrice", + "columnName": "ask_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id" + ] + } + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `is_favored` INTEGER NOT NULL, `created_at` TEXT NOT NULL, PRIMARY KEY(`market_id`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavored", + "columnName": "is_favored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id" + ] + } + }, + { + "tableName": "market_categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `category` INTEGER NOT NULL, PRIMARY KEY(`market_id`, `category`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id", + "category" + ] + }, + "indices": [ + { + "name": "index_market_categories_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_market_categories_category` ON `${TABLE_NAME}` (`category`)" + } + ] + } + ], + "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, '37cac3de0d8a5f685d464158ffc1f3d9')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/one/mixin/android/db/PerpsMigrationTest.kt b/app/src/androidTest/java/one/mixin/android/db/PerpsMigrationTest.kt index 4f7de75a27..a4fbc804bb 100644 --- a/app/src/androidTest/java/one/mixin/android/db/PerpsMigrationTest.kt +++ b/app/src/androidTest/java/one/mixin/android/db/PerpsMigrationTest.kt @@ -190,6 +190,29 @@ class PerpsMigrationTest { } } + @Test + fun migrate_7_8_addsTradeVolumeScore() { + migrationTestHelper.createDatabase(Constants.DataBase.PERPS_DB_NAME, 7).apply { + insertMarket() + close() + } + + val migratedDb = + migrationTestHelper.runMigrationsAndValidate( + Constants.DataBase.PERPS_DB_NAME, + 8, + true, + PerpsDatabase.MIGRATION_7_8, + ) + + migratedDb.query( + "SELECT trade_volume_score_1d FROM markets WHERE market_id = 'market-1'", + ).use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals(0, cursor.getInt(0)) + } + } + private fun SupportSQLiteDatabase.insertMarket() { execSQL( """ diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsMarket.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsMarket.kt index d887956cf5..f152073329 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsMarket.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsMarket.kt @@ -71,6 +71,10 @@ data class PerpsMarket( @ColumnInfo(name = "volume") val volume: String, + @SerializedName("trade_volume_score_1d") + @ColumnInfo(name = "trade_volume_score_1d", defaultValue = "0") + val tradeVolumeScore1D: Int = 0, + @SerializedName("high") @ColumnInfo(name = "high") val high: String, diff --git a/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt b/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt index 627e1421ce..898a0f0426 100644 --- a/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt +++ b/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt @@ -34,7 +34,7 @@ import kotlin.math.min PerpsFavorite::class, PerpsMarketCategoryRelation::class, ], - version = 7, + version = 8, ) abstract class PerpsDatabase : RoomDatabase() { companion object { @@ -106,6 +106,12 @@ abstract class PerpsDatabase : RoomDatabase() { db.execSQL("CREATE INDEX IF NOT EXISTS `index_market_categories_category` ON `market_categories` (`category`)") } } + val MIGRATION_7_8 = + object : Migration(7, 8) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `markets` ADD COLUMN `trade_volume_score_1d` INTEGER NOT NULL DEFAULT 0") + } + } @Suppress("DEPRECATION") fun getDatabase( @@ -131,7 +137,7 @@ abstract class PerpsDatabase : RoomDatabase() { listOf( object : MixinCorruptionCallback { override fun onCorruption(database: SupportSQLiteDatabase) { - val e = IllegalStateException("Perps database is corrupted, current DB version: 7") + val e = IllegalStateException("Perps database is corrupted, current DB version: 8") reportException(e) } }, @@ -144,7 +150,7 @@ abstract class PerpsDatabase : RoomDatabase() { db.execSQL("PRAGMA synchronous = NORMAL") } }, - ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7) + ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8) .fallbackToDestructiveMigration() .enableMultiInstanceInvalidation() .setQueryExecutor( diff --git a/app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt b/app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt index 1c05946657..3b47ca66ed 100644 --- a/app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt +++ b/app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import kotlinx.coroutines.flow.Flow import one.mixin.android.api.response.perps.PerpsMarket import one.mixin.android.db.BaseDao @@ -16,12 +17,32 @@ interface PerpsMarketDao : BaseDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(markets: List) + @Query("SELECT * FROM markets WHERE market_id IN (:marketIds)") + suspend fun getMarkets(marketIds: List): List + + @Transaction + suspend fun upsertPreservingTradeVolumeScores(markets: List): List { + if (markets.isEmpty()) return emptyList() + val existingScores = + getMarkets(markets.map(PerpsMarket::marketId)) + .associate { market -> market.marketId to market.tradeVolumeScore1D } + val mergedMarkets = + markets.map { market -> + market.copy( + tradeVolumeScore1D = existingScores[market.marketId] ?: 0, + ) + } + upsertList(mergedMarkets) + return mergedMarkets + } + @Query( """ SELECT * FROM markets WHERE CAST(volume AS REAL) > 0 - ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC + ORDER BY trade_volume_score_1d DESC, CAST(volume AS REAL) DESC, + token_symbol COLLATE NOCASE ASC, market_id ASC """, ) suspend fun getAllMarkets(): List @@ -31,7 +52,8 @@ interface PerpsMarketDao : BaseDao { SELECT * FROM markets WHERE CAST(volume AS REAL) > 0 - ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC + ORDER BY trade_volume_score_1d DESC, CAST(volume AS REAL) DESC, + token_symbol COLLATE NOCASE ASC, market_id ASC """, ) fun observeAllMarkets(): Flow> diff --git a/app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt b/app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt index d6027d27c6..4e3cfff2bb 100644 --- a/app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt +++ b/app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt @@ -50,34 +50,40 @@ class PerpsMarketRepository suspend fun syncFavoriteMarkets(): List? { val markets = fetchMarkets(CATEGORY_FAVORITE) ?: return null - database.withTransaction { - marketDao.upsertList(markets) + return database.withTransaction { + val mergedMarkets = marketDao.upsertPreservingTradeVolumeScores(markets) favoriteDao.replaceAll( - marketIds = markets.map(PerpsMarket::marketId), + marketIds = mergedMarkets.map(PerpsMarket::marketId), createdAt = nowInUtc(), ) + mergedMarkets } - return markets } suspend fun syncCategory(category: MarketCategory): List? { val markets = fetchMarkets(category.apiValue) ?: return null - database.withTransaction { - marketDao.upsertList(markets) + return database.withTransaction { + val mergedMarkets = + if (category == MarketCategory.TRENDING) { + marketDao.upsertList(markets) + markets + } else { + marketDao.upsertPreservingTradeVolumeScores(markets) + } categoryDao.replaceCategory( category = category.value, - marketIds = markets.map(PerpsMarket::marketId), + marketIds = mergedMarkets.map(PerpsMarket::marketId), ) + mergedMarkets } - return markets } suspend fun refreshMarket(marketId: String): PerpsMarket? = requestRouteAPI( invokeNetwork = { routeService.getPerpsMarket(marketId) }, successBlock = { response -> - response.data?.withDefaults()?.also { market -> - marketDao.upsertSuspend(market) + response.data?.withDefaults()?.let { market -> + marketDao.upsertPreservingTradeVolumeScores(listOf(market)).single() } }, failureBlock = { true }, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt index 0324d55971..93db2ba001 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt @@ -96,8 +96,6 @@ fun defaultMarketSortState( MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) topTab == MarketTopTab.CRYPTO && subTab == MarketSubTab.TRENDING -> MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) - topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.TRENDING -> - MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.MEME -> MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.INDICES -> @@ -274,6 +272,7 @@ object MarketPageMapper { subTab: MarketSubTab, ): List = when (subTab) { + MarketSubTab.TRENDING -> markets.sortedByDescending(PerpsMarket::tradeVolumeScore1D) MarketSubTab.TOP_GAINERS -> markets.sortedByDescending { it.changePercentValue() ?: BigDecimal.ZERO } MarketSubTab.TOP_LOSERS -> markets.sortedBy { it.changePercentValue() ?: BigDecimal.ZERO } MarketSubTab.INDICES -> markets.filterByCategory(PerpsMarketCategoryKey.INDICES) diff --git a/app/src/test/java/one/mixin/android/db/perps/PerpsMarketDaoTest.kt b/app/src/test/java/one/mixin/android/db/perps/PerpsMarketDaoTest.kt new file mode 100644 index 0000000000..f1df54fdee --- /dev/null +++ b/app/src/test/java/one/mixin/android/db/perps/PerpsMarketDaoTest.kt @@ -0,0 +1,101 @@ +package one.mixin.android.db.perps + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.db.PerpsDatabase +import org.junit.Assert.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PerpsMarketDaoTest { + private lateinit var database: PerpsDatabase + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + database = + Room.inMemoryDatabaseBuilder(context, PerpsDatabase::class.java) + .allowMainThreadQueries() + .build() + } + + @After + fun tearDown() { + database.close() + } + + @Test + fun marketsOrderByTradeVolumeScoreBeforeVolume() = + runBlocking { + database.perpsMarketDao().upsertList( + listOf( + market("highest-volume", volume = "1000", score = 10), + market("highest-score", volume = "1", score = 30), + market("middle-score", volume = "10", score = 20), + ), + ) + + val result = database.perpsMarketDao().observeAllMarkets().first() + + assertEquals( + listOf("highest-score", "middle-score", "highest-volume"), + result.map(PerpsMarket::marketId), + ) + } + + @Test + fun nonTrendingRefreshPreservesExistingTradeVolumeScore() = + runBlocking { + val cached = market("cached", volume = "10", score = 30) + database.perpsMarketDao().upsertSuspend(cached) + + val merged = + database.perpsMarketDao().upsertPreservingTradeVolumeScores( + listOf( + cached.copy(last = "2", tradeVolumeScore1D = 0), + market("uncached", volume = "20", score = 50), + ), + ) + + assertEquals(30, merged.first { it.marketId == "cached" }.tradeVolumeScore1D) + assertEquals("2", database.perpsMarketDao().getMarket("cached")?.last) + assertEquals(30, database.perpsMarketDao().getMarket("cached")?.tradeVolumeScore1D) + assertEquals(0, database.perpsMarketDao().getMarket("uncached")?.tradeVolumeScore1D) + } + + private fun market( + marketId: String, + volume: String, + score: Int, + ) = PerpsMarket( + marketId = marketId, + displaySymbol = marketId, + tokenSymbol = marketId, + quoteSymbol = "USD", + markPrice = "1", + leverage = 10, + iconUrl = "", + fundingRate = "0", + minAmount = "0", + maxAmount = "0", + last = "1", + volume = volume, + tradeVolumeScore1D = score, + high = "1", + low = "1", + open = "1", + change = "0", + bidPrice = "1", + askPrice = "1", + createdAt = "", + updatedAt = "", + ) +} diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt index 3e666c91f2..85b6ffda53 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt @@ -197,12 +197,12 @@ class MarketPageModelsTest { } @Test - fun perpetualTrendingPreservesApiRankOrder() { + fun perpetualTrendingSortsByTradeVolumeScore() { val markets = listOf( - perpsMarket(marketId = "first"), - perpsMarket(marketId = "second"), - perpsMarket(marketId = "third"), + perpsMarket(marketId = "second", tradeVolumeScore1D = 20), + perpsMarket(marketId = "third", tradeVolumeScore1D = 10), + perpsMarket(marketId = "first", tradeVolumeScore1D = 30), ) val trending = MarketPageMapper.perpetualMarkets(markets, MarketSubTab.TRENDING) @@ -326,7 +326,7 @@ class MarketPageModelsTest { defaultMarketSortState(MarketTopTab.CRYPTO, MarketSubTab.ALL), ) assertEquals( - MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING), + MarketSortState(), defaultMarketSortState(MarketTopTab.PERPETUAL, MarketSubTab.TRENDING), ) assertEquals( @@ -487,6 +487,7 @@ class MarketPageModelsTest { marketId: String, change: String = "0", category: String = "", + tradeVolumeScore1D: Int = 0, ) = PerpsMarket( marketId = marketId, displaySymbol = marketId, @@ -501,6 +502,7 @@ class MarketPageModelsTest { maxAmount = "0", last = "1", volume = "10", + tradeVolumeScore1D = tradeVolumeScore1D, high = "1", low = "1", open = "1", From aa30c73b5d27f861de0cbaef12d558e9bfc62e9e Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 18 Aug 2026 16:14:47 +0800 Subject: [PATCH 2/3] fix(market): sort all picker markets by score --- .../perps/PerpsMarketListBottomSheetModels.kt | 36 ++++++++++++------- .../PerpsMarketListBottomSheetModelsTest.kt | 17 +++++++++ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt index ed12b7034c..d4f13fabd4 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt @@ -32,7 +32,7 @@ internal enum class PerpsMarketCategory(val categoryKey: PerpsMarketCategoryKey? internal data class PerpsMarketListUiState( val query: String = "", val selectedCategory: PerpsMarketCategory = PerpsMarketCategory.ALL, - val sortState: MarketSortState = defaultPerpsMarketSortState(), + val sortState: MarketSortState = defaultPerpsMarketSortState(PerpsMarketCategory.ALL), val markets: List = emptyList(), val favoriteMarketIds: Set = emptySet(), val favoriteOverrides: Map = emptyMap(), @@ -76,7 +76,7 @@ internal data class PerpsMarketListUiState( if (selectedCategory == category) return this return copy( selectedCategory = category, - sortState = defaultPerpsMarketSortState(), + sortState = defaultPerpsMarketSortState(category), scrollToTopRequest = scrollToTopRequest + 1, ).normalizeRecommendationSelection() } @@ -176,8 +176,14 @@ internal data class PerpsMarketListUiState( } private fun List.sortedByCurrentState(): List { - val column = sortState.column ?: return this - if (sortState.direction == MarketSortDirection.DEFAULT) return this + val column = sortState.column + if (column == null || sortState.direction == MarketSortDirection.DEFAULT) { + return if (selectedCategory == PerpsMarketCategory.ALL) { + sortedByDescending(PerpsMarket::tradeVolumeScore1D) + } else { + this + } + } val comparator = compareBy { market -> when (column) { @@ -222,21 +228,27 @@ internal data class PerpsMarketListUiState( initialCategory: String?, initialSort: MarketSort?, quoteColorReversed: Boolean, - ): PerpsMarketListUiState = - PerpsMarketListUiState( - selectedCategory = PerpsMarketCategory.fromInitialCategory(initialCategory), - sortState = initialSort.toPerpsMarketSortState(), + ): PerpsMarketListUiState { + val selectedCategory = PerpsMarketCategory.fromInitialCategory(initialCategory) + return PerpsMarketListUiState( + selectedCategory = selectedCategory, + sortState = initialSort.toPerpsMarketSortState(selectedCategory), quoteColorReversed = quoteColorReversed, ) + } } } -internal fun defaultPerpsMarketSortState() = - MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) +internal fun defaultPerpsMarketSortState(category: PerpsMarketCategory) = + if (category == PerpsMarketCategory.ALL) { + MarketSortState() + } else { + MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) + } -internal fun MarketSort?.toPerpsMarketSortState(): MarketSortState = +internal fun MarketSort?.toPerpsMarketSortState(category: PerpsMarketCategory): MarketSortState = when (this) { - null, MarketSort.RANK_DESCENDING -> defaultPerpsMarketSortState() + null, MarketSort.RANK_DESCENDING -> defaultPerpsMarketSortState(category) MarketSort.RANK_ASCENDING -> MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.ASCENDING) MarketSort.PRICE_ASCENDING -> MarketSortState(MarketSortColumn.PRICE, MarketSortDirection.ASCENDING) MarketSort.PRICE_DESCENDING -> MarketSortState(MarketSortColumn.PRICE, MarketSortDirection.DESCENDING) diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt index 30749bab42..0594dc4618 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt @@ -35,6 +35,21 @@ class PerpsMarketListBottomSheetModelsTest { assertEquals(setOf("stock", "stocks"), visibleMarkets.mapTo(mutableSetOf()) { it.marketId }) } + @Test + fun allMarketsSortByTradeVolumeScore() { + val visibleMarkets = + PerpsMarketListUiState( + markets = + listOf( + perpsMarket("second", "crypto", tradeVolumeScore1D = 20), + perpsMarket("third", "stocks", tradeVolumeScore1D = 10), + perpsMarket("first", "crypto", tradeVolumeScore1D = 30), + ), + ).visibleMarkets + + assertEquals(listOf("first", "second", "third"), visibleMarkets.map { it.marketId }) + } + @Test fun cachesDerivedMarketListsForEachStateInstance() { val state = @@ -91,6 +106,7 @@ class PerpsMarketListBottomSheetModelsTest { private fun perpsMarket( marketId: String, category: String, + tradeVolumeScore1D: Int = 0, ) = PerpsMarket( marketId = marketId, displaySymbol = marketId, @@ -105,6 +121,7 @@ class PerpsMarketListBottomSheetModelsTest { maxAmount = "0", last = "1", volume = "10", + tradeVolumeScore1D = tradeVolumeScore1D, high = "1", low = "1", open = "1", From 2f7733a49faa0082a79d908dac35a528956e2b24 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 19 Aug 2026 22:55:30 +0800 Subject: [PATCH 3/3] feat(market): cycle headers back to score sort Match iOS so Volume/Price/Change can return to score ordering on perpetual trending and the all-markets picker. --- .../ui/home/web3/market/MarketAnalytics.kt | 1 + .../ui/home/web3/market/MarketPageModels.kt | 29 +++++++++++++-- .../home/web3/market/MarketPageViewModel.kt | 8 +++-- .../perps/PerpsMarketListBottomSheetModels.kt | 13 ++++--- .../util/analytics/AnalyticsTracker.kt | 1 + .../home/web3/market/MarketPageModelsTest.kt | 36 ++++++++++++++++++- .../PerpsMarketListBottomSheetModelsTest.kt | 29 ++++++++++++++- 7 files changed, 107 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketAnalytics.kt b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketAnalytics.kt index 09221c2bfb..3b866c5a77 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketAnalytics.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketAnalytics.kt @@ -54,4 +54,5 @@ internal fun MarketPageUiState.analyticsSortField(column: MarketSortColumn): Str MarketPriceChangePeriod.TWENTY_FOUR_HOURS -> AnalyticsTracker.MarketsSortField.TWENTY_FOUR_HOURS MarketPriceChangePeriod.SEVEN_DAYS -> AnalyticsTracker.MarketsSortField.SEVEN_DAYS } + MarketSortColumn.SCORE -> AnalyticsTracker.MarketsSortField.SCORE } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt index 93db2ba001..871a758753 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt @@ -53,6 +53,7 @@ enum class MarketSortColumn { VOLUME, PRICE, CHANGE, + SCORE, } enum class MarketSortDirection { @@ -65,14 +66,22 @@ data class MarketSortState( val column: MarketSortColumn? = null, val direction: MarketSortDirection = MarketSortDirection.DEFAULT, ) { - fun next(selectedColumn: MarketSortColumn): MarketSortState { + fun next( + selectedColumn: MarketSortColumn, + isScoreOrderingAvailable: Boolean = false, + ): MarketSortState { if (column != selectedColumn) { return MarketSortState(selectedColumn, MarketSortDirection.DESCENDING) } return when (direction) { MarketSortDirection.DEFAULT -> MarketSortState(selectedColumn, MarketSortDirection.DESCENDING) MarketSortDirection.DESCENDING -> MarketSortState(selectedColumn, MarketSortDirection.ASCENDING) - MarketSortDirection.ASCENDING -> MarketSortState() + MarketSortDirection.ASCENDING -> + if (isScoreOrderingAvailable) { + scoreMarketSortState() + } else { + MarketSortState() + } } } } @@ -96,6 +105,7 @@ fun defaultMarketSortState( MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) topTab == MarketTopTab.CRYPTO && subTab == MarketSubTab.TRENDING -> MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) + isScoreOrderingAvailable(topTab, subTab) -> scoreMarketSortState() topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.MEME -> MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.INDICES -> @@ -107,6 +117,14 @@ fun defaultMarketSortState( else -> MarketSortState() } +internal fun scoreMarketSortState() = + MarketSortState(MarketSortColumn.SCORE, MarketSortDirection.DESCENDING) + +internal fun isScoreOrderingAvailable( + topTab: MarketTopTab, + subTab: MarketSubTab?, +): Boolean = topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.TRENDING + data class MarketDisplaySettings( val quoteColorReversed: Boolean = false, val priceChangePeriod: MarketPriceChangePeriod = MarketPriceChangePeriod.SEVEN_DAYS, @@ -337,6 +355,7 @@ object MarketPageMapper { MarketSortColumn.VOLUME -> entry.volume(useMarketCapForSpot) MarketSortColumn.PRICE -> entry.price() MarketSortColumn.CHANGE -> entry.changePercent(period) + MarketSortColumn.SCORE -> entry.score() } ?: BigDecimal.ZERO } return if (sortState.direction == MarketSortDirection.ASCENDING) { @@ -385,3 +404,9 @@ fun MarketListEntry.changePercent(period: MarketPriceChangePeriod): BigDecimal? is MarketListEntry.Spot -> market.changePercent(period) is MarketListEntry.Perpetual -> market.changePercentValue() } + +fun MarketListEntry.score(): BigDecimal? = + when (this) { + is MarketListEntry.Spot -> null + is MarketListEntry.Perpetual -> market.tradeVolumeScore1D.toBigDecimal() + } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt index 12eb07e246..254dd22a46 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt @@ -115,7 +115,11 @@ class MarketPageViewModel fun updateSort(column: MarketSortColumn) { val state = _uiState.value - val sortState = state.sortState.next(column) + val sortState = + state.sortState.next( + column, + isScoreOrderingAvailable(state.selectedTopTab, state.selectedSubTab), + ) _uiState.value = state.copy(sortState = sortState) rebuildEntries() sortState.direction.analyticsValue()?.let { direction -> @@ -123,7 +127,7 @@ class MarketPageViewModel sortDirection = direction, primaryTab = state.selectedTopTab.analyticsValue(), secondaryTab = state.selectedSubTab.analyticsValue(), - sortField = state.analyticsSortField(column), + sortField = state.analyticsSortField(sortState.column ?: column), ) } } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt index d4f13fabd4..902b840002 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt @@ -6,6 +6,7 @@ import one.mixin.android.ui.home.web3.market.MarketSortDirection import one.mixin.android.ui.home.web3.market.MarketSortState import one.mixin.android.ui.home.web3.market.PerpsMarketCategoryKey import one.mixin.android.ui.home.web3.market.changePercentValue +import one.mixin.android.ui.home.web3.market.scoreMarketSortState import one.mixin.android.ui.home.web3.widget.MarketSort import java.math.BigDecimal @@ -83,7 +84,7 @@ internal data class PerpsMarketListUiState( fun selectSort(column: MarketSortColumn): PerpsMarketListUiState = copy( - sortState = sortState.next(column), + sortState = sortState.next(column, selectedCategory.isScoreOrderingAvailable), scrollToTopRequest = scrollToTopRequest + 1, ).normalizeRecommendationSelection() @@ -178,7 +179,7 @@ internal data class PerpsMarketListUiState( private fun List.sortedByCurrentState(): List { val column = sortState.column if (column == null || sortState.direction == MarketSortDirection.DEFAULT) { - return if (selectedCategory == PerpsMarketCategory.ALL) { + return if (selectedCategory.isScoreOrderingAvailable) { sortedByDescending(PerpsMarket::tradeVolumeScore1D) } else { this @@ -190,6 +191,7 @@ internal data class PerpsMarketListUiState( MarketSortColumn.VOLUME -> market.volume.toBigDecimalOrNull() MarketSortColumn.PRICE -> market.last.toBigDecimalOrNull() MarketSortColumn.CHANGE -> market.changePercentValue() + MarketSortColumn.SCORE -> market.tradeVolumeScore1D.toBigDecimal() } ?: BigDecimal.ZERO } return if (sortState.direction == MarketSortDirection.ASCENDING) { @@ -240,12 +242,15 @@ internal data class PerpsMarketListUiState( } internal fun defaultPerpsMarketSortState(category: PerpsMarketCategory) = - if (category == PerpsMarketCategory.ALL) { - MarketSortState() + if (category.isScoreOrderingAvailable) { + scoreMarketSortState() } else { MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING) } +private val PerpsMarketCategory.isScoreOrderingAvailable: Boolean + get() = this == PerpsMarketCategory.ALL + internal fun MarketSort?.toPerpsMarketSortState(category: PerpsMarketCategory): MarketSortState = when (this) { null, MarketSort.RANK_DESCENDING -> defaultPerpsMarketSortState(category) diff --git a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt index a0f8a35d6a..576119aef3 100644 --- a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt +++ b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt @@ -608,6 +608,7 @@ object AnalyticsTracker { const val TWENTY_FOUR_HOURS = "24h" const val SEVEN_DAYS = "7d" const val MARKET_CAP = "market_cap" + const val SCORE = "score" } object MarketDetailSource { diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt index 85b6ffda53..42545739d5 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/market/MarketPageModelsTest.kt @@ -310,6 +310,23 @@ class MarketPageModelsTest { assertEquals(MarketSortState(), reset) } + @Test + fun sortHeaderCyclesBackToScoreWhenAvailable() { + val descending = scoreMarketSortState().next(MarketSortColumn.VOLUME, isScoreOrderingAvailable = true) + val ascending = descending.next(MarketSortColumn.VOLUME, isScoreOrderingAvailable = true) + val score = + ascending + .next(MarketSortColumn.PRICE, isScoreOrderingAvailable = true) + .next(MarketSortColumn.PRICE, isScoreOrderingAvailable = true) + .next(MarketSortColumn.PRICE, isScoreOrderingAvailable = true) + + assertEquals(MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING), descending) + assertEquals(MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.ASCENDING), ascending) + assertEquals(scoreMarketSortState(), score) + assertTrue(isScoreOrderingAvailable(MarketTopTab.PERPETUAL, MarketSubTab.TRENDING)) + assertTrue(!isScoreOrderingAvailable(MarketTopTab.CRYPTO, MarketSubTab.TRENDING)) + } + @Test fun defaultVolumeSortMatchesMarketCategory() { listOf( @@ -326,7 +343,7 @@ class MarketPageModelsTest { defaultMarketSortState(MarketTopTab.CRYPTO, MarketSubTab.ALL), ) assertEquals( - MarketSortState(), + scoreMarketSortState(), defaultMarketSortState(MarketTopTab.PERPETUAL, MarketSubTab.TRENDING), ) assertEquals( @@ -415,6 +432,23 @@ class MarketPageModelsTest { assertEquals(listOf("spot:higher-volume", "spot:lower-volume"), result.map { it.stableId }) } + @Test + fun applySortByScoreOrdersPerpetualMarkets() { + val second = MarketListEntry.Perpetual(perpsMarket(marketId = "second", tradeVolumeScore1D = 20), false) + val third = MarketListEntry.Perpetual(perpsMarket(marketId = "third", tradeVolumeScore1D = 10), false) + val first = MarketListEntry.Perpetual(perpsMarket(marketId = "first", tradeVolumeScore1D = 30), false) + + val result = + MarketPageMapper.applySort( + entries = listOf(second, third, first), + sortState = scoreMarketSortState(), + period = MarketPriceChangePeriod.TWENTY_FOUR_HOURS, + useMarketCapForSpot = false, + ) + + assertEquals(listOf("perpetual:first", "perpetual:second", "perpetual:third"), result.map { it.stableId }) + } + @Test fun initialLoadingDoesNotFlashSpinnerBeforeLocalDatabaseResult() { val state = MarketPageUiState(isLoading = true, hasLoadedLocalData = false) diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt index 0594dc4618..5caebbed36 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModelsTest.kt @@ -1,6 +1,10 @@ package one.mixin.android.ui.home.web3.trade.perps import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.ui.home.web3.market.MarketSortColumn +import one.mixin.android.ui.home.web3.market.MarketSortDirection +import one.mixin.android.ui.home.web3.market.MarketSortState +import one.mixin.android.ui.home.web3.market.scoreMarketSortState import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertSame @@ -50,6 +54,28 @@ class PerpsMarketListBottomSheetModelsTest { assertEquals(listOf("first", "second", "third"), visibleMarkets.map { it.marketId }) } + @Test + fun allCategoryHeaderCyclesBackToScore() { + val markets = + listOf( + perpsMarket("second", "crypto", tradeVolumeScore1D = 20, volume = "300"), + perpsMarket("third", "stocks", tradeVolumeScore1D = 10, volume = "200"), + perpsMarket("first", "crypto", tradeVolumeScore1D = 30, volume = "100"), + ) + val descending = PerpsMarketListUiState(markets = markets).selectSort(MarketSortColumn.VOLUME) + val ascending = descending.selectSort(MarketSortColumn.VOLUME) + val score = ascending.selectSort(MarketSortColumn.VOLUME) + + assertEquals(MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING), descending.sortState) + assertEquals(MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.ASCENDING), ascending.sortState) + assertEquals(scoreMarketSortState(), score.sortState) + assertEquals(listOf("first", "second", "third"), score.visibleMarkets.map { it.marketId }) + assertEquals( + MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING), + score.selectCategory(PerpsMarketCategory.CRYPTO).sortState, + ) + } + @Test fun cachesDerivedMarketListsForEachStateInstance() { val state = @@ -107,6 +133,7 @@ class PerpsMarketListBottomSheetModelsTest { marketId: String, category: String, tradeVolumeScore1D: Int = 0, + volume: String = "10", ) = PerpsMarket( marketId = marketId, displaySymbol = marketId, @@ -120,7 +147,7 @@ class PerpsMarketListBottomSheetModelsTest { minAmount = "0", maxAmount = "0", last = "1", - volume = "10", + volume = volume, tradeVolumeScore1D = tradeVolumeScore1D, high = "1", low = "1",