diff --git a/app/schemas/one.mixin.android.db.PerpsDatabase/5.json b/app/schemas/one.mixin.android.db.PerpsDatabase/5.json new file mode 100644 index 0000000000..bd6f971759 --- /dev/null +++ b/app/schemas/one.mixin.android.db.PerpsDatabase/5.json @@ -0,0 +1,411 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "caa79a2cd9e2735f87b8b2af9c6eae2c", + "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, `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, 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": "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 + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_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, 'caa79a2cd9e2735f87b8b2af9c6eae2c')" + ] + } +} \ 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 00e626fceb..3c75539c1d 100644 --- a/app/src/androidTest/java/one/mixin/android/db/PerpsMigrationTest.kt +++ b/app/src/androidTest/java/one/mixin/android/db/PerpsMigrationTest.kt @@ -119,6 +119,26 @@ class PerpsMigrationTest { } } + @Test + fun migrate_4_5_clearsOrders() { + migrationTestHelper.createDatabase(Constants.DataBase.PERPS_DB_NAME, 4).apply { + insertOrderV4() + close() + } + + val migratedDb = migrationTestHelper.runMigrationsAndValidate( + Constants.DataBase.PERPS_DB_NAME, + 5, + true, + PerpsDatabase.MIGRATION_4_5, + ) + + migratedDb.query("SELECT COUNT(*) FROM perps_orders").use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals(0, cursor.getInt(0)) + } + } + private fun SupportSQLiteDatabase.insertMarketV2() { execSQL( """ @@ -165,4 +185,20 @@ class PerpsMigrationTest { """.trimIndent(), ) } + + private fun SupportSQLiteDatabase.insertOrderV4() { + execSQL( + """ + INSERT INTO perps_orders ( + order_id, position_id, market_id, side, order_type, status, leverage, quantity, + entry_price, close_price, realized_pnl, roe, close_reason, trigger_price, + created_at, updated_at + ) VALUES ( + 'order-1', 'position-1', 'market-1', 'long', 'open', 'filled', 10, '1', + '99000', '0', '0', '0', NULL, NULL, + '2026-05-15T15:00:00Z', '2026-05-15T15:00:00Z' + ) + """.trimIndent(), + ) + } } diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt index 86b9befcd7..3509737a0d 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt @@ -35,6 +35,9 @@ data class PerpsOrder( @SerializedName("quantity") @ColumnInfo(name = "quantity") val quantity: String, + @SerializedName("pay_amount") + @ColumnInfo(name = "pay_amount") + val payAmount: String = "0", @SerializedName("entry_price") @ColumnInfo(name = "entry_price") val entryPrice: String, diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt index 8800b4aaca..f1e4f53a85 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt @@ -31,6 +31,9 @@ data class PerpsOrderItem( @SerializedName("quantity") @ColumnInfo(name = "quantity") val quantity: String, + @SerializedName("pay_amount") + @ColumnInfo(name = "pay_amount") + val payAmount: String, @SerializedName("entry_price") @ColumnInfo(name = "entry_price") val entryPrice: 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 986e6086f9..71975cd80a 100644 --- a/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt +++ b/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt @@ -28,7 +28,7 @@ import kotlin.math.min PerpsOrder::class, PerpsMarket::class, ], - version = 4, + version = 5, ) abstract class PerpsDatabase : RoomDatabase() { companion object { @@ -79,6 +79,13 @@ abstract class PerpsDatabase : RoomDatabase() { ) } } + val MIGRATION_4_5 = + object : Migration(4, 5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE perps_orders ADD COLUMN pay_amount TEXT NOT NULL DEFAULT '0'") + db.execSQL("DELETE FROM perps_orders") + } + } fun getDatabase( context: Context, @@ -103,7 +110,7 @@ abstract class PerpsDatabase : RoomDatabase() { listOf( object : MixinCorruptionCallback { override fun onCorruption(database: SupportSQLiteDatabase) { - val e = IllegalStateException("Perps database is corrupted, current DB version: 4") + val e = IllegalStateException("Perps database is corrupted, current DB version: 5") reportException(e) } }, @@ -116,7 +123,7 @@ abstract class PerpsDatabase : RoomDatabase() { db.execSQL("PRAGMA synchronous = NORMAL") } }, - ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .fallbackToDestructiveMigration() .enableMultiInstanceInvalidation() .setQueryExecutor( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt index d7cdbfc45a..a0af91e362 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt @@ -35,7 +35,6 @@ import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences import java.math.BigDecimal -import java.math.RoundingMode @Composable fun OpenedOrderItem( @@ -75,11 +74,8 @@ fun OpenedOrderItem( stringResource(if (isLong) R.string.Opened_Long else R.string.Opened_Short) } - val amountValue = if (!isFailed && order.leverage > 0) { - val quantityDecimal = order.quantity.toBigDecimalOrNull()?.abs() ?: BigDecimal.ZERO - val entryPriceDecimal = order.entryPrice.toBigDecimalOrNull() ?: BigDecimal.ZERO - quantityDecimal.multiply(entryPriceDecimal) - .divide(BigDecimal(order.leverage), 8, RoundingMode.HALF_UP) + val amountValue = if (!isFailed) { + order.payAmount.toBigDecimalOrNull() ?: BigDecimal.ZERO } else { null }