Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-25 10:52:33 +05:00
parent ddcb68caf6
commit 782a2ec0f5
21 changed files with 479 additions and 6 deletions

View file

@ -0,0 +1,86 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.blockchain.common.txhistory.TransactionHistoryState
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TxHistoryManager
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem
import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState
import com.tangem.lib.crypto.models.txhistory.ProxyTransactionStatus
class TxHistoryManagerImpl(
private val appStateHolder: AppStateHolder,
) : TxHistoryManager {
override suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
val state = walletManager.getTransactionHistoryState(address = walletManager.wallet.address)
return state.mapToProxy()
}
override suspend fun getTxHistoryItems(
networkId: String,
derivationPath: String?,
page: Int,
pageSize: Int,
): List<ProxyTransactionHistoryItem> {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
val itemsResult = walletManager.getTransactionsHistory(
address = walletManager.wallet.address,
page = page,
pageSize = pageSize,
)
return when (itemsResult) {
is Result.Success -> itemsResult.data.map { historyItem -> historyItem.mapToProxy() }
is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage)
}
}
private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList())
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
return requireNotNull(walletManager) { "no wallet manager found" }
}
private fun TransactionHistoryState.mapToProxy(): ProxyTransactionHistoryState {
return when (this) {
TransactionHistoryState.Success.Empty -> ProxyTransactionHistoryState.Success.Empty
is TransactionHistoryState.Failed.FetchError -> ProxyTransactionHistoryState.Failed.FetchError(exception)
TransactionHistoryState.NotImplemented -> ProxyTransactionHistoryState.NotImplemented
is TransactionHistoryState.Success.HasTransactions ->
ProxyTransactionHistoryState.Success.HasTransactions(txCount)
}
}
private fun TransactionHistoryItem.mapToProxy() = ProxyTransactionHistoryItem(
txHash = txHash,
timestamp = timestamp,
direction = when (val direction = direction) {
is TransactionHistoryItem.TransactionDirection.Incoming ->
ProxyTransactionHistoryItem.TransactionDirection.Incoming(direction.from)
is TransactionHistoryItem.TransactionDirection.Outgoing ->
ProxyTransactionHistoryItem.TransactionDirection.Outgoing(direction.to)
},
status = when (status) {
TransactionStatus.Confirmed -> ProxyTransactionStatus.Confirmed
TransactionStatus.Unconfirmed -> ProxyTransactionStatus.Unconfirmed
},
type = when (type) {
TransactionHistoryItem.TransactionType.Transfer -> ProxyTransactionHistoryItem.TransactionType.Transfer
},
amount = ProxyAmount(
currencySymbol = amount.currencySymbol,
value = requireNotNull(amount.value) { "Amount value must not be null" },
decimals = amount.decimals,
),
)
}

View file

@ -8,11 +8,9 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.TxHistoryManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.DerivationManagerImpl
import com.tangem.tap.proxy.TransactionManagerImpl
import com.tangem.tap.proxy.UserWalletManagerImpl
import com.tangem.tap.proxy.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -61,6 +59,12 @@ class ProxyModule {
)
}
@Provides
@Singleton
fun provideTxHistoryManager(appStateHolder: AppStateHolder): TxHistoryManager {
return TxHistoryManagerImpl(appStateHolder = appStateHolder)
}
// regions FeatureConsumers
@Provides
@Singleton

1
data/txhistory/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,21 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.txhistory"
}
dependencies {
implementation(projects.domain.txhistory)
implementation(deps.kotlin.coroutines)
implementation(deps.androidx.paging.runtime)
implementation(deps.arrow.core)
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,18 @@
package com.tangem.data.txhistory.di
import com.tangem.data.txhistory.repository.MockTxHistoryRepository
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TxHistoryDataModule {
@Provides
@Singleton
fun provideTxHistoryRepository(): TxHistoryRepository = MockTxHistoryRepository()
}

View file

@ -0,0 +1,70 @@
package com.tangem.data.txhistory.mock
import com.tangem.domain.txhistory.model.TxHistoryItem
import java.math.BigDecimal
internal object MockTxHistoryItems {
private val txHistoryItem1 = TxHistoryItem(
txHash = "noster",
timestamp = 1689930746,
direction = TxHistoryItem.TransactionDirection.Incoming("address"),
status = TxHistoryItem.TxStatus.Confirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal("1000000000.5"),
)
private val txHistoryItem2 = TxHistoryItem(
txHash = "noster",
timestamp = 1689844346,
direction = TxHistoryItem.TransactionDirection.Incoming("address2"),
status = TxHistoryItem.TxStatus.Unconfirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal("1000000000.5"),
)
private val txHistoryItem3 = TxHistoryItem(
txHash = "noster",
timestamp = 1689757946,
direction = TxHistoryItem.TransactionDirection.Outgoing("address3"),
status = TxHistoryItem.TxStatus.Confirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal("1000000000.5"),
)
private val txHistoryItem4 = TxHistoryItem(
txHash = "noster",
timestamp = 1689671546,
direction = TxHistoryItem.TransactionDirection.Incoming("address4"),
status = TxHistoryItem.TxStatus.Confirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal("1000000000.5"),
)
private val txHistoryItem5 = TxHistoryItem(
txHash = "noster",
timestamp = 1689585146,
direction = TxHistoryItem.TransactionDirection.Outgoing("address5"),
status = TxHistoryItem.TxStatus.Unconfirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal("1000000000.5"),
)
private val txHistoryItem6 = TxHistoryItem(
txHash = "noster",
timestamp = 1689585146,
direction = TxHistoryItem.TransactionDirection.Incoming("address6"),
status = TxHistoryItem.TxStatus.Confirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal("1000000000.5"),
)
val txHistoryItems = listOf(
txHistoryItem1,
txHistoryItem2,
txHistoryItem3,
txHistoryItem4,
txHistoryItem5,
txHistoryItem6,
)
}

View file

@ -0,0 +1,25 @@
package com.tangem.data.txhistory.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource
import com.tangem.domain.txhistory.model.TxHistoryItem
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import kotlinx.coroutines.flow.Flow
internal class MockTxHistoryRepository : TxHistoryRepository {
override suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int {
return 0
}
override fun getTxHistoryItems(networkId: String, pageSize: Int): Flow<PagingData<TxHistoryItem>> {
return Pager(
config = PagingConfig(
pageSize = pageSize,
),
pagingSourceFactory = { TxHistoryPagingSource() },
).flow
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.data.txhistory.repository.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.data.txhistory.mock.MockTxHistoryItems
import com.tangem.domain.txhistory.model.TxHistoryItem
private const val INITIAL_PAGE = 1
internal class TxHistoryPagingSource : PagingSource<Int, TxHistoryItem>() {
override fun getRefreshKey(state: PagingState<Int, TxHistoryItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TxHistoryItem> {
val currentPage = params.key ?: INITIAL_PAGE
return try {
// TODO: [REDACTED_JIRA]
// val result = txHistoryManager.getTxHistoryItems(
// networkId = networkId,
// derivationPath = derivationPath,
// page = currentPage,
// pageSize = params.loadSize,
// )
val result = MockTxHistoryItems.txHistoryItems
LoadResult.Page(
data = result,
prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null,
// TODO: handle end of reached [REDACTED_JIRA]
nextKey = null,
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}

1
domain/txhistory/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,17 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.domain.txhistory"
}
dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)
implementation(deps.androidx.paging.runtime)
implementation(projects.core.utils)
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.txhistory.error
sealed class TxHistoryListError : Throwable() {
data class DataError(override val cause: Throwable) : TxHistoryListError()
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.txhistory.error
sealed class TxHistoryStateError : Throwable() {
object TxHistoryNotImplemented : TxHistoryStateError()
object EmptyTxHistories : TxHistoryStateError()
data class DataError(override val cause: Throwable) : TxHistoryStateError()
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.txhistory.model
import java.math.BigDecimal
data class TxHistoryItem(
val txHash: String,
val timestamp: Long,
val direction: TransactionDirection,
val status: TxStatus,
val type: TransactionType,
val amount: BigDecimal,
) {
sealed interface TransactionDirection {
data class Incoming(val from: String) : TransactionDirection
data class Outgoing(val to: String) : TransactionDirection
}
sealed interface TransactionType {
object Transfer : TransactionType
}
enum class TxStatus { Confirmed, Unconfirmed }
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.txhistory.repository
import androidx.paging.PagingData
import com.tangem.domain.txhistory.error.TxHistoryListError
import com.tangem.domain.txhistory.error.TxHistoryStateError
import com.tangem.domain.txhistory.model.TxHistoryItem
import kotlinx.coroutines.flow.Flow
interface TxHistoryRepository {
@Throws(TxHistoryStateError::class)
suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int
@Throws(TxHistoryListError::class)
fun getTxHistoryItems(networkId: String, pageSize: Int): Flow<PagingData<TxHistoryItem>>
}

View file

@ -0,0 +1,27 @@
package com.tangem.domain.txhistory.usecase
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.txhistory.error.TxHistoryStateError
import com.tangem.domain.txhistory.repository.TxHistoryRepository
class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) {
suspend operator fun invoke(networkId: String, derivationPath: String): Either<TxHistoryStateError, Int> {
return either {
catch(
block = { repository.getTxHistoryItemsCount(networkId, derivationPath) },
catch = { e ->
raise(
when (e) {
is TxHistoryStateError.TxHistoryNotImplemented -> e
is TxHistoryStateError.EmptyTxHistories -> e
else -> TxHistoryStateError.DataError(e)
},
)
},
)
}
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.domain.txhistory.usecase
import androidx.paging.PagingData
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.Raise
import arrow.core.raise.recover
import arrow.core.right
import com.tangem.domain.txhistory.error.TxHistoryListError
import com.tangem.domain.txhistory.model.TxHistoryItem
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
private const val DEFAULT_PAGE_SIZE = 20
class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) {
operator fun invoke(
networkId: String,
pageSize: Int = DEFAULT_PAGE_SIZE,
): Flow<Either<TxHistoryListError,
PagingData<TxHistoryItem>,>,> {
return channelFlow {
recover(
block = {
getTxHistoryItems(
networkId = networkId,
pageSize = pageSize,
).collect { items ->
send(items.right())
}
},
recover = { error -> send(error.left()) },
)
}
}
private fun Raise<TxHistoryListError>.getTxHistoryItems(
networkId: String,
pageSize: Int,
): Flow<PagingData<TxHistoryItem>> {
return repository
.getTxHistoryItems(networkId = networkId, pageSize = pageSize)
.catch { raise(TxHistoryListError.DataError(it)) }
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem
import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState
interface TxHistoryManager {
@Throws(IllegalStateException::class)
suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState
@Throws(IllegalStateException::class)
suspend fun getTxHistoryItems(
networkId: String,
derivationPath: String?,
page: Int,
pageSize: Int,
): List<ProxyTransactionHistoryItem>
}

View file

@ -0,0 +1,21 @@
package com.tangem.lib.crypto.models.txhistory
import com.tangem.lib.crypto.models.ProxyAmount
data class ProxyTransactionHistoryItem(
val txHash: String,
val timestamp: Long,
val direction: TransactionDirection,
val status: ProxyTransactionStatus,
val type: TransactionType,
val amount: ProxyAmount,
) {
sealed interface TransactionDirection {
data class Incoming(val from: String) : TransactionDirection
data class Outgoing(val to: String) : TransactionDirection
}
sealed interface TransactionType {
object Transfer : TransactionType
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.lib.crypto.models.txhistory
sealed class ProxyTransactionHistoryState {
sealed class Success : ProxyTransactionHistoryState() {
object Empty : Success()
data class HasTransactions(val txCount: Int) : Success()
}
sealed class Failed : ProxyTransactionHistoryState() {
data class FetchError(val exception: Exception) : Failed()
}
object NotImplemented : ProxyTransactionHistoryState()
}

View file

@ -0,0 +1,3 @@
package com.tangem.lib.crypto.models.txhistory
enum class ProxyTransactionStatus { Confirmed, Unconfirmed }

View file

@ -83,6 +83,7 @@ include(":domain:settings")
include(":domain:tokens")
include(":domain:wallets")
include(":domain:wallets:models")
include(":domain:txhistory")
// endregion Domain modules
// region Data modules
@ -90,5 +91,6 @@ include(":data:common")
include(":data:card")
include(":data:tokens")
include(":data:source:preferences")
// endregion Data modules
include(":data:settings")
include(":data:txhistory")
// endregion Data modules