diff --git a/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json b/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json index 42576f0e65..16b7afa8d9 100644 --- a/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json +++ b/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "c0f8e7d958a8852440a5bdec5a989f0e", + "identityHash": "1ddc01e929ea21940e7f1e9e35810691", "entities": [ { "tableName": "express_provider", @@ -271,15 +271,16 @@ "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_from_address_from_network_from_contract_address_created_at` ON `${TABLE_NAME}` (`from_address`, `from_network`, `from_contract_address`, `created_at`)" }, { - "name": "index_express_exchange_to_network_to_contract_address_created_at", + "name": "index_express_exchange_payout_address_to_network_to_contract_address_created_at", "unique": false, "columnNames": [ + "payout_address", "to_network", "to_contract_address", "created_at" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`to_network`, `to_contract_address`, `created_at`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_payout_address_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`payout_address`, `to_network`, `to_contract_address`, `created_at`)" } ] }, @@ -650,7 +651,7 @@ ], "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, 'c0f8e7d958a8852440a5bdec5a989f0e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ddc01e929ea21940e7f1e9e35810691')" ] } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt index e11ee0da8a..7e493bc1ef 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt @@ -62,20 +62,21 @@ interface ExpressHistoryDao { ): Flow> /** - * Incoming swaps: the viewed currency is the swap's `to` side. Such a deal was initiated from a - * different coin, so the row is stored under that coin's `from_address` — hence this query is - * cross-address, matched by the `to` asset. Join to on-chain by `payout_hash`. + * Incoming swaps: the viewed currency is the swap's `to` side, so the row is looked up by its `payout_address` + * (where the target assets landed = this currency's address). Join to on-chain by `payout_hash`. */ @Query( """ SELECT * FROM express_exchange - WHERE to_network = :network + WHERE payout_address = :payoutAddress + AND to_network = :network AND to_contract_address = :contract AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses)) ORDER BY created_at DESC """, ) fun observeIncomingSwaps( + payoutAddress: String, network: String, contract: String, fromCreatedAtIso: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt index 6eed9b91c6..adac8047b6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt @@ -12,9 +12,8 @@ import androidx.room.* indices = [ // Outgoing swaps lookup (observeOutgoingSwaps): from-address + from-asset equality, created_at range/sort. Index(value = ["from_address", "from_network", "from_contract_address", "created_at"]), - // Incoming (cross-owner) swaps lookup (observeIncomingSwaps): to-asset equality, created_at range/sort. - // No owner filter here, so to_contract_address in the index is what keeps a popular to-network selective. - Index(value = ["to_network", "to_contract_address", "created_at"]), + // Incoming swaps lookup (observeIncomingSwaps): payout-address + to-asset equality, created_at range/sort. + Index(value = ["payout_address", "to_network", "to_contract_address", "created_at"]), ], ) data class ExpressExchangeEntity( diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt index 449959c978..18a49bd83b 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt @@ -95,6 +95,26 @@ internal class DefaultExpressServiceFetcher @Inject constructor( return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } } } + override suspend fun getOrFetch( + userWalletId: UserWalletId, + assetId: ExpressAsset.ID, + ): Either = either { + // Return the already-loaded asset (in-memory status or persisted cache) without hitting the network. + findLoadedAsset(userWalletId, assetId)?.let { return@either it } + + // Not loaded yet — fetch it from the API, then take it from the refreshed status. + fetch(userWalletId = userWalletId, assetIds = setOf(assetId)).bind() + + findLoadedAsset(userWalletId, assetId) + ?: raise(IllegalStateException("Express asset $assetId is not available after fetch")) + } + + private suspend fun findLoadedAsset(userWalletId: UserWalletId, assetId: ExpressAsset.ID): ExpressAsset? { + return getInitializationStatusInternal(userWalletId).value + .getOrNull() + ?.firstOrNull { it.id == assetId } + } + @Suppress("SuspendFunWithFlowReturnType") private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow { val initializationStatus = initializationStatuses.value[userWalletId] diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt index 60cf192e2b..9a97a375e7 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt @@ -2,8 +2,8 @@ package com.tangem.data.txhistory.repository import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.converter.ExpressProviderConverter -import com.tangem.data.txhistory.repository.converter.ExpressStatusMapper import com.tangem.data.txhistory.repository.converter.ExpressOnrampConverter +import com.tangem.data.txhistory.repository.converter.ExpressStatusMapper import com.tangem.data.txhistory.repository.converter.ExpressSwapConverter import com.tangem.data.txhistory.repository.converter.OnrampCountryConverter import com.tangem.data.txhistory.repository.factory.ExpressTransactionAssetFactory @@ -11,6 +11,7 @@ import com.tangem.data.txhistory.repository.factory.toAssetId import com.tangem.data.txhistory.repository.paging.TxHistoryPageBatchFetcher import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao +import com.tangem.datasource.local.txhistory.db.dao.HistoryIndexDao import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity @@ -25,6 +26,7 @@ import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext import com.tangem.domain.txhistory.model.TxHistoryListConfig import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.repository.ExpressHistoryPage import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.utils.SdkPageConverter @@ -33,16 +35,19 @@ import com.tangem.pagination.BatchListSource import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.format.ISODateTimeFormat import javax.inject.Inject +@Suppress("LongParameterList") internal class RefactoredTxHistoryRepository @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val txHistoryItemsStore: TxHistoryItemsStore, private val expressHistoryDao: ExpressHistoryDao, + private val historyIndexDao: HistoryIndexDao, private val expressTransactionAssetFactory: ExpressTransactionAssetFactory, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, @@ -80,6 +85,7 @@ internal class RefactoredTxHistoryRepository @Inject constructor( activeStatuses = ExpressStatusMapper.activeExchangeStatuses, ).distinctUntilChanged(), flow2 = expressHistoryDao.observeIncomingSwaps( + payoutAddress = address, network = rawNetwork, contract = contract, fromCreatedAtIso = fromCreatedAtIso, @@ -110,6 +116,31 @@ internal class RefactoredTxHistoryRepository @Inject constructor( emitAll(flow) }.flowOn(dispatchers.io) + @OptIn(ExperimentalCoroutinesApi::class) + override fun getIndexedExpressHistory( + userWalletId: UserWalletId, + currency: CryptoCurrency, + limit: Int, + ): Flow = flow { + val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network).orEmpty() + val pages = historyIndexDao + .observePage(addresses = listOf(address), cursor = null, limit = limit) + .map { page -> + IndexWindow( + fromCreatedAtMillis = page.lastOrNull()?.sortTimeMillis ?: 0L, + hasMore = page.size >= limit, + ) + } + .distinctUntilChanged() + .flatMapLatest { window -> + getExpressHistory(userWalletId, currency, window.fromCreatedAtMillis) + .map { express -> ExpressHistoryPage(items = express, hasMore = window.hasMore) } + } + emitAll(pages) + }.flowOn(dispatchers.io) + + private data class IndexWindow(val fromCreatedAtMillis: Long, val hasMore: Boolean) + /** The reactive express-history inputs gathered from the DB in a single [combine] tick. */ private data class ExpressHistorySources( val outgoingSwaps: List, diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressAsset.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressAsset.kt index 0d8b046954..b2df4dc9d1 100644 --- a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressAsset.kt +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressAsset.kt @@ -1,5 +1,6 @@ package com.tangem.domain.express.models +import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.serialization.Serializable /** @@ -39,6 +40,13 @@ data class ExpressAsset( operator fun invoke(networkId: String, contractAddress: String?): ID { return ID(networkId = networkId, contractAddress = contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE) } + + operator fun invoke(cryptoCurrency: CryptoCurrency): ID { + return ID( + networkId = cryptoCurrency.network.rawId, + contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress, + ) + } } } diff --git a/domain/express/src/main/java/com/tangem/domain/express/ExpressServiceFetcher.kt b/domain/express/src/main/java/com/tangem/domain/express/ExpressServiceFetcher.kt index 1d7d7b1dbc..00e7644f4c 100644 --- a/domain/express/src/main/java/com/tangem/domain/express/ExpressServiceFetcher.kt +++ b/domain/express/src/main/java/com/tangem/domain/express/ExpressServiceFetcher.kt @@ -35,4 +35,6 @@ interface ExpressServiceFetcher { * @return A flow emitting Lce states containing either a list of Express assets or an error. */ fun getInitializationStatus(userWalletId: UserWalletId): Flow>> + + suspend fun getOrFetch(userWalletId: UserWalletId, assetId: ExpressAsset.ID): Either } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepositoryV2.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepositoryV2.kt index 624ff8eb2a..2243ae12b9 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepositoryV2.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepositoryV2.kt @@ -23,4 +23,21 @@ interface TxHistoryRepositoryV2 { currency: CryptoCurrency, fromCreatedAtMillis: Long, ): Flow> -} \ No newline at end of file + + /** + * Reactive express history for [currency] paginated via the unified history index: the [limit] most recent index + * rows define the window (their oldest sort time), which bounds [getExpressHistory]. Grow [limit] to load more. + * + * Used as the standalone backbone when there is no on-chain history source for the currency. + */ + fun getIndexedExpressHistory( + userWalletId: UserWalletId, + currency: CryptoCurrency, + limit: Int, + ): Flow +} + +data class ExpressHistoryPage( + val items: List, + val hasMore: Boolean, +) \ No newline at end of file diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManagerTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManagerTest.kt index 801fbd968a..e3fb2bd693 100644 --- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManagerTest.kt +++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManagerTest.kt @@ -11,6 +11,7 @@ import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext import com.tangem.domain.txhistory.model.TxHistoryListConfig import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.repository.ExpressHistoryPage import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter import com.tangem.pagination.BatchFetchResult @@ -231,6 +232,12 @@ internal class TxHistoryListManagerTest { fromCreatedAtMillis: Long, ) = emptyFlow>() + override fun getIndexedExpressHistory( + userWalletId: UserWalletId, + currency: CryptoCurrency, + limit: Int, + ) = emptyFlow() + fun loadedItemsCount(): Int = batchFlow.state.value.data.sumOf { batch -> batch.data.items.size } fun status(): PaginationStatus<*> = batchFlow.state.value.status