Updated on 2026-08-14
This commit is contained in:
parent
3f5f3ef68b
commit
d1a09370c7
39 changed files with 317 additions and 301 deletions
|
|
@ -12,6 +12,9 @@ internal class RuntimeUserWalletsStore(
|
|||
private val walletsStateHolder: WalletsStateHolder,
|
||||
) : UserWalletsStore {
|
||||
|
||||
override val selectedUserWalletOrNull: UserWallet?
|
||||
get() = walletsStateHolder.userWalletsListManager?.selectedUserWalletSync
|
||||
|
||||
override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? {
|
||||
return walletsStateHolder.userWalletsListManager
|
||||
?.userWallets
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ 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.*
|
||||
import dagger.Module
|
||||
|
|
@ -59,12 +58,6 @@ class ProxyModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryManager(appStateHolder: AppStateHolder): TxHistoryManager {
|
||||
return TxHistoryManagerImpl(appStateHolder = appStateHolder)
|
||||
}
|
||||
|
||||
// regions FeatureConsumers
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
|
||||
interface UserWalletsStore {
|
||||
|
||||
val selectedUserWalletOrNull: UserWallet?
|
||||
|
||||
suspend fun getSyncOrNull(key: UserWalletId): UserWallet?
|
||||
}
|
||||
|
|
@ -10,7 +10,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.txhistory.di
|
||||
|
||||
import com.tangem.data.txhistory.repository.MockTxHistoryRepository
|
||||
import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,5 +16,11 @@ internal object TxHistoryDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryRepository(): TxHistoryRepository = MockTxHistoryRepository()
|
||||
fun provideTxHistoryRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
): TxHistoryRepository = DefaultTxHistoryRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsStore = userWalletsStore,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
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 = System.currentTimeMillis(),
|
||||
direction = TxHistoryItem.TransactionDirection.Incoming("address"),
|
||||
status = TxHistoryItem.TxStatus.Confirmed,
|
||||
type = TxHistoryItem.TransactionType.Transfer,
|
||||
amount = BigDecimal("1000000000.5"),
|
||||
)
|
||||
|
||||
private val txHistoryItem2 = TxHistoryItem(
|
||||
txHash = "noster",
|
||||
timestamp = 1689844346000,
|
||||
direction = TxHistoryItem.TransactionDirection.Incoming("address2"),
|
||||
status = TxHistoryItem.TxStatus.Unconfirmed,
|
||||
type = TxHistoryItem.TransactionType.Transfer,
|
||||
amount = BigDecimal("1000000000.5"),
|
||||
)
|
||||
|
||||
private val txHistoryItem3 = TxHistoryItem(
|
||||
txHash = "noster",
|
||||
timestamp = 1689757946000,
|
||||
direction = TxHistoryItem.TransactionDirection.Outgoing("address3"),
|
||||
status = TxHistoryItem.TxStatus.Confirmed,
|
||||
type = TxHistoryItem.TransactionType.Transfer,
|
||||
amount = BigDecimal("1000000000.5"),
|
||||
)
|
||||
|
||||
private val txHistoryItem4 = TxHistoryItem(
|
||||
txHash = "noster",
|
||||
timestamp = 1689671546000,
|
||||
direction = TxHistoryItem.TransactionDirection.Incoming("address4"),
|
||||
status = TxHistoryItem.TxStatus.Confirmed,
|
||||
type = TxHistoryItem.TransactionType.Transfer,
|
||||
amount = BigDecimal("1000000000.5"),
|
||||
)
|
||||
|
||||
private val txHistoryItem5 = TxHistoryItem(
|
||||
txHash = "noster",
|
||||
timestamp = 1689585146000,
|
||||
direction = TxHistoryItem.TransactionDirection.Outgoing("address5"),
|
||||
status = TxHistoryItem.TxStatus.Unconfirmed,
|
||||
type = TxHistoryItem.TransactionType.Transfer,
|
||||
amount = BigDecimal("1000000000.5"),
|
||||
)
|
||||
|
||||
private val txHistoryItem6 = TxHistoryItem(
|
||||
txHash = "noster",
|
||||
timestamp = 1689585146000,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
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.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class DefaultTxHistoryRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
) : TxHistoryRepository {
|
||||
|
||||
override suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int {
|
||||
val userWallet = getUserWallet()
|
||||
val state = walletManagersFacade.getTxHistoryState(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = networkId,
|
||||
rawDerivationPath = derivationPath,
|
||||
)
|
||||
return when (state) {
|
||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||
TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||
TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||
is TxHistoryState.Success.HasTransactions -> state.txCount
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTxHistoryItems(
|
||||
networkId: Network.ID,
|
||||
derivationPath: String?,
|
||||
pageSize: Int,
|
||||
): Flow<PagingData<TxHistoryItem>> {
|
||||
val userWallet = getUserWallet()
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
TxHistoryPagingSource(
|
||||
loadPage = { page: Int, pageSize: Int ->
|
||||
walletManagersFacade.getTxHistoryItems(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = networkId,
|
||||
rawDerivationPath = derivationPath,
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
).flow
|
||||
}
|
||||
|
||||
private fun getUserWallet(): UserWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) {
|
||||
"Selected wallet must not be null"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,14 @@ 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
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
||||
private const val INITIAL_PAGE = 1
|
||||
|
||||
internal class TxHistoryPagingSource : PagingSource<Int, TxHistoryItem>() {
|
||||
internal class TxHistoryPagingSource(
|
||||
private val loadPage: suspend (page: Int, pageSize: Int) -> PaginationWrapper<TxHistoryItem>,
|
||||
) : PagingSource<Int, TxHistoryItem>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, TxHistoryItem>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
|
|
@ -19,20 +21,12 @@ internal class TxHistoryPagingSource : PagingSource<Int, TxHistoryItem>() {
|
|||
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
|
||||
val result = loadPage(currentPage, params.loadSize)
|
||||
|
||||
LoadResult.Page(
|
||||
data = result,
|
||||
data = result.items,
|
||||
prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null,
|
||||
// TODO: handle end of reached [REDACTED_JIRA]
|
||||
nextKey = null,
|
||||
nextKey = if (result.page < result.totalPages) currentPage.plus(1) else null,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Tangem libraries */
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.walletmanager
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -12,10 +13,11 @@ import com.tangem.domain.common.util.hasDerivation
|
|||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import com.tangem.domain.walletmanager.utils.SdkTokenConverter
|
||||
import com.tangem.domain.walletmanager.utils.UpdateWalletManagerResultFactory
|
||||
import com.tangem.domain.walletmanager.utils.WalletManagerFactory
|
||||
import com.tangem.domain.walletmanager.utils.*
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import timber.log.Timber
|
||||
|
|
@ -32,24 +34,22 @@ class DefaultWalletManagersFacade(
|
|||
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
|
||||
private val walletManagerFactory by lazy { WalletManagerFactory(configManager) }
|
||||
private val sdkTokenConverter by lazy { SdkTokenConverter() }
|
||||
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
|
||||
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter() }
|
||||
|
||||
override suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
extraTokens: Set<CryptoCurrency.Token>,
|
||||
): UpdateWalletManagerResult {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find a user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
|
||||
return getAndUpdateWalletManager(userWallet, blockchain, extraTokens)
|
||||
}
|
||||
|
||||
override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find a user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
|
||||
|
|
@ -63,6 +63,57 @@ class DefaultWalletManagersFacade(
|
|||
.orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getTxHistoryState(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
rawDerivationPath: String?,
|
||||
): TxHistoryState {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
val derivationPath = rawDerivationPath?.let(::DerivationPath)
|
||||
val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) {
|
||||
"Unable to get a wallet manager for blockchain: $blockchain"
|
||||
}
|
||||
return walletManager
|
||||
.getTransactionHistoryState(walletManager.wallet.address)
|
||||
.let(txHistoryStateConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
rawDerivationPath: String?,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
val derivationPath = rawDerivationPath?.let(::DerivationPath)
|
||||
val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) {
|
||||
"Unable to get a wallet manager for blockchain: $blockchain"
|
||||
}
|
||||
val itemsResult = walletManager.getTransactionsHistory(
|
||||
address = walletManager.wallet.address,
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
)
|
||||
|
||||
return when (itemsResult) {
|
||||
is Result.Success -> PaginationWrapper(
|
||||
page = itemsResult.data.page,
|
||||
totalPages = itemsResult.data.totalPages,
|
||||
itemsOnPage = itemsResult.data.itemsOnPage,
|
||||
items = txHistoryItemConverter.convertList(itemsResult.data.items),
|
||||
)
|
||||
is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId) =
|
||||
requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find a user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
private suspend fun getAndUpdateWalletManager(
|
||||
userWallet: UserWallet,
|
||||
blockchain: Blockchain,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package com.tangem.domain.walletmanager
|
|||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -26,4 +29,35 @@ interface WalletManagersFacade {
|
|||
): UpdateWalletManagerResult
|
||||
|
||||
suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String
|
||||
|
||||
/**
|
||||
* Returns transactions count
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param networkId The network ID.
|
||||
* @param rawDerivationPath Derivation path in raw form.
|
||||
|
||||
*/
|
||||
suspend fun getTxHistoryState(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
rawDerivationPath: String?,
|
||||
): TxHistoryState
|
||||
|
||||
/**
|
||||
* Returns transaction history items wrapped to pagination
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param networkId The network ID.
|
||||
* @param rawDerivationPath Derivation path in raw form.
|
||||
* @param page Pagination page.
|
||||
* @param pageSize Pagination size.
|
||||
*/
|
||||
suspend fun getTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
rawDerivationPath: String?,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
): PaginationWrapper<TxHistoryItem>
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.common.TransactionStatus
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem as SdkTransactionHistoryItem
|
||||
|
||||
internal class SdkTransactionHistoryItemConverter : Converter<SdkTransactionHistoryItem, TxHistoryItem> {
|
||||
|
||||
override fun convert(value: SdkTransactionHistoryItem): TxHistoryItem = TxHistoryItem(
|
||||
txHash = value.txHash,
|
||||
timestampInMillis = value.timestamp,
|
||||
direction = when (val direction = value.direction) {
|
||||
is SdkTransactionHistoryItem.TransactionDirection.Incoming ->
|
||||
TxHistoryItem.TransactionDirection.Incoming(direction.from)
|
||||
is SdkTransactionHistoryItem.TransactionDirection.Outgoing ->
|
||||
TxHistoryItem.TransactionDirection.Outgoing(direction.to)
|
||||
},
|
||||
status = when (value.status) {
|
||||
TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed
|
||||
TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed
|
||||
},
|
||||
type = when (value.type) {
|
||||
TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer
|
||||
},
|
||||
amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryState
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryState as SdkTransactionHistoryState
|
||||
|
||||
internal class SdkTransactionHistoryStateConverter : Converter<SdkTransactionHistoryState, TxHistoryState> {
|
||||
|
||||
override fun convert(value: TransactionHistoryState): TxHistoryState = when (value) {
|
||||
is TransactionHistoryState.Success.Empty -> TxHistoryState.Success.Empty
|
||||
is TransactionHistoryState.Success.HasTransactions -> TxHistoryState.Success.HasTransactions(value.txCount)
|
||||
is TransactionHistoryState.Failed.FetchError -> TxHistoryState.Failed.FetchError(value.exception)
|
||||
is TransactionHistoryState.NotImplemented -> TxHistoryState.NotImplemented
|
||||
}
|
||||
}
|
||||
|
|
@ -14,4 +14,6 @@ dependencies {
|
|||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
}
|
||||
1
domain/txhistory/models/.gitignore
vendored
Normal file
1
domain/txhistory/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
4
domain/txhistory/models/build.gradle.kts
Normal file
4
domain/txhistory/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.txhistory.models
|
||||
|
||||
data class PaginationWrapper<T>(
|
||||
val page: Int,
|
||||
val totalPages: Int,
|
||||
val itemsOnPage: Int,
|
||||
val items: List<T>,
|
||||
)
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.domain.txhistory.model
|
||||
package com.tangem.domain.txhistory.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class TxHistoryItem(
|
||||
val txHash: String,
|
||||
val timestamp: Long,
|
||||
val timestampInMillis: Long,
|
||||
val direction: TransactionDirection,
|
||||
val status: TxStatus,
|
||||
val type: TransactionType,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.txhistory.error
|
||||
package com.tangem.domain.txhistory.models
|
||||
|
||||
sealed class TxHistoryListError : Throwable() {
|
||||
data class DataError(override val cause: Throwable) : TxHistoryListError()
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.txhistory.models
|
||||
|
||||
sealed class TxHistoryState {
|
||||
|
||||
sealed class Success : TxHistoryState() {
|
||||
object Empty : Success()
|
||||
data class HasTransactions(val txCount: Int) : Success()
|
||||
}
|
||||
|
||||
sealed class Failed : TxHistoryState() {
|
||||
data class FetchError(val exception: Exception) : Failed()
|
||||
}
|
||||
|
||||
object NotImplemented : TxHistoryState()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.txhistory.error
|
||||
package com.tangem.domain.txhistory.models
|
||||
|
||||
sealed class TxHistoryStateError : Throwable() {
|
||||
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
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 com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TxHistoryRepository {
|
||||
|
||||
@Throws(TxHistoryStateError::class)
|
||||
suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int
|
||||
suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int
|
||||
|
||||
@Throws(TxHistoryListError::class)
|
||||
fun getTxHistoryItems(networkId: String, pageSize: Int): Flow<PagingData<TxHistoryItem>>
|
||||
fun getTxHistoryItems(
|
||||
networkId: Network.ID,
|
||||
derivationPath: String?,
|
||||
pageSize: Int,
|
||||
): Flow<PagingData<TxHistoryItem>>
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ 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.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.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> {
|
||||
suspend operator fun invoke(networkId: Network.ID, derivationPath: String?): Either<TxHistoryStateError, Int> {
|
||||
return either {
|
||||
catch(
|
||||
block = { repository.getTxHistoryItemsCount(networkId, derivationPath) },
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ package com.tangem.domain.txhistory.usecase
|
|||
import androidx.paging.PagingData
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.txhistory.error.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.model.TxHistoryItem
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
|
|
@ -14,12 +15,13 @@ private const val DEFAULT_PAGE_SIZE = 20
|
|||
class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) {
|
||||
|
||||
operator fun invoke(
|
||||
networkId: String,
|
||||
networkId: Network.ID,
|
||||
derivationPath: String?,
|
||||
pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||
): Either<TxHistoryListError, Flow<PagingData<TxHistoryItem>>> {
|
||||
return either {
|
||||
repository
|
||||
.getTxHistoryItems(networkId = networkId, pageSize = pageSize)
|
||||
.getTxHistoryItems(networkId = networkId, derivationPath = derivationPath, pageSize = pageSize)
|
||||
.catch { raise(TxHistoryListError.DataError(it)) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(deps.compose.coil)
|
||||
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
@ -37,6 +38,9 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tokendetails.api)
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
|||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.txhistory.error.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.error.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.model.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletLoading
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.tangem.common.Provider
|
|||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.txhistory.error.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.model.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
|||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.TransactionState
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.txhistory.error.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import android.text.format.DateUtils
|
|||
import androidx.paging.*
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.components.transactions.TransactionState
|
||||
import com.tangem.domain.txhistory.model.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTxHistoryState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTxHistoryState.TxHistoryItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
|
|
@ -180,7 +180,7 @@ internal class WalletTxHistoryItemFlowConverter(
|
|||
*
|
||||
* @see [convert]
|
||||
*/
|
||||
private fun TxHistoryItem.getRawTimestamp() = this.timestamp.toString()
|
||||
private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString()
|
||||
|
||||
private fun TxHistoryItemState?.getTimestamp(): Long? {
|
||||
return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.cachedIn
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.domain.card.*
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
|
||||
|
|
@ -143,23 +143,30 @@ internal class WalletViewModel @Inject constructor(
|
|||
|
||||
private fun updateByTxHistory(index: Int) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val blockchain = getWallet(index).scanResponse.cardTypesResolver.getBlockchain()
|
||||
val wallet = getWallet(index)
|
||||
val blockchain = wallet.scanResponse.cardTypesResolver.getBlockchain()
|
||||
val derivationPath = blockchain.derivationPath(style = wallet.scanResponse.card.derivationStyle)?.rawPath
|
||||
|
||||
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
|
||||
networkId = blockchain.id,
|
||||
derivationPath = requireNotNull(blockchain.derivationPath(style = DerivationStyle.LEGACY)).rawPath,
|
||||
networkId = Network.ID(blockchain.id),
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
|
||||
|
||||
txHistoryItemsCountEither.onRight { updateTxHistory(networkId = blockchain.id) }
|
||||
txHistoryItemsCountEither.onRight {
|
||||
updateTxHistory(
|
||||
networkId = Network.ID(blockchain.id),
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
updateNotifications(index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTxHistory(networkId: String) {
|
||||
private fun updateTxHistory(networkId: Network.ID, derivationPath: String?) {
|
||||
uiState = stateFactory.getLoadedTxHistoryState(
|
||||
txHistoryEither = txHistoryItemsUseCase(networkId = networkId).map { it.cachedIn(viewModelScope) },
|
||||
txHistoryEither = txHistoryItemsUseCase(networkId, derivationPath).map { it.cachedIn(viewModelScope) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ okHttp-prettyLogging = "3.1.0"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "develop-297"
|
||||
tangemBlockchainSdk = "develop-306"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-280"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
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>
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tangem.lib.crypto.models.txhistory
|
||||
|
||||
enum class ProxyTransactionStatus { Confirmed, Unconfirmed }
|
||||
|
|
@ -107,6 +107,7 @@ include(":domain:tokens:models")
|
|||
include(":domain:wallets")
|
||||
include(":domain:wallets:models")
|
||||
include(":domain:txhistory")
|
||||
include(":domain:txhistory:models")
|
||||
// endregion Domain modules
|
||||
|
||||
// region Data modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue