diff --git a/app/schemas/one.mixin.android.db.WalletDatabase/8.json b/app/schemas/one.mixin.android.db.WalletDatabase/8.json new file mode 100644 index 0000000000..1bc85d95aa --- /dev/null +++ b/app/schemas/one.mixin.android.db.WalletDatabase/8.json @@ -0,0 +1,777 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "8e2e65b1b06a67ea47b795d48d8790c1", + "entities": [ + { + "tableName": "tokens", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `asset_id` TEXT NOT NULL, `chain_id` TEXT NOT NULL, `name` TEXT NOT NULL, `asset_key` TEXT NOT NULL, `symbol` TEXT NOT NULL, `icon_url` TEXT NOT NULL, `precision` INTEGER NOT NULL, `kernel_asset_id` TEXT NOT NULL, `amount` TEXT NOT NULL, `price_usd` TEXT NOT NULL, `change_usd` TEXT NOT NULL, `level` INTEGER NOT NULL, PRIMARY KEY(`wallet_id`, `asset_id`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetKey", + "columnName": "asset_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precision", + "columnName": "precision", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kernelAssetId", + "columnName": "kernel_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priceUsd", + "columnName": "price_usd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "changeUsd", + "columnName": "change_usd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wallet_id", + "asset_id" + ] + } + }, + { + "tableName": "transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`transaction_hash` TEXT NOT NULL, `chain_id` TEXT NOT NULL, `address` TEXT NOT NULL, `transaction_type` TEXT NOT NULL, `status` TEXT NOT NULL, `block_number` INTEGER NOT NULL, `fee` TEXT NOT NULL, `sponsor_fee_asset_id` TEXT, `sponsor_fee_amount` TEXT, `senders` TEXT, `receivers` TEXT, `approvals` TEXT, `send_asset_id` TEXT, `receive_asset_id` TEXT, `transaction_at` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `level` INTEGER NOT NULL, PRIMARY KEY(`transaction_hash`, `chain_id`, `address`))", + "fields": [ + { + "fieldPath": "transactionHash", + "columnName": "transaction_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionType", + "columnName": "transaction_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockNumber", + "columnName": "block_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fee", + "columnName": "fee", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sponsorFeeAssetId", + "columnName": "sponsor_fee_asset_id", + "affinity": "TEXT" + }, + { + "fieldPath": "sponsorFeeAmount", + "columnName": "sponsor_fee_amount", + "affinity": "TEXT" + }, + { + "fieldPath": "senders", + "columnName": "senders", + "affinity": "TEXT" + }, + { + "fieldPath": "receivers", + "columnName": "receivers", + "affinity": "TEXT" + }, + { + "fieldPath": "approvals", + "columnName": "approvals", + "affinity": "TEXT" + }, + { + "fieldPath": "sendAssetId", + "columnName": "send_asset_id", + "affinity": "TEXT" + }, + { + "fieldPath": "receiveAssetId", + "columnName": "receive_asset_id", + "affinity": "TEXT" + }, + { + "fieldPath": "transactionAt", + "columnName": "transaction_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "transaction_hash", + "chain_id", + "address" + ] + }, + "indices": [ + { + "name": "index_transactions_address_transaction_at", + "unique": false, + "columnNames": [ + "address", + "transaction_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_transactions_address_transaction_at` ON `${TABLE_NAME}` (`address`, `transaction_at`)" + }, + { + "name": "index_transactions_transaction_type_send_asset_id_receive_asset_id_transaction_at_level", + "unique": false, + "columnNames": [ + "transaction_type", + "send_asset_id", + "receive_asset_id", + "transaction_at", + "level" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_transactions_transaction_type_send_asset_id_receive_asset_id_transaction_at_level` ON `${TABLE_NAME}` (`transaction_type`, `send_asset_id`, `receive_asset_id`, `transaction_at`, `level`)" + } + ] + }, + { + "tableName": "wallets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `category` TEXT NOT NULL, `name` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`wallet_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "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": [ + "wallet_id" + ] + } + }, + { + "tableName": "addresses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `chain_id` TEXT NOT NULL, `destination` TEXT NOT NULL, `path` TEXT, `created_at` TEXT NOT NULL, PRIMARY KEY(`address_id`))", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "address_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "address_id" + ] + } + }, + { + "tableName": "tokens_extra", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `asset_id` TEXT NOT NULL, `hidden` INTEGER, PRIMARY KEY(`wallet_id`, `asset_id`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wallet_id", + "asset_id" + ] + } + }, + { + "tableName": "chains", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain_id` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `icon_url` TEXT NOT NULL, `threshold` INTEGER NOT NULL, PRIMARY KEY(`chain_id`))", + "fields": [ + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threshold", + "columnName": "threshold", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "chain_id" + ] + } + }, + { + "tableName": "raw_transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hash` TEXT NOT NULL, `chain_id` TEXT NOT NULL, `account` TEXT NOT NULL, `nonce` TEXT NOT NULL, `raw` TEXT NOT NULL, `state` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`hash`))", + "fields": [ + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nonce", + "columnName": "nonce", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "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": [ + "hash" + ] + }, + "indices": [ + { + "name": "index_raw_transactions_chain_id", + "unique": false, + "columnNames": [ + "chain_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_raw_transactions_chain_id` ON `${TABLE_NAME}` (`chain_id`)" + } + ] + }, + { + "tableName": "properties", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "orders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `pay_asset_id` TEXT NOT NULL, `receive_asset_id` TEXT NOT NULL, `pay_amount` TEXT NOT NULL, `receive_amount` TEXT, `pay_trace_id` TEXT, `receive_trace_id` TEXT, `state` TEXT NOT NULL, `created_at` TEXT NOT NULL, `order_type` TEXT NOT NULL, `fund_status` TEXT, `price` TEXT, `pending_amount` TEXT, `filled_receive_amount` TEXT, `expected_receive_amount` TEXT, `expired_at` TEXT, PRIMARY KEY(`order_id`))", + "fields": [ + { + "fieldPath": "orderId", + "columnName": "order_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payAssetId", + "columnName": "pay_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "receiveAssetId", + "columnName": "receive_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payAmount", + "columnName": "pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "receiveAmount", + "columnName": "receive_amount", + "affinity": "TEXT" + }, + { + "fieldPath": "payTraceId", + "columnName": "pay_trace_id", + "affinity": "TEXT" + }, + { + "fieldPath": "receiveTraceId", + "columnName": "receive_trace_id", + "affinity": "TEXT" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderType", + "columnName": "order_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fundStatus", + "columnName": "fund_status", + "affinity": "TEXT" + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT" + }, + { + "fieldPath": "pendingAmount", + "columnName": "pending_amount", + "affinity": "TEXT" + }, + { + "fieldPath": "filledReceiveAmount", + "columnName": "filled_receive_amount", + "affinity": "TEXT" + }, + { + "fieldPath": "expectedReceiveAmount", + "columnName": "expected_receive_amount", + "affinity": "TEXT" + }, + { + "fieldPath": "expiredAt", + "columnName": "expired_at", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "order_id" + ] + }, + "indices": [ + { + "name": "index_orders_state_created_at", + "unique": false, + "columnNames": [ + "state", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_orders_state_created_at` ON `${TABLE_NAME}` (`state`, `created_at`)" + }, + { + "name": "index_orders_order_type_created_at", + "unique": false, + "columnNames": [ + "order_type", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_orders_order_type_created_at` ON `${TABLE_NAME}` (`order_type`, `created_at`)" + } + ] + }, + { + "tableName": "safe_wallets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wallet_id` TEXT NOT NULL, `name` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `role` TEXT NOT NULL, `chain_id` TEXT NOT NULL, `address` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`wallet_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeRole", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeChainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeAddress", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeUrl", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wallet_id" + ] + } + }, + { + "tableName": "outputs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`output_id` TEXT NOT NULL, `asset_id` TEXT NOT NULL, `transaction_hash` TEXT NOT NULL, `output_index` INTEGER NOT NULL, `amount` TEXT NOT NULL, `address` TEXT NOT NULL, `pubkey_hex` TEXT NOT NULL, `pubkey_type` TEXT NOT NULL, `status` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`output_id`))", + "fields": [ + { + "fieldPath": "outputId", + "columnName": "output_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionHash", + "columnName": "transaction_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "outputIndex", + "columnName": "output_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubkeyHex", + "columnName": "pubkey_hex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubkeyType", + "columnName": "pubkey_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "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": [ + "output_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, '8e2e65b1b06a67ea47b795d48d8790c1')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/one/mixin/android/db/WalletDatabase.kt b/app/src/main/java/one/mixin/android/db/WalletDatabase.kt index 195e52e292..3d50b9a0dd 100644 --- a/app/src/main/java/one/mixin/android/db/WalletDatabase.kt +++ b/app/src/main/java/one/mixin/android/db/WalletDatabase.kt @@ -53,7 +53,7 @@ import kotlin.math.min SafeWallets::class, WalletOutput::class, ], - version = 7, + version = 8, ) @TypeConverters(Web3TypeConverters::class, AssetChangeListConverter::class) abstract class WalletDatabase : RoomDatabase() { @@ -115,6 +115,13 @@ abstract class WalletDatabase : RoomDatabase() { } } + val MIGRATION_7_8 = object : Migration(7, 8) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE transactions ADD COLUMN sponsor_fee_asset_id TEXT") + db.execSQL("ALTER TABLE transactions ADD COLUMN sponsor_fee_amount TEXT") + } + } + fun getDatabase( context: Context, identityNumber: String, @@ -139,7 +146,7 @@ abstract class WalletDatabase : RoomDatabase() { listOf( object : MixinCorruptionCallback { override fun onCorruption(database: SupportSQLiteDatabase) { - val e = IllegalStateException("Wallet database is corrupted, current DB version: 7") + val e = IllegalStateException("Wallet database is corrupted, current DB version: 8") reportException(e) } }, @@ -153,7 +160,7 @@ abstract class WalletDatabase : RoomDatabase() { supportSQLiteDatabase = db } }, - ).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) .enableMultiInstanceInvalidation() .setQueryExecutor( Executors.newFixedThreadPool( diff --git a/app/src/main/java/one/mixin/android/db/web3/Web3TransactionDao.kt b/app/src/main/java/one/mixin/android/db/web3/Web3TransactionDao.kt index 6b54b948ab..49d0c4e31b 100644 --- a/app/src/main/java/one/mixin/android/db/web3/Web3TransactionDao.kt +++ b/app/src/main/java/one/mixin/android/db/web3/Web3TransactionDao.kt @@ -14,17 +14,19 @@ import one.mixin.android.db.web3.vo.Web3TransactionItem interface Web3TransactionDao : BaseDao { @Query(""" - SELECT DISTINCT w.transaction_hash, w.transaction_type, w.status, w.block_number, w.chain_id, w.address, w.fee, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, w.transaction_at, w.updated_at, w.level, + SELECT DISTINCT w.transaction_hash, w.transaction_type, w.status, w.block_number, w.chain_id, w.address, w.fee, w.sponsor_fee_asset_id, w.sponsor_fee_amount, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, w.transaction_at, w.updated_at, w.level, c.symbol as chain_symbol, c.icon_url as chain_icon_url, s.icon_url as send_asset_icon_url, s.symbol as send_asset_symbol, r.icon_url as receive_asset_icon_url, - r.symbol as receive_asset_symbol + r.symbol as receive_asset_symbol, + sf.symbol as sponsor_fee_asset_symbol FROM transactions w LEFT JOIN tokens c ON c.asset_id = w.chain_id AND c.wallet_id = :walletId LEFT JOIN tokens s ON s.asset_id = w.send_asset_id AND s.wallet_id = :walletId LEFT JOIN tokens r ON r.asset_id = w.receive_asset_id AND r.wallet_id = :walletId + LEFT JOIN tokens sf ON sf.asset_id = w.sponsor_fee_asset_id AND sf.wallet_id = :walletId WHERE (w.send_asset_id = :assetId OR w.receive_asset_id = :assetId) AND (s.wallet_id = :walletId OR c.wallet_id = :walletId) AND w.level >= (SELECT level FROM tokens WHERE asset_id = :assetId) AND w.address in (SELECT destination FROM addresses WHERE wallet_id = :walletId) ORDER BY w.transaction_at DESC diff --git a/app/src/main/java/one/mixin/android/db/web3/vo/Web3Transaction.kt b/app/src/main/java/one/mixin/android/db/web3/vo/Web3Transaction.kt index e4d7a6bc56..abd679bcca 100644 --- a/app/src/main/java/one/mixin/android/db/web3/vo/Web3Transaction.kt +++ b/app/src/main/java/one/mixin/android/db/web3/vo/Web3Transaction.kt @@ -49,6 +49,14 @@ data class Web3Transaction( @SerializedName("fee") val fee: String, + @ColumnInfo(name = "sponsor_fee_asset_id") + @SerializedName("sponsor_fee_asset_id") + val sponsorFeeAssetId: String? = null, + + @ColumnInfo(name = "sponsor_fee_amount") + @SerializedName("sponsor_fee_amount") + val sponsorFeeAmount: String? = null, + @TypeConverters(AssetChangeListConverter::class) @ColumnInfo(name = "senders") @SerializedName("senders") diff --git a/app/src/main/java/one/mixin/android/db/web3/vo/Web3TransactionItem.kt b/app/src/main/java/one/mixin/android/db/web3/vo/Web3TransactionItem.kt index be2f205d9a..cd56890923 100644 --- a/app/src/main/java/one/mixin/android/db/web3/vo/Web3TransactionItem.kt +++ b/app/src/main/java/one/mixin/android/db/web3/vo/Web3TransactionItem.kt @@ -32,6 +32,12 @@ data class Web3TransactionItem( @ColumnInfo(name = "fee") val fee: String, + + @ColumnInfo(name = "sponsor_fee_asset_id") + val sponsorFeeAssetId: String? = null, + + @ColumnInfo(name = "sponsor_fee_amount") + val sponsorFeeAmount: String? = null, @TypeConverters(AssetChangeListConverter::class) @ColumnInfo(name = "senders") @@ -75,6 +81,9 @@ data class Web3TransactionItem( @ColumnInfo(name = "receive_asset_symbol") val receiveAssetSymbol: String? = null, + @ColumnInfo(name = "sponsor_fee_asset_symbol") + val sponsorFeeAssetSymbol: String? = null, + @ColumnInfo(name = "level") val level: Int, ) : Parcelable { @@ -98,6 +107,12 @@ data class Web3TransactionItem( fun isNotVerified() = level < Constants.AssetLevel.VERIFIED + fun displayFeeAmount(): String = sponsorFeeAmount?.ifBlank { fee } ?: fee + + fun displayFeeSymbol(): String? = sponsorFeeAssetSymbol ?: chainSymbol + + fun hasSponsorFee(): Boolean = !sponsorFeeAmount.isNullOrBlank() + fun getMainAmount(): String { return when (transactionType) { TransactionType.TRANSFER_IN.value -> { diff --git a/app/src/main/java/one/mixin/android/repository/TokenRepository.kt b/app/src/main/java/one/mixin/android/repository/TokenRepository.kt index e9c6720bdb..d55d1a9778 100644 --- a/app/src/main/java/one/mixin/android/repository/TokenRepository.kt +++ b/app/src/main/java/one/mixin/android/repository/TokenRepository.kt @@ -1551,6 +1551,8 @@ class TokenRepository nonce = nonce, createdAt = createdAt, updatedAt = updatedAt, + sponsorFeeAssetId = assetId, + sponsorFeeAmount = fee, ) } @@ -1593,9 +1595,12 @@ class TokenRepository nonce: String, createdAt: String, updatedAt: String, + sponsorFeeAssetId: String? = null, + sponsorFeeAmount: String? = null, ) { val normalizedAmount = amount.removePrefix("-") val normalizedFee = fee.toBigDecimalOrNull()?.stripTrailingZeros()?.toPlainString() ?: fee + val normalizedSponsorFeeAmount = sponsorFeeAmount?.toBigDecimalOrNull()?.stripTrailingZeros()?.toPlainString() ?: sponsorFeeAmount appDatabase.withTransaction { web3RawTransactionDao.insertSuspend( Web3RawTransaction( @@ -1619,6 +1624,8 @@ class TokenRepository status = TransactionStatus.PENDING.value, blockNumber = 0, fee = normalizedFee, + sponsorFeeAssetId = sponsorFeeAssetId, + sponsorFeeAmount = normalizedSponsorFeeAmount, senders = listOf( AssetChange( assetId = assetId, diff --git a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt index 41dc6e6523..bb464cbd78 100644 --- a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt +++ b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt @@ -230,8 +230,8 @@ constructor( } suspend fun mapWeb3Transaction(transaction: Web3TransactionItem, walletId: String): Web3TransactionItem = withContext(Dispatchers.IO) { - val assetIds = transaction.senders.map { it.assetId } + transaction.receivers.map { it.assetId } + (transaction.approvals?.map { it.assetId } ?: emptyList()) - val tokens = web3TokenDao.findWeb3TokenItemsByIdsSync(walletId, assetIds.distinct()).associateBy { it.assetId } + val assetIds = transaction.senders.map { it.assetId } + transaction.receivers.map { it.assetId } + (transaction.approvals?.map { it.assetId } ?: emptyList()) + transaction.sponsorFeeAssetId.orEmpty() + val tokens = web3TokenDao.findWeb3TokenItemsByIdsSync(walletId, assetIds.filter(String::isNotBlank).distinct()).associateBy { it.assetId } transaction.copy( senders = transaction.senders.map { it.copy(symbol = tokens[it.assetId]?.symbol) @@ -241,7 +241,8 @@ constructor( }, approvals = transaction.approvals?.map { it.copy(symbol = tokens[it.assetId]?.symbol) - } + }, + sponsorFeeAssetSymbol = tokens[transaction.sponsorFeeAssetId]?.symbol ?: transaction.sponsorFeeAssetSymbol, ) } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/LimitTransferBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/LimitTransferBottomSheetDialogFragment.kt index 0cfec460af..96923a98dd 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/LimitTransferBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/LimitTransferBottomSheetDialogFragment.kt @@ -230,6 +230,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag private var web3Transaction: JsSignMessage? by mutableStateOf(null) private var gaslessPrepareResponse: GaslessTxResponse? by mutableStateOf(null) + private var gaslessFeeAmount: String? by mutableStateOf(null) private var tipGas: TipGas? by mutableStateOf(null) private var solanaFee: BigDecimal? by mutableStateOf(null) private var solanaTx: VersionedTransactionCompat? by mutableStateOf(null) @@ -815,6 +816,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag gaslessPrepareResponse = previewData.gaslessPrepareResponseJson?.let { GsonHelper.customGson.fromJson(it, GaslessTxResponse::class.java) } + gaslessFeeAmount = previewData.feeAmount val previewFee = previewData.feeAmount.toBigDecimalOrNull() when (token.chainId) { @@ -876,6 +878,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag amount = amount, chainId = preparedResponse.chainId, payload = preparedResponse.payload, + fee = normalizeGaslessPendingFeeAmount(gaslessFeeAmount), privateKey = privateKey, ) else -> throw IllegalArgumentException("Gasless is not supported for ${transferToken.chainId}") @@ -936,6 +939,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag amount: String, chainId: String, payload: JsonElement, + fee: String, privateKey: ByteArray, ) { if (!payload.isJsonObject) { @@ -968,7 +972,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag account = fromAddress, assetId = token.assetId, amount = amount.stripAmountZero(), - fee = "", + fee = fee, to = toAddress, nonce = ethPayload.userOperation.nonce, createdAt = now, diff --git a/app/src/main/java/one/mixin/android/ui/wallet/SwapTransferBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/SwapTransferBottomSheetDialogFragment.kt index efbcaf1d0f..71d710bebc 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/SwapTransferBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/SwapTransferBottomSheetDialogFragment.kt @@ -319,6 +319,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm private var web3Transaction: JsSignMessage? by mutableStateOf(null) private var gaslessPrepareResponse: GaslessTxResponse? by mutableStateOf(null) + private var gaslessFeeAmount: String? by mutableStateOf(null) private var isGaslessLoading by mutableStateOf(false) private var tipGas: TipGas? by mutableStateOf(null) private var solanaFee: BigDecimal? by mutableStateOf(null) @@ -1075,6 +1076,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm gaslessPrepareResponse = previewData.gaslessPrepareResponseJson?.let { GsonHelper.customGson.fromJson(it, GaslessTxResponse::class.java) } + gaslessFeeAmount = previewData.feeAmount val previewFee = previewData.feeAmount.toBigDecimalOrNull() when (token.chainId) { @@ -1110,6 +1112,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm amount: String, ): GaslessTxResponse? { val feeAmount = resolveGaslessFeeAmount(token, fromAddress, toAddress) ?: return null + gaslessFeeAmount = feeAmount return runCatching { web3ViewModel.gaslessPrepare( GaslessTxRequest( @@ -1135,6 +1138,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm val feeAmount = requireNotNull(resolveGaslessFeeAmount(token, fromAddress, toAddress)) { "gasless fee amount is required" } + gaslessFeeAmount = feeAmount val response = web3ViewModel.gaslessPrepare( GaslessTxRequest( from = fromAddress, @@ -1196,6 +1200,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm amount = amount, chainId = preparedResponse.chainId, payload = preparedResponse.payload, + fee = normalizeGaslessPendingFeeAmount(gaslessFeeAmount), privateKey = privateKey, ) else -> throw IllegalArgumentException("Gasless is not supported for ${transferToken.chainId}") @@ -1256,6 +1261,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm amount: String, chainId: String, payload: JsonElement, + fee: String, privateKey: ByteArray, ) { if (!payload.isJsonObject) { @@ -1288,7 +1294,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm account = fromAddress, assetId = token.assetId, amount = amount.stripAmountZero(), - fee = "", + fee = fee, to = toAddress, nonce = ethPayload.userOperation.nonce, createdAt = now, diff --git a/app/src/main/java/one/mixin/android/ui/wallet/Web3FilterParams.kt b/app/src/main/java/one/mixin/android/ui/wallet/Web3FilterParams.kt index 0356a63373..91ffba93a9 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/Web3FilterParams.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/Web3FilterParams.kt @@ -107,20 +107,21 @@ class Web3FilterParams( return SimpleSQLiteQuery( "SELECT DISTINCT w.transaction_hash, w.transaction_type, w.status, w.block_number, w.chain_id, " + - "w.address, w.fee, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, " + + "w.address, w.fee, w.sponsor_fee_asset_id, w.sponsor_fee_amount, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, " + "w.transaction_at, w.updated_at, w.level, " + "c.symbol as chain_symbol, " + "c.icon_url as chain_icon_url, " + "s.icon_url as send_asset_icon_url, " + "s.symbol as send_asset_symbol, " + "r.icon_url as receive_asset_icon_url, " + - "r.symbol as receive_asset_symbol " + + "r.symbol as receive_asset_symbol, " + + "sf.symbol as sponsor_fee_asset_symbol " + "FROM transactions w " + "LEFT JOIN tokens c ON c.asset_id = w.chain_id " + "LEFT JOIN tokens s ON s.asset_id = w.send_asset_id " + "LEFT JOIN tokens r ON r.asset_id = w.receive_asset_id " + + "LEFT JOIN tokens sf ON sf.asset_id = w.sponsor_fee_asset_id " + "$whereSql $orderSql" ) } } - diff --git a/app/src/main/java/one/mixin/android/ui/wallet/Web3PendingFee.kt b/app/src/main/java/one/mixin/android/ui/wallet/Web3PendingFee.kt new file mode 100644 index 0000000000..ea86c013ac --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/wallet/Web3PendingFee.kt @@ -0,0 +1,4 @@ +package one.mixin.android.ui.wallet + +internal fun normalizeGaslessPendingFeeAmount(feeAmount: String?): String = + feeAmount?.toBigDecimalOrNull()?.stripTrailingZeros()?.toPlainString() ?: feeAmount.orEmpty() diff --git a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionFragment.kt b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionFragment.kt index fb7aaddeee..b2543831bb 100644 --- a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionFragment.kt +++ b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionFragment.kt @@ -542,7 +542,7 @@ class Web3TransactionFragment : BaseFragment(R.layout.fragment_web3_transaction) dateTv.text = transaction.transactionAt.fullDate() feeLl.isVisible = shouldShowFee(transaction.status) - feeTv.text = "${transaction.fee} ${transaction.chainSymbol ?: ""}" + feeTv.text = "${transaction.displayFeeAmount()} ${transaction.displayFeeSymbol() ?: ""}" statusLl.isVisible = false networkLl.isVisible = true @@ -619,13 +619,13 @@ class Web3TransactionFragment : BaseFragment(R.layout.fragment_web3_transaction) status: String, pendingRawTx: Web3RawTransaction? = null, ): Boolean { - if (transaction.transactionType == TransactionType.TRANSFER_IN.value || transaction.fee.isEmpty()) { + if (!transaction.hasSponsorFee() && (transaction.transactionType == TransactionType.TRANSFER_IN.value || transaction.fee.isEmpty())) { return false } if (status != TransactionStatus.PENDING.value) { return true } - return pendingRawTx?.isGaslessPending() == false || transaction.fee.isNotEmpty() + return pendingRawTx?.isGaslessPending() == false || transaction.displayFeeAmount().isNotEmpty() } private suspend fun updateFeeVisibility(status: String = transaction.status) { diff --git a/app/src/test/java/one/mixin/android/ui/wallet/Web3PendingFeeTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/Web3PendingFeeTest.kt new file mode 100644 index 0000000000..1c3bedd9ce --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/wallet/Web3PendingFeeTest.kt @@ -0,0 +1,69 @@ +package one.mixin.android.ui.wallet + +import one.mixin.android.db.web3.vo.Web3TransactionItem +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Test + +class Web3PendingFeeTest { + @Test + fun gaslessPendingFeeKeepsDisplayableAmount() { + assertEquals("0.00021", normalizeGaslessPendingFeeAmount("0.0002100")) + } + + @Test + fun gaslessPendingFeeFallsBackToEmptyWhenMissing() { + assertEquals("", normalizeGaslessPendingFeeAmount(null)) + } + + @Test + fun sponsorFeeIsPreferredForDisplay() { + val transaction = transactionItem( + fee = "0.0001", + sponsorFeeAmount = "0.25", + sponsorFeeAssetSymbol = "USDT", + ) + + assertEquals("0.25", transaction.displayFeeAmount()) + assertEquals("USDT", transaction.displayFeeSymbol()) + assertTrue(transaction.hasSponsorFee()) + } + + @Test + fun chainFeeIsUsedWhenSponsorFeeIsMissing() { + val transaction = transactionItem( + fee = "0.0001", + chainSymbol = "ETH", + ) + + assertEquals("0.0001", transaction.displayFeeAmount()) + assertEquals("ETH", transaction.displayFeeSymbol()) + } + + private fun transactionItem( + fee: String, + sponsorFeeAmount: String? = null, + chainSymbol: String? = null, + sponsorFeeAssetSymbol: String? = null, + ) = Web3TransactionItem( + transactionHash = "hash", + transactionType = "transfer_in", + status = "success", + blockNumber = 1, + chainId = "chain", + address = "address", + fee = fee, + sponsorFeeAssetId = if (sponsorFeeAmount.isNullOrBlank()) null else "sponsor", + sponsorFeeAmount = sponsorFeeAmount, + senders = emptyList(), + receivers = emptyList(), + approvals = null, + sendAssetId = null, + receiveAssetId = null, + transactionAt = "2026-04-03T14:52:35.000000Z", + updatedAt = "2026-04-03T14:52:55.822512Z", + chainSymbol = chainSymbol, + sponsorFeeAssetSymbol = sponsorFeeAssetSymbol, + level = 0, + ) +}