From 7b2a56b1769080788a2d9178144802a63f04376a Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 3 Nov 2025 14:55:48 +0800 Subject: [PATCH] Replace wallet data provider --- .../android/db/provider/Web3DataProvider.kt | 190 ++++++++++++++++++ .../android/repository/Web3Repository.kt | 6 +- .../android/ui/wallet/Web3FilterParams.kt | 76 ------- 3 files changed, 195 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/db/provider/Web3DataProvider.kt diff --git a/app/src/main/java/one/mixin/android/db/provider/Web3DataProvider.kt b/app/src/main/java/one/mixin/android/db/provider/Web3DataProvider.kt new file mode 100644 index 0000000000..786bb5e45a --- /dev/null +++ b/app/src/main/java/one/mixin/android/db/provider/Web3DataProvider.kt @@ -0,0 +1,190 @@ +package one.mixin.android.db.provider + +import android.annotation.SuppressLint +import android.database.Cursor +import androidx.paging.DataSource +import androidx.room.RoomSQLiteQuery +import one.mixin.android.db.WalletDatabase +import one.mixin.android.db.converter.AssetChangeListConverter +import one.mixin.android.db.datasource.MixinLimitOffsetDataSource +import one.mixin.android.db.web3.vo.AssetChange +import one.mixin.android.db.web3.vo.TransactionStatus +import one.mixin.android.db.web3.vo.Web3TransactionItem +import one.mixin.android.tip.wc.SortOrder +import one.mixin.android.ui.wallet.Web3FilterParams + +@SuppressLint("RestrictedApi") +class Web3DataProvider { + companion object { + fun allTransactions( + database: WalletDatabase, + filter: Web3FilterParams, + ): DataSource.Factory { + return object : DataSource.Factory() { + override fun create(): DataSource { + val baseSelect = """ + 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, + 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 + 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 + """.trimIndent() + + val filters = buildFilters(filter) + val whereSql = if (filters.isEmpty()) "" else "WHERE ${filters.joinToString(" AND ")}" + val orderSql = when (filter.order) { + SortOrder.Recent -> "ORDER BY w.transaction_at DESC" + SortOrder.Oldest -> "ORDER BY w.transaction_at ASC" + else -> "ORDER BY w.transaction_at DESC" + } + + val countSql = "SELECT count(1) FROM transactions w $whereSql" + val countStmt = RoomSQLiteQuery.acquire(countSql, 0) + + val offsetSql = """ + SELECT w.rowid FROM transactions w + $whereSql + $orderSql + LIMIT ? OFFSET ? + """.trimIndent() + val offsetStmt = RoomSQLiteQuery.acquire(offsetSql, 2) + + val sqlGenerator = fun(ids: String): RoomSQLiteQuery { + val querySql = StringBuilder() + .append(baseSelect) + .append('\n') + .append("WHERE w.rowid IN ($ids)") + .append('\n') + .append(orderSql) + .toString() + return RoomSQLiteQuery.acquire(querySql, 0) + } + + return object : MixinLimitOffsetDataSource( + database, + countStmt, + offsetStmt, + sqlGenerator, + arrayOf("transactions", "tokens", "addresses"), + ) { + private val assetChangeConverter = AssetChangeListConverter() + + override fun convertRows(cursor: Cursor?): List { + if (cursor == null) return emptyList() + val list = ArrayList(cursor.count) + val idxHash = cursor.getColumnIndexOrThrow("transaction_hash") + val idxType = cursor.getColumnIndexOrThrow("transaction_type") + val idxStatus = cursor.getColumnIndexOrThrow("status") + val idxBlock = cursor.getColumnIndexOrThrow("block_number") + val idxChain = cursor.getColumnIndexOrThrow("chain_id") + val idxAddress = cursor.getColumnIndexOrThrow("address") + val idxFee = cursor.getColumnIndexOrThrow("fee") + val idxSenders = cursor.getColumnIndexOrThrow("senders") + val idxReceivers = cursor.getColumnIndexOrThrow("receivers") + val idxApprovals = cursor.getColumnIndexOrThrow("approvals") + val idxSendAssetId = cursor.getColumnIndexOrThrow("send_asset_id") + val idxReceiveAssetId = cursor.getColumnIndexOrThrow("receive_asset_id") + val idxAt = cursor.getColumnIndexOrThrow("transaction_at") + val idxUpdated = cursor.getColumnIndexOrThrow("updated_at") + val idxLevel = cursor.getColumnIndexOrThrow("level") + val idxChainSymbol = cursor.getColumnIndexOrThrow("chain_symbol") + val idxChainIcon = cursor.getColumnIndexOrThrow("chain_icon_url") + val idxSendIcon = cursor.getColumnIndexOrThrow("send_asset_icon_url") + val idxSendSymbol = cursor.getColumnIndexOrThrow("send_asset_symbol") + val idxRecvIcon = cursor.getColumnIndexOrThrow("receive_asset_icon_url") + val idxRecvSymbol = cursor.getColumnIndexOrThrow("receive_asset_symbol") + + while (cursor.moveToNext()) { + val sendersJson = cursor.getString(idxSenders) + val receiversJson = cursor.getString(idxReceivers) + val approvalsJson = cursor.getString(idxApprovals) + val senders: List = assetChangeConverter.toAssetChangeList(sendersJson) ?: emptyList() + val receivers: List = assetChangeConverter.toAssetChangeList(receiversJson) ?: emptyList() + val approvals: List? = approvalsJson?.let { assetChangeConverter.toAssetChangeList(it) } + + val item = Web3TransactionItem( + transactionHash = cursor.getString(idxHash), + transactionType = cursor.getString(idxType), + status = cursor.getString(idxStatus), + blockNumber = cursor.getLong(idxBlock), + chainId = cursor.getString(idxChain), + address = cursor.getString(idxAddress), + fee = cursor.getString(idxFee), + senders = senders, + receivers = receivers, + approvals = approvals, + sendAssetId = cursor.getString(idxSendAssetId), + receiveAssetId = cursor.getString(idxReceiveAssetId), + transactionAt = cursor.getString(idxAt), + updatedAt = cursor.getString(idxUpdated), + chainSymbol = cursor.getString(idxChainSymbol), + chainIconUrl = cursor.getString(idxChainIcon), + sendAssetIconUrl = cursor.getString(idxSendIcon), + sendAssetSymbol = cursor.getString(idxSendSymbol), + receiveAssetIconUrl = cursor.getString(idxRecvIcon), + receiveAssetSymbol = cursor.getString(idxRecvSymbol), + level = cursor.getInt(idxLevel), + ) + list.add(item) + } + return list + } + } + } + } + } + + private fun buildFilters(filter: Web3FilterParams): MutableList { + val filters = mutableListOf() + filter.tokenItems?.let { tokens -> + if (tokens.isNotEmpty()) { + val tokenIds = tokens.joinToString(", ") { t -> "'${t.assetId}'" } + filters.add("(w.send_asset_id IN ($tokenIds) OR w.receive_asset_id IN ($tokenIds))") + } + } + filters.add("w.address IN (SELECT destination FROM addresses WHERE wallet_id = '${filter.walletId}')") + when (filter.tokenFilterType) { + one.mixin.android.ui.wallet.Web3TokenFilterType.SEND -> filters.add("w.transaction_type = 'transfer_out'") + one.mixin.android.ui.wallet.Web3TokenFilterType.RECEIVE -> filters.add("w.transaction_type = 'transfer_in'") + one.mixin.android.ui.wallet.Web3TokenFilterType.APPROVAL -> filters.add("w.transaction_type = 'approval'") + one.mixin.android.ui.wallet.Web3TokenFilterType.SWAP -> filters.add("w.transaction_type = 'swap'") + one.mixin.android.ui.wallet.Web3TokenFilterType.PENDING -> filters.add("w.status = '${TransactionStatus.PENDING.value}'") + one.mixin.android.ui.wallet.Web3TokenFilterType.ALL -> {} + } + filter.startTime?.let { + filters.add("w.transaction_at >= '${org.threeten.bp.Instant.ofEpochMilli(it)}'") + } + filter.endTime?.let { + filters.add("w.transaction_at <= '${org.threeten.bp.Instant.ofEpochMilli(it + 24 * 60 * 60 * 1000)}'") + } + when (filter.level and Web3FilterParams.FILTER_MASK) { + Web3FilterParams.FILTER_GOOD_ONLY -> filters.add("w.level >= 11") + Web3FilterParams.FILTER_GOOD_AND_UNKNOWN -> filters.add("w.level >= 10") + Web3FilterParams.FILTER_GOOD_AND_SPAM -> filters.add("(w.level >= 11 OR w.level <= 1)") + Web3FilterParams.FILTER_ALL -> {} + } + return filters + } + } +} 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 07fadb9196..ce21b274b5 100644 --- a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt +++ b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt @@ -6,11 +6,14 @@ import androidx.lifecycle.switchMap import androidx.paging.DataSource import androidx.room.RoomRawQuery import dagger.hilt.android.qualifiers.ApplicationContext +import one.mixin.android.db.MixinDatabase +import one.mixin.android.db.provider.Web3DataProvider import one.mixin.android.api.request.AddressSearchRequest import one.mixin.android.api.request.web3.EstimateFeeRequest import one.mixin.android.api.request.web3.WalletRequest import one.mixin.android.api.service.RouteService import one.mixin.android.crypto.CryptoWalletHelper +import one.mixin.android.db.WalletDatabase import one.mixin.android.db.property.Web3PropertyHelper import one.mixin.android.db.web3.Web3AddressDao import one.mixin.android.db.web3.Web3ChainDao @@ -102,7 +105,8 @@ constructor( } fun allWeb3Transaction(filterParams: Web3FilterParams): DataSource.Factory { - return web3TransactionDao.allTransactions(filterParams.buildQuery()).map { transaction -> + val database = WalletDatabase.getDatabase(context) + return Web3DataProvider.allTransactions(database, filterParams).map { transaction -> val assetIds = transaction.senders.map { it.assetId } + transaction.receivers.map { it.assetId } + (transaction.approvals?.map { it.assetId } ?: emptyList()) val tokens = web3TokenDao.findWeb3TokenItemsByIdsSync(filterParams.walletId, assetIds.distinct()).associateBy { it.assetId } transaction.copy( 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..f4324c7b64 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 @@ -46,81 +46,5 @@ class Web3FilterParams( return "${formatter.format(start)} - ${formatter.format(end)}" } } - - fun formatDate(timestamp: Long): String { - val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd") - .withZone(ZoneId.systemDefault()) - return formatter.format(Instant.ofEpochMilli(timestamp)) - } - - fun buildQuery(): SimpleSQLiteQuery { - val filters = mutableListOf() - - tokenItems?.let { - if (it.isNotEmpty()) { - val tokenIds = it.joinToString(", ") { token -> "'${token.assetId}'" } - filters.add("(w.send_asset_id IN ($tokenIds) OR w.receive_asset_id IN ($tokenIds))") - } - } - - walletId.let { - filters.add("w.address IN (SELECT destination FROM addresses WHERE wallet_id = '$it')") - } - - tokenFilterType.let { - when (it) { - Web3TokenFilterType.SEND -> filters.add("w.transaction_type = 'transfer_out'") - Web3TokenFilterType.RECEIVE -> filters.add("w.transaction_type = 'transfer_in'") - Web3TokenFilterType.APPROVAL -> filters.add("w.transaction_type = 'approval'") - Web3TokenFilterType.SWAP -> filters.add("w.transaction_type = 'swap'") - Web3TokenFilterType.PENDING -> filters.add("w.status = '${TransactionStatus.PENDING.value}'") - Web3TokenFilterType.ALL -> {} - } - } - - startTime?.let { - filters.add("w.transaction_at >= '${Instant.ofEpochMilli(it)}'") - } - - endTime?.let { - filters.add("w.transaction_at <= '${Instant.ofEpochMilli(it + 24 * 60 * 60 * 1000)}'") - } - - when (level and FILTER_MASK) { - FILTER_GOOD_ONLY -> filters.add("w.level >= 11") // Good - FILTER_GOOD_AND_UNKNOWN -> filters.add("w.level >= 10") // Good + Unknown - FILTER_GOOD_AND_SPAM -> filters.add("(w.level >= 11 OR w.level <= 1)") // Good + Spam - FILTER_ALL -> { /* Good + Unknown + Spam */ } - } - - val whereSql = if (filters.isEmpty()) { - "" - } else { - "WHERE ${filters.joinToString(" AND ")}" - } - - val orderSql = when (order) { - SortOrder.Recent -> "ORDER BY w.transaction_at DESC" - SortOrder.Oldest -> "ORDER BY w.transaction_at ASC" - else -> "" - } - - 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.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 " + - "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 " + - "$whereSql $orderSql" - ) - } }