Updated on 2026-08-14
This commit is contained in:
parent
68f451d926
commit
8643eb4a90
10 changed files with 256 additions and 30 deletions
|
|
@ -5,9 +5,14 @@ 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.ExpressSwapConverter
|
||||
import com.tangem.data.txhistory.repository.factory.ExpressTransactionAssetFactory
|
||||
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.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
|
|
@ -36,6 +41,7 @@ internal class RefactoredTxHistoryRepository @Inject constructor(
|
|||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
private val expressHistoryDao: ExpressHistoryDao,
|
||||
private val expressTransactionAssetFactory: ExpressTransactionAssetFactory,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TxHistoryRepositoryV2 {
|
||||
|
|
@ -85,37 +91,67 @@ internal class RefactoredTxHistoryRepository @Inject constructor(
|
|||
).distinctUntilChanged(),
|
||||
flow4 = expressHistoryDao.getProvidersById().distinctUntilChanged(),
|
||||
transform = { outgoingSwaps, incomingSwaps, onramps, providers ->
|
||||
buildList<ExpressTx> {
|
||||
fun String.expressProvider() = providers[this]?.let(expressProviderConverter::convert)
|
||||
outgoingSwaps.forEach { entity ->
|
||||
val input = ExpressSwapConverter.Input(
|
||||
entity = entity,
|
||||
provider = entity.providerId.expressProvider(),
|
||||
isOutgoing = true,
|
||||
)
|
||||
add(swapConverter.convert(input))
|
||||
}
|
||||
incomingSwaps.forEach { entity ->
|
||||
val input = ExpressSwapConverter.Input(
|
||||
entity = entity,
|
||||
provider = entity.providerId.expressProvider(),
|
||||
isOutgoing = false,
|
||||
)
|
||||
add(swapConverter.convert(input))
|
||||
}
|
||||
onramps.forEach { entity ->
|
||||
val input = ExpressOnrampConverter.Input(entity, entity.providerId.expressProvider())
|
||||
add(onrampConverter.convert(input))
|
||||
}
|
||||
}
|
||||
// An exchange row may satisfy both swap queries only in degenerate cases;
|
||||
// keep the outgoing interpretation (added first).
|
||||
.distinctBy { it.txId }
|
||||
buildExpressHistory(
|
||||
userWalletId = userWalletId,
|
||||
outgoingSwaps = outgoingSwaps,
|
||||
incomingSwaps = incomingSwaps,
|
||||
onramps = onramps,
|
||||
providers = providers,
|
||||
)
|
||||
},
|
||||
)
|
||||
emitAll(flow)
|
||||
}.flowOn(dispatchers.io)
|
||||
|
||||
private suspend fun buildExpressHistory(
|
||||
userWalletId: UserWalletId,
|
||||
outgoingSwaps: List<ExpressExchangeEntity>,
|
||||
incomingSwaps: List<ExpressExchangeEntity>,
|
||||
onramps: List<ExpressOnrampEntity>,
|
||||
providers: Map<String, ExpressProviderEntity>,
|
||||
): List<ExpressTx> {
|
||||
val currencies = expressTransactionAssetFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
outgoingSwaps = outgoingSwaps,
|
||||
incomingSwaps = incomingSwaps,
|
||||
onramps = onramps,
|
||||
)
|
||||
fun String.expressProvider() = providers[this]?.let(expressProviderConverter::convert)
|
||||
return buildList {
|
||||
outgoingSwaps.forEach { entity ->
|
||||
val input = ExpressSwapConverter.Input(
|
||||
entity = entity,
|
||||
provider = entity.providerId.expressProvider(),
|
||||
isOutgoing = true,
|
||||
fromCurrency = currencies[entity.from.toAssetId()],
|
||||
toCurrency = currencies[entity.to.toAssetId()],
|
||||
)
|
||||
add(swapConverter.convert(input))
|
||||
}
|
||||
incomingSwaps.forEach { entity ->
|
||||
val input = ExpressSwapConverter.Input(
|
||||
entity = entity,
|
||||
provider = entity.providerId.expressProvider(),
|
||||
isOutgoing = false,
|
||||
fromCurrency = currencies[entity.from.toAssetId()],
|
||||
toCurrency = currencies[entity.to.toAssetId()],
|
||||
)
|
||||
add(swapConverter.convert(input))
|
||||
}
|
||||
onramps.forEach { entity ->
|
||||
val input = ExpressOnrampConverter.Input(
|
||||
entity = entity,
|
||||
provider = entity.providerId.expressProvider(),
|
||||
toCurrency = currencies[entity.to.toAssetId()],
|
||||
)
|
||||
add(onrampConverter.convert(input))
|
||||
}
|
||||
}
|
||||
// An exchange row may satisfy both swap queries only in degenerate cases;
|
||||
// keep the outgoing interpretation (added first).
|
||||
.distinctBy { it.txId }
|
||||
}
|
||||
|
||||
override fun getTxHistoryBatchFlow(batchSize: Int, context: TxHistoryListBatchingContext): TxHistoryListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.express.models.ExpressOnrampStatus
|
|||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressTransactionAsset
|
||||
import com.tangem.domain.express.models.OnrampTransaction
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
|
|
@ -26,7 +27,7 @@ import java.math.BigDecimal
|
|||
internal class ExpressSwapConverter : Converter<ExpressSwapConverter.Input, ExpressTx.Swap> {
|
||||
|
||||
override fun convert(value: Input): ExpressTx.Swap = ExpressTx.Swap(
|
||||
tx = convertExchangeTransaction(value.entity, value.provider),
|
||||
tx = convertExchangeTransaction(value),
|
||||
isOutgoing = value.isOutgoing,
|
||||
txInfo = null,
|
||||
)
|
||||
|
|
@ -35,6 +36,8 @@ internal class ExpressSwapConverter : Converter<ExpressSwapConverter.Input, Expr
|
|||
val entity: ExpressExchangeEntity,
|
||||
val provider: ExpressProvider?,
|
||||
val isOutgoing: Boolean,
|
||||
val fromCurrency: CryptoCurrency? = null,
|
||||
val toCurrency: CryptoCurrency? = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -59,32 +62,40 @@ internal class ExpressOnrampConverter : Converter<ExpressOnrampConverter.Input,
|
|||
id = ExpressAssetId(networkId = entity.to.network, contractAddress = entity.to.contractAddress),
|
||||
amount = (entity.to.actualAmount ?: entity.to.amount).toBigDecimalOrZero(),
|
||||
decimals = entity.to.decimals,
|
||||
cryptoCurrency = value.toCurrency,
|
||||
),
|
||||
),
|
||||
txInfo = null,
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val entity: ExpressOnrampEntity, val provider: ExpressProvider?)
|
||||
data class Input(
|
||||
val entity: ExpressOnrampEntity,
|
||||
val provider: ExpressProvider?,
|
||||
val toCurrency: CryptoCurrency? = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExchangeTransaction(entity: ExpressExchangeEntity, provider: ExpressProvider?): ExchangeTransaction {
|
||||
private fun convertExchangeTransaction(value: ExpressSwapConverter.Input): ExchangeTransaction {
|
||||
val entity = value.entity
|
||||
return ExchangeTransaction(
|
||||
txId = entity.txId,
|
||||
status = ExpressExchangeStatus.fromRaw(entity.status),
|
||||
createdAtMillis = parseIsoMillis(entity.createdAt),
|
||||
provider = provider,
|
||||
provider = value.provider,
|
||||
payinHash = entity.payinHash,
|
||||
payoutHash = entity.payoutHash,
|
||||
fromAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = entity.from.network, contractAddress = entity.from.contractAddress),
|
||||
amount = entity.from.amount.toBigDecimalOrZero(),
|
||||
decimals = entity.from.decimals,
|
||||
cryptoCurrency = value.fromCurrency,
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = entity.to.network, contractAddress = entity.to.contractAddress),
|
||||
amount = (entity.to.actualAmount ?: entity.to.amount).toBigDecimalOrZero(),
|
||||
decimals = entity.to.decimals,
|
||||
cryptoCurrency = value.toCurrency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.data.txhistory.repository.factory
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Resolves a portfolio [CryptoCurrency] for every express asset (network id + contract address) referenced by a
|
||||
* batch of exchange/onramp entities.
|
||||
*
|
||||
* Strategy: read every account of every wallet ONCE (via [MultiAccountListSupplier]) and match each express asset
|
||||
* against the flattened portfolio currencies by network id + contract address. When nothing matches — notably
|
||||
* tokens that are not present in any portfolio — a coin is built for the asset's network as a fallback (for now).
|
||||
*/
|
||||
internal class ExpressTransactionAssetFactory @Inject constructor(
|
||||
private val multiAccountListSupplier: MultiAccountListSupplier,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) {
|
||||
|
||||
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
|
||||
|
||||
/**
|
||||
* Builds a `assetId -> resolved currency` map covering both legs of every swap and the to-leg of every onramp.
|
||||
* Entries whose currency could not be resolved at all (no match and no fallback coin) are omitted.
|
||||
*/
|
||||
suspend fun create(
|
||||
userWalletId: UserWalletId,
|
||||
outgoingSwaps: List<ExpressExchangeEntity>,
|
||||
incomingSwaps: List<ExpressExchangeEntity>,
|
||||
onramps: List<ExpressOnrampEntity>,
|
||||
): Map<ExpressAsset.ID, CryptoCurrency> {
|
||||
val assetIds = buildSet {
|
||||
(outgoingSwaps + incomingSwaps).forEach { entity ->
|
||||
add(entity.from.toAssetId())
|
||||
add(entity.to.toAssetId())
|
||||
}
|
||||
onramps.forEach { entity -> add(entity.to.toAssetId()) }
|
||||
}
|
||||
if (assetIds.isEmpty()) return emptyMap()
|
||||
|
||||
val portfolioCurrencies = multiAccountListSupplier.invoke()
|
||||
.first()
|
||||
.flatMap { accountList -> accountList.flattenCurrencies() }
|
||||
|
||||
val userWallet = userWalletsListRepository.userWalletsSync()
|
||||
.firstOrNull { it.walletId == userWalletId }
|
||||
|
||||
return buildMap {
|
||||
assetIds.forEach { id ->
|
||||
val currency = portfolioCurrencies.findMatching(id) ?: createFallbackCoin(id, userWallet)
|
||||
if (currency != null) put(id, currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrency>.findMatching(id: ExpressAsset.ID): CryptoCurrency? {
|
||||
val isCoin = id.contractAddress == ExpressAsset.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
return firstOrNull { currency ->
|
||||
currency.network.rawId == id.networkId &&
|
||||
if (isCoin) {
|
||||
currency is CryptoCurrency.Coin
|
||||
} else {
|
||||
currency is CryptoCurrency.Token &&
|
||||
currency.contractAddress.equals(id.contractAddress, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO txHistory: tokens that are not in any portfolio cannot be resolved yet — fall back to a coin on the asset's
|
||||
// network.
|
||||
private fun createFallbackCoin(id: ExpressAsset.ID, userWallet: UserWallet?): CryptoCurrency.Coin? {
|
||||
userWallet ?: return null
|
||||
return cryptoCurrencyFactory.createCoin(
|
||||
networkId = id.networkId,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ExpressExchangeEntity.AssetEmbedded.toAssetId(): ExpressAsset.ID =
|
||||
ExpressAsset.ID(networkId = network, contractAddress = contractAddress)
|
||||
|
||||
internal fun ExpressOnrampEntity.AssetEmbedded.toAssetId(): ExpressAsset.ID =
|
||||
ExpressAsset.ID(networkId = network, contractAddress = contractAddress)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.domain.models.network.SdkAmount
|
||||
import com.tangem.domain.models.network.SdkAmountType
|
||||
import com.tangem.blockchain.common.Amount as BlockchainAmount
|
||||
import com.tangem.blockchain.common.AmountType as BlockchainAmountType
|
||||
|
||||
/** Maps the blockchain SDK [BlockchainAmount] to the serializable domain [SdkAmount]. */
|
||||
internal fun BlockchainAmount.toDomain(): SdkAmount = SdkAmount(
|
||||
currencySymbol = currencySymbol,
|
||||
value = value,
|
||||
decimals = decimals,
|
||||
type = type.toDomain(),
|
||||
)
|
||||
|
||||
private fun BlockchainAmountType.toDomain(): SdkAmountType = when (this) {
|
||||
BlockchainAmountType.Coin -> SdkAmountType.Coin
|
||||
BlockchainAmountType.Reserve -> SdkAmountType.Reserve
|
||||
is BlockchainAmountType.FeeResource -> SdkAmountType.FeeResource(name = name)
|
||||
is BlockchainAmountType.Token -> SdkAmountType.Token(contractAddress = token.contractAddress, id = token.id)
|
||||
is BlockchainAmountType.TokenYieldSupply -> SdkAmountType.Token(
|
||||
contractAddress = token.contractAddress,
|
||||
id = token.id,
|
||||
)
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ internal class SdkTransactionHistoryItemConverter(
|
|||
},
|
||||
type = typeConverter.convert(value),
|
||||
amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" },
|
||||
fee = value.fee.toDomain(),
|
||||
)
|
||||
|
||||
private fun SdkTransactionHistoryItem.SourceType.toDomain(): TxInfo.SourceType = when (this) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ internal class TransactionDataToTxHistoryItemConverter(
|
|||
},
|
||||
type = getTransactionType(value),
|
||||
amount = amount,
|
||||
fee = value.fee?.amount?.toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue