Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 17:10:55 +04:00
parent 91200c4973
commit 0693835f07
9 changed files with 99 additions and 13 deletions

View file

@ -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')"
]
}
}

View file

@ -62,20 +62,21 @@ interface ExpressHistoryDao {
): Flow<List<ExpressExchangeEntity>>
/**
* 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,

View file

@ -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(

View file

@ -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<Throwable, ExpressAsset> = 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]

View file

@ -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<ExpressHistoryPage> = 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<ExpressExchangeEntity>,

View file

@ -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,
)
}
}
}

View file

@ -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<Lce<Throwable, List<ExpressAsset>>>
suspend fun getOrFetch(userWalletId: UserWalletId, assetId: ExpressAsset.ID): Either<Throwable, ExpressAsset>
}

View file

@ -23,4 +23,21 @@ interface TxHistoryRepositoryV2 {
currency: CryptoCurrency,
fromCreatedAtMillis: Long,
): Flow<List<ExpressTx>>
}
/**
* 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<ExpressHistoryPage>
}
data class ExpressHistoryPage(
val items: List<ExpressTx>,
val hasMore: Boolean,
)

View file

@ -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<List<ExpressTx>>()
override fun getIndexedExpressHistory(
userWalletId: UserWalletId,
currency: CryptoCurrency,
limit: Int,
) = emptyFlow<ExpressHistoryPage>()
fun loadedItemsCount(): Int = batchFlow.state.value.data.sumOf { batch -> batch.data.items.size }
fun status(): PaginationStatus<*> = batchFlow.state.value.status