Updated on 2026-08-14
This commit is contained in:
commit
c32a5fa18d
711 changed files with 18558 additions and 4312 deletions
|
|
@ -0,0 +1,152 @@
|
|||
package com.tangem.data.account.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Implementation of [MainAccountTokensMigration] for migrating tokens associated with a main account.
|
||||
* The migration process involves transferring unassigned tokens from the main account to a selected account.
|
||||
*
|
||||
* @property accountsResponseStoreFactory Factory for creating stores to access cached account responses.
|
||||
* @property userTokensSaver Saver for updating user tokens in persistent storage.
|
||||
* @property walletAccountsSaver Saver for updating wallet accounts in persistent storage.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMainAccountTokensMigration(
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val walletAccountsSaver: WalletAccountsSaver,
|
||||
) : MainAccountTokensMigration {
|
||||
|
||||
override suspend fun migrate(
|
||||
userWalletId: UserWalletId,
|
||||
derivationIndex: DerivationIndex,
|
||||
): Either<Throwable, Unit> = either {
|
||||
if (derivationIndex == DerivationIndex.Main) {
|
||||
Timber.i("Migration skipped: derivation index is Main")
|
||||
return@either
|
||||
}
|
||||
|
||||
val store = accountsResponseStoreFactory.create(userWalletId)
|
||||
|
||||
val response = store.getSyncOrNull()
|
||||
|
||||
ensureNotNull(response) {
|
||||
val exception = IllegalStateException("No cached accounts response found")
|
||||
Timber.e(exception)
|
||||
exception
|
||||
}
|
||||
|
||||
val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main)
|
||||
val selectedAccount = findAccount(response = response, derivationIndex = derivationIndex)
|
||||
|
||||
val unassignedTokens = mainAccount.findUnassignedTokens(derivationIndex)
|
||||
|
||||
if (unassignedTokens == null) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
return@either
|
||||
}
|
||||
|
||||
val updatedResponse = response.copy(
|
||||
accounts = response.accounts.map { account ->
|
||||
when (account.id) {
|
||||
mainAccount.id -> {
|
||||
account.copy(tokens = account.tokens.orEmpty() - unassignedTokens)
|
||||
}
|
||||
selectedAccount.id -> {
|
||||
selectedAccount.assignTokens(userWalletId, unassignedTokens)
|
||||
}
|
||||
else -> account
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
walletAccountsSaver.store(userWalletId = userWalletId, response = updatedResponse)
|
||||
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = updatedResponse.toUserTokensResponse(),
|
||||
onFailSend = {
|
||||
// TODO: save failed state to retry later
|
||||
// [REDACTED_JIRA]
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Raise<Throwable>.findAccount(
|
||||
response: GetWalletAccountsResponse,
|
||||
derivationIndex: DerivationIndex,
|
||||
): WalletAccountDTO {
|
||||
val account = response.accounts.firstOrNull { it.derivationIndex == derivationIndex.value }
|
||||
|
||||
return ensureNotNull(account) {
|
||||
val exception = IllegalStateException("No account found with derivation index: $derivationIndex")
|
||||
Timber.e(exception)
|
||||
exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletAccountDTO.findUnassignedTokens(
|
||||
derivationIndex: DerivationIndex,
|
||||
): List<UserTokensResponse.Token>? {
|
||||
val tokens = this.tokens
|
||||
|
||||
if (tokens.isNullOrEmpty()) return tokens
|
||||
|
||||
return tokens
|
||||
.filterByDerivationIndex(derivationIndex)
|
||||
.map { it.copy(accountId = null) }
|
||||
.toNonEmptyListOrNull()
|
||||
}
|
||||
|
||||
private fun List<UserTokensResponse.Token>.filterByDerivationIndex(
|
||||
derivationIndex: DerivationIndex,
|
||||
): List<UserTokensResponse.Token> {
|
||||
return filter { savedToken ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId)
|
||||
if (blockchain == null) {
|
||||
Timber.e("Token has unknown networkId: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val derivationPathValue = savedToken.derivationPath
|
||||
if (derivationPathValue == null) {
|
||||
Timber.e("Token has no derivation path: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val accountNodeRecognizer = AccountNodeRecognizer(blockchain)
|
||||
val accountNodeValue = accountNodeRecognizer.recognize(derivationPathValue)
|
||||
|
||||
if (accountNodeValue == null) {
|
||||
Timber.e("Token has unrecognized derivation path: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
accountNodeValue == derivationIndex.value.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,4 +54,14 @@ internal fun List<WalletAccountDTO>.assignTokens(
|
|||
tokens = enrichedTokens[accountDTO.id].orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletAccountDTO.assignTokens(
|
||||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): WalletAccountDTO {
|
||||
val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
.filter { it.accountId == this.id }
|
||||
|
||||
return copy(tokens = enrichedTokens)
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
package com.tangem.data.account.token
|
||||
|
||||
import com.tangem.common.test.utils.assertEitherLeft
|
||||
import com.tangem.common.test.utils.assertEitherRight
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultMainAccountTokensMigrationTest {
|
||||
|
||||
private val accountsResponseStoreFactory = mockk<AccountsResponseStoreFactory>()
|
||||
private val accountsResponseStore = mockk<AccountsResponseStore>()
|
||||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
|
||||
private val userTokensSaver = mockk<UserTokensSaver>(relaxed = true)
|
||||
private val walletAccountsSaver = mockk<WalletAccountsSaver>(relaxed = true)
|
||||
private val migration = DefaultMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
userTokensSaver = userTokensSaver,
|
||||
walletAccountsSaver = walletAccountsSaver,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val derivationIndex = DerivationIndex(1).getOrNull()!!
|
||||
|
||||
@BeforeEach
|
||||
fun setupAll() {
|
||||
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
|
||||
every { accountsResponseStore.data } returns accountsResponseStoreFlow
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(accountsResponseStoreFactory, accountsResponseStore, userTokensSaver, walletAccountsSaver)
|
||||
accountsResponseStoreFlow.value = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate skips when derivation index is Main`() = runTest {
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, DerivationIndex.Main)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
accountsResponseStoreFactory.create(any())
|
||||
accountsResponseStore.data
|
||||
walletAccountsSaver.store(userWalletId = any(), response = any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate fails when no cached accounts response`() = runTest {
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("No cached accounts response found")
|
||||
assertEitherLeft(actual, expected)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
walletAccountsSaver.store(userWalletId = any(), response = any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate fails when selected account DTO not found`() = runTest {
|
||||
// Arrange
|
||||
val response = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = DerivationIndex.Main.value),
|
||||
),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("No account found with derivation index: $derivationIndex")
|
||||
assertEitherLeft(actual, expected)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
walletAccountsSaver.store(userWalletId = any(), response = any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate skips when no unassigned tokens`() = runTest {
|
||||
// Arrange
|
||||
val response = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = DerivationIndex.Main.value),
|
||||
),
|
||||
)
|
||||
|
||||
val selectedAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex).value,
|
||||
derivationIndex = derivationIndex.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response.copy(accounts = response.accounts + selectedAccount)
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
walletAccountsSaver.store(userWalletId = any(), response = any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate updates tokens for selected account`() = runTest {
|
||||
// Arrange
|
||||
val unassignedToken = createBitcoin(accountIndex = 1)
|
||||
|
||||
val mainAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main).value,
|
||||
derivationIndex = DerivationIndex.Main.value,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = 0),
|
||||
unassignedToken,
|
||||
),
|
||||
)
|
||||
|
||||
val selectedAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex).value,
|
||||
derivationIndex = derivationIndex.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(mainAccount, selectedAccount),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
val migratedResponse = response.copy(
|
||||
accounts = listOf(
|
||||
mainAccount.copy(tokens = mainAccount.tokens!! - unassignedToken),
|
||||
selectedAccount.copy(tokens = listOf(unassignedToken)),
|
||||
),
|
||||
)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
walletAccountsSaver.store(userWalletId = userWalletId, response = migratedResponse)
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = migratedResponse.toUserTokensResponse(),
|
||||
onFailSend = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBitcoin(accountIndex: Int): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = "ne",
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(accountIndex).getOrNull()!!,
|
||||
).value,
|
||||
networkId = "bitcoin",
|
||||
derivationPath = "m/44'/60'/$accountIndex'/0/0",
|
||||
name = "Phil Hinton",
|
||||
symbol = "graeci",
|
||||
decimals = 6487,
|
||||
contractAddress = "vim",
|
||||
addresses = listOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ android {
|
|||
dependencies {
|
||||
/* Project - Domain */
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.blockaid)
|
||||
implementation(projects.domain.blockaid.models)
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ dependencies {
|
|||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.core)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.data.blockaid
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.BlockAidScanOptions
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.Data
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionBulkScanRequest
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.TransactionData as SDKTransactionData
|
||||
|
||||
internal class BlockAidEvmScanTransactionConverter(
|
||||
private val blockchain: Blockchain,
|
||||
) : Converter<List<SDKTransactionData.Uncompiled>, EvmTransactionBulkScanRequest> {
|
||||
|
||||
override fun convert(value: List<SDKTransactionData.Uncompiled>): EvmTransactionBulkScanRequest {
|
||||
return EvmTransactionBulkScanRequest(
|
||||
chain = blockchain.getChainId().toString(),
|
||||
options = listOf(BlockAidScanOptions.GasEstimation.value),
|
||||
metadata = TransactionMetadata(domain = "https://tangem.com"),
|
||||
data = value.map {
|
||||
Data(
|
||||
from = it.sourceAddress,
|
||||
to = it.destinationAddress,
|
||||
data = (it.extras as? EthereumTransactionExtras)?.callData?.dataHex.orEmpty(),
|
||||
)
|
||||
},
|
||||
aggregated = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,19 @@ package com.tangem.data.blockaid
|
|||
import com.domain.blockaid.models.dapp.CheckDAppResult
|
||||
import com.domain.blockaid.models.dapp.DAppData
|
||||
import com.domain.blockaid.models.transaction.CheckTransactionResult
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.domain.blockaid.models.transaction.TransactionData
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.blockchain.common.TransactionData as SDKTransactionData
|
||||
|
||||
interface BlockAidRepository {
|
||||
|
||||
suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult
|
||||
|
||||
suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult
|
||||
|
||||
suspend fun getGasEstimation(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
): GasEstimationResult
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.blockaid
|
||||
|
||||
import arrow.core.Either
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultBlockAidGasEstimate @Inject constructor(
|
||||
private val repository: BlockAidRepository,
|
||||
) : BlockAidGasEstimate {
|
||||
override suspend fun getGasEstimation(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
transactionDataList: List<TransactionData.Uncompiled>,
|
||||
): Either<Throwable, GasEstimationResult> = Either.catch {
|
||||
repository.getGasEstimation(cryptoCurrency = cryptoCurrency, transactionDataList = transactionDataList)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,18 @@ package com.tangem.data.blockaid
|
|||
import com.domain.blockaid.models.dapp.CheckDAppResult
|
||||
import com.domain.blockaid.models.dapp.DAppData
|
||||
import com.domain.blockaid.models.transaction.CheckTransactionResult
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.domain.blockaid.models.transaction.TransactionData
|
||||
import com.domain.blockaid.models.transaction.TransactionParams
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.blockaid.converters.GasEstimationResponseConverter
|
||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.tangem.blockchain.common.TransactionData as SDKTransactionData
|
||||
|
||||
internal class DefaultBlockAidRepository(
|
||||
private val api: BlockAidApi,
|
||||
|
|
@ -30,6 +36,17 @@ internal class DefaultBlockAidRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getGasEstimation(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
): GasEstimationResult {
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
return when {
|
||||
blockchain.isEvm() -> scanEvmTransactionBulk(blockchain, transactionDataList)
|
||||
else -> error("Gas estimation with BlockAid not supported by ${blockchain.fullName}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult =
|
||||
withContext(dispatchers.io) {
|
||||
val response = api.scanJsonRpc(mapper.mapToEvmRequest(data))
|
||||
|
|
@ -41,4 +58,15 @@ internal class DefaultBlockAidRepository(
|
|||
val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data))
|
||||
mapper.mapToDomain(response)
|
||||
}
|
||||
|
||||
private suspend fun scanEvmTransactionBulk(
|
||||
blockchain: Blockchain,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
): GasEstimationResult = withContext(dispatchers.io) {
|
||||
val response = api.scanEvmTransactionBulk(
|
||||
BlockAidEvmScanTransactionConverter(blockchain).convert(transactionDataList),
|
||||
)
|
||||
|
||||
GasEstimationResponseConverter.convert(response)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.data.blockaid.converters
|
||||
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.tangem.blockchain.extensions.hexToBigInteger
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.GasEstimationResponse
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object GasEstimationResponseConverter : Converter<List<GasEstimationResponse>, GasEstimationResult> {
|
||||
override fun convert(value: List<GasEstimationResponse>): GasEstimationResult {
|
||||
return GasEstimationResult(
|
||||
estimatedGasList = value.map { it.gasEstimation.estimate.hexToBigInteger() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.blockaid.di
|
||||
|
||||
import com.tangem.data.blockaid.DefaultBlockAidGasEstimate
|
||||
import com.tangem.data.blockaid.DefaultBlockAidVerifier
|
||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||
import com.tangem.domain.blockaid.BlockAidVerifier
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -15,4 +17,8 @@ interface BlockAidDataModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindVerifier(verifier: DefaultBlockAidVerifier): BlockAidVerifier
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBlockAidGasEstimate(gasEstimate: DefaultBlockAidGasEstimate): BlockAidGasEstimate
|
||||
}
|
||||
|
|
@ -29,6 +29,5 @@ interface ETagsStore {
|
|||
enum class Key {
|
||||
WalletAccounts,
|
||||
UserTokens,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,6 @@ interface QuotesFetcher {
|
|||
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(),
|
||||
),
|
||||
LAST_UPDATED_AT(value = "lastUpdatedAt"),
|
||||
;
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.data.feedback.converters.BlockchainInfoConverter
|
|||
import com.tangem.data.feedback.converters.WalletMetaInfoConverter
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.models.*
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.data.feedback.DefaultFeedbackFeatureToggles
|
|||
import com.tangem.data.feedback.DefaultFeedbackRepository
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package com.tangem.data.notifications
|
|||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultNotificationsRepository @Inject constructor(
|
||||
|
|
@ -17,6 +19,10 @@ class DefaultNotificationsRepository @Inject constructor(
|
|||
return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override fun getShouldShowNotification(key: String): Flow<Boolean> {
|
||||
return appPreferencesStore.get(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override suspend fun setShouldShowNotifications(key: String, value: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.swap
|
||||
|
||||
import arrow.core.right
|
||||
import arrow.core.none
|
||||
import arrow.core.some
|
||||
import arrow.core.toOption
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.swap.converter.SwapDataConverter
|
||||
|
|
@ -28,7 +30,7 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.*
|
||||
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
|
||||
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -49,7 +51,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||
private val currencyStatusProxyCreator: CurrencyStatusProxyCreator,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
) : SwapRepositoryV2 {
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
|
||||
val mappedProviders = pair.providers.mapNotNull {
|
||||
mappedProviders[it.providerId]
|
||||
}
|
||||
}.filterYieldSupplyProvider(statusFrom)
|
||||
|
||||
if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) {
|
||||
SwapPairModel(
|
||||
|
|
@ -152,7 +153,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
|
||||
val mappedProvider = pair.providers.mapNotNull {
|
||||
mappedProviders[it.providerId]
|
||||
}
|
||||
}.filterYieldSupplyProvider(currencyStatusFrom)
|
||||
|
||||
if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) {
|
||||
SwapPairModel(
|
||||
|
|
@ -391,17 +392,19 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
return currencyStatusProxyCreator.createCurrencyStatus(
|
||||
val quoteStatus = quote ?: singleQuoteStatusSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
|
||||
)
|
||||
|
||||
return CryptoCurrencyStatusFactory.create(
|
||||
currency = cryptoCurrency,
|
||||
maybeQuoteStatus = quote?.right() ?: singleQuoteStatusSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
|
||||
).right(),
|
||||
maybeNetworkStatus = NetworkStatus(
|
||||
network = cryptoCurrency.network,
|
||||
value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status
|
||||
).right(),
|
||||
maybeYieldBalance = null,
|
||||
).getOrNull()
|
||||
).some(),
|
||||
maybeQuoteStatus = quoteStatus.toOption(),
|
||||
maybeYieldBalance = none(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseTxDetails(txDetailsJson: String): TxDetails? {
|
||||
|
|
@ -419,4 +422,15 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
is CryptoCurrency.Coin -> "0"
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<ExpressProvider>.filterYieldSupplyProvider(cryptoCurrencyStatus: CryptoCurrencyStatus?) =
|
||||
filter { provider ->
|
||||
// !!!WARNING!!! Filter out dex provider if yield supply is active
|
||||
val yieldSupplyStatus = cryptoCurrencyStatus?.value?.yieldSupplyStatus
|
||||
if (yieldSupplyStatus != null && yieldSupplyStatus.isActive) {
|
||||
provider.type == ExpressProviderType.CEX
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
|||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -59,7 +58,6 @@ internal object SwapDataModule {
|
|||
moshi = moshi,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
currencyStatusProxyCreator = CurrencyStatusProxyCreator(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,17 +24,18 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Project - Domain
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets.models)
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
/**
|
||||
* Implementation of [MultiWalletCryptoCurrenciesFetcher] that fetches crypto currencies of all accounts
|
||||
*
|
||||
* @property userWalletsStore [UserWallet]'s store
|
||||
* @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a multi wallet
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AccountListCryptoCurrenciesFetcher(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiWalletCryptoCurrenciesFetcher {
|
||||
|
||||
override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
|
||||
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
|
||||
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
package com.tangem.data.tokens.di
|
||||
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher
|
||||
import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.data.tokens.utils.CustomTokensMerger
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -24,28 +27,38 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
|
|||
@Singleton
|
||||
@Provides
|
||||
fun provideMultiWalletCryptoCurrenciesFetcher(
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userTokensResponseStore: UserTokensResponseStore,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
expressServiceLoader: ExpressServiceLoader,
|
||||
walletAccountsFetcher: WalletAccountsFetcher,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): MultiWalletCryptoCurrenciesFetcher {
|
||||
return DefaultMultiWalletCryptoCurrenciesFetcher(
|
||||
demoConfig = DemoConfig(),
|
||||
userWalletsStore = userWalletsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
customTokensMerger = CustomTokensMerger(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensSaver = userTokensSaver,
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
AccountListCryptoCurrenciesFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
dispatchers = dispatchers,
|
||||
),
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
DefaultMultiWalletCryptoCurrenciesFetcher(
|
||||
demoConfig = DemoConfig(),
|
||||
userWalletsStore = userWalletsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
customTokensMerger = CustomTokensMerger(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensSaver = userTokensSaver,
|
||||
dispatchers = dispatchers,
|
||||
),
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
|||
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository
|
||||
import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository
|
||||
import com.tangem.data.tokens.repository.DefaultYieldSupplyWarningsViewedRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -19,6 +20,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
|
|||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -96,4 +98,16 @@ internal object TokensDataModule {
|
|||
tokenReceiveWarningActionStore = tokenReceiveWarningActionStore,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultYieldSupplyWarningsViewedRepository(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): YieldSupplyWarningsViewedRepository {
|
||||
return DefaultYieldSupplyWarningsViewedRepository(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSet
|
||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultYieldSupplyWarningsViewedRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : YieldSupplyWarningsViewedRepository {
|
||||
|
||||
override suspend fun getViewedWarnings(): Set<String> = withContext(dispatchers.io) {
|
||||
appPreferencesStore.getObjectSet<String>(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull()
|
||||
?: emptySet()
|
||||
}
|
||||
|
||||
override suspend fun view(symbol: String) = withContext(dispatchers.io) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val stored = mutablePreferences.getObjectSet<String>(
|
||||
PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY,
|
||||
) ?: mutableSetOf()
|
||||
|
||||
val updated = stored + symbol
|
||||
|
||||
mutablePreferences.setObjectSet<String>(
|
||||
key = PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY,
|
||||
value = updated,
|
||||
)
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import arrow.core.left
|
||||
import com.tangem.common.test.utils.assertEither
|
||||
import com.tangem.common.test.utils.assertEitherRight
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class AccountListCryptoCurrenciesFetcherTest {
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true)
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true)
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val fetcher = AccountListCryptoCurrenciesFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(userWalletsStore, walletAccountsFetcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns failure if wallet is not multi-currency`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
val mockUserWallet = mockk<UserWallet> { every { isMultiCurrency } returns false }
|
||||
every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException(
|
||||
"${AccountListCryptoCurrenciesFetcher::class.simpleName} supports only multi-currency wallet",
|
||||
).left()
|
||||
assertEither(actual, expected)
|
||||
|
||||
verify { userWalletsStore.getSyncStrict(key = params.userWalletId) }
|
||||
coVerify(inverse = true) { walletAccountsFetcher.fetch(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns accounts if wallet is multi-currency`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
val mockUserWallet = mockk<UserWallet> { every { isMultiCurrency } returns true }
|
||||
|
||||
every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns error if walletAccountsFetcher returns error`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
val mockUserWallet = mockk<UserWallet> { every { isMultiCurrency } returns true }
|
||||
val error = RuntimeException("fetch error")
|
||||
|
||||
every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet
|
||||
coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } throws error
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = error.left()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("012")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.eip1559.isSupportEIP1559
|
||||
import com.tangem.blockchain.blockchains.ethereum.network.EthereumFeeHistory
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
|
|
@ -12,6 +18,8 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
internal class DefaultFeeRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
|
|
@ -22,6 +30,53 @@ internal class DefaultFeeRepository(
|
|||
return networkId.toBlockchain().isFeeApproximate(amountType)
|
||||
}
|
||||
|
||||
override suspend fun getEthereumFeeWithoutGas(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Fee.Ethereum {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
|
||||
val ethereumWalletManager = walletManager as? EthereumWalletManager
|
||||
?: error("Not supported for ${cryptoCurrency.network}")
|
||||
|
||||
val fee = if (blockchain.isSupportEIP1559) {
|
||||
val gasHistory = when (val gasHistory = ethereumWalletManager.getGasHistory()) {
|
||||
is Result.Failure -> throw gasHistory.error
|
||||
is Result.Success -> gasHistory.data
|
||||
}
|
||||
|
||||
val marketPriorityFee = when (gasHistory) {
|
||||
is EthereumFeeHistory.Common -> gasHistory.marketPriorityFee
|
||||
is EthereumFeeHistory.Fallback -> gasHistory.gasPrice.toBigDecimal() * MULTIPLIER_GAS_PRICE_NORMAL_FEE
|
||||
}
|
||||
|
||||
val maxFeePerGas = gasHistory.baseFee * MULTIPLIER_GAS_PRICE_NORMAL_FEE + marketPriorityFee
|
||||
|
||||
getEthEip1559Fee(
|
||||
maxFeePerGas = maxFeePerGas.toBigInteger(),
|
||||
priorityFee = marketPriorityFee.toBigInteger(),
|
||||
blockchain = blockchain,
|
||||
)
|
||||
} else {
|
||||
val gasPrice = when (val gasPrice = ethereumWalletManager.getGasPrice()) {
|
||||
is Result.Failure -> throw gasPrice.error
|
||||
is Result.Success -> gasPrice.data
|
||||
}
|
||||
|
||||
getEthLegacyFee(
|
||||
gasPrice = gasPrice,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
}
|
||||
|
||||
return fee
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -54,4 +109,37 @@ internal class DefaultFeeRepository(
|
|||
?: error("WalletManager is null"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEthLegacyFee(gasPrice: BigInteger, blockchain: Blockchain): Fee.Ethereum.Legacy {
|
||||
val amount = Amount(
|
||||
value = BigDecimal.ZERO,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
return Fee.Ethereum.Legacy(
|
||||
amount = amount,
|
||||
gasLimit = BigInteger.ZERO,
|
||||
gasPrice = gasPrice,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEthEip1559Fee(
|
||||
maxFeePerGas: BigInteger,
|
||||
priorityFee: BigInteger,
|
||||
blockchain: Blockchain,
|
||||
): Fee.Ethereum.EIP1559 {
|
||||
val amount = Amount(
|
||||
value = BigDecimal.ZERO,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
return Fee.Ethereum.EIP1559(
|
||||
amount = amount,
|
||||
gasLimit = BigInteger.ZERO,
|
||||
maxFeePerGas = maxFeePerGas,
|
||||
priorityFee = priorityFee,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val MULTIPLIER_GAS_PRICE_NORMAL_FEE = "1.2".toBigDecimal() // 120%
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,17 @@ internal class DefaultTransactionRepository(
|
|||
|
||||
val extras = txExtras ?: getMemoExtras(networkId = network.rawId, memo)
|
||||
|
||||
val destination = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
walletManager.getYieldModuleAddress()
|
||||
} else {
|
||||
destination
|
||||
}
|
||||
val amount = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
amount.copy(value = BigDecimal.ZERO)
|
||||
} else {
|
||||
amount
|
||||
}
|
||||
|
||||
return@withContext if (fee != null) {
|
||||
walletManager.createTransaction(
|
||||
amount = amount,
|
||||
|
|
@ -86,11 +97,6 @@ internal class DefaultTransactionRepository(
|
|||
nonce: BigInteger?,
|
||||
): TransactionData.Uncompiled = withContext(dispatchers.io) {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
|
||||
val callData = SmartContractCallDataProviderFactory.getTokenTransferCallData(
|
||||
destinationAddress = destination,
|
||||
|
|
@ -98,7 +104,7 @@ internal class DefaultTransactionRepository(
|
|||
blockchain = blockchain,
|
||||
)
|
||||
|
||||
val extras = if (amount.type is AmountType.Token && callData != null) {
|
||||
val extras = if (callData != null) {
|
||||
createTransactionDataExtras(
|
||||
callData = callData,
|
||||
network = network,
|
||||
|
|
@ -109,25 +115,15 @@ internal class DefaultTransactionRepository(
|
|||
null
|
||||
}
|
||||
|
||||
return@withContext if (fee != null) {
|
||||
createTransaction(
|
||||
amount = amount,
|
||||
fee = fee,
|
||||
memo = null,
|
||||
destination = destination,
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
txExtras = getMemoExtras(networkId = network.rawId, memo = memo) ?: extras,
|
||||
)
|
||||
} else {
|
||||
TransactionData.Uncompiled(
|
||||
amount = amount,
|
||||
sourceAddress = walletManager.wallet.address,
|
||||
destinationAddress = destination,
|
||||
extras = getMemoExtras(networkId = network.rawId, memo = memo) ?: extras,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
createTransaction(
|
||||
amount = amount,
|
||||
fee = fee,
|
||||
memo = null,
|
||||
destination = destination,
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
txExtras = getMemoExtras(networkId = network.rawId, memo = memo) ?: extras,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createApprovalTransaction(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ dependencies {
|
|||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.networks)
|
||||
|
||||
/** Feature API - remove after removing [HotWalletFeatureToggles] */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
|
||||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -21,4 +26,24 @@ internal interface TangemPayDataModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayMainScreenCustomerInfoUseCase(
|
||||
repository: OnboardingRepository,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase {
|
||||
return TangemPayIssueOrderUseCase(repository = repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,26 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: KycRepository"
|
||||
|
||||
internal class DefaultKycRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
) : KycRepository {
|
||||
|
||||
override suspend fun getKycStartInfo() = withContext(dispatchers.io) {
|
||||
requestHelper.request { authHeader ->
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result
|
||||
}.map {
|
||||
KycStartInfo(token = it.token, locale = it.locale)
|
||||
override suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader)
|
||||
}.result
|
||||
|
||||
KycStartInfo(token = result.token, locale = result.locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,108 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
||||
import com.tangem.datasource.api.pay.models.request.OrderRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.ProductInstance
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val VALID_STATUS = "valid"
|
||||
private const val TAG = "TangemPay: OnboardingRepository"
|
||||
|
||||
internal class DefaultOnboardingRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
) : OnboardingRepository {
|
||||
|
||||
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> = either {
|
||||
return requestHelper.request {
|
||||
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)).getOrThrow().result
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}.map { result -> result.status == VALID_STATUS }
|
||||
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request {
|
||||
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
|
||||
}.result
|
||||
result?.status == VALID_STATUS
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> = either {
|
||||
return requestHelper.request { authHeader ->
|
||||
val response = tangemPayApi.getCustomerMe(authHeader).getOrThrow()
|
||||
response.result ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}.map { result ->
|
||||
CustomerInfo(
|
||||
productInstance = result.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
|
||||
kycStatus = result.kyc?.status,
|
||||
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.result
|
||||
getCustomerInfo(result)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMainScreenCustomerInfo(): Either<UniversalError, MainScreenCustomerInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.requestWithPersistedToken { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.result
|
||||
|
||||
val orderStatus = getOrderStatus().getOrNull() ?: error("Order status is null")
|
||||
|
||||
MainScreenCustomerInfo(info = getCustomerInfo(result), orderStatus = orderStatus)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createOrder(): Either<UniversalError, Unit> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress()
|
||||
val result = requestHelper.requestWithPersistedToken { authHeader ->
|
||||
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
|
||||
}.result ?: error("Create order result is null")
|
||||
|
||||
tangemPayStorage.storeOrderId(result.data.customerWalletAddress, result.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo {
|
||||
val card = response?.card
|
||||
val balance = response?.balance
|
||||
val productInstance = response?.productInstance
|
||||
val cardInfo = if (productInstance != null && card != null && balance != null) {
|
||||
CardInfo(
|
||||
lastFourDigits = card.cardNumberEnd,
|
||||
balance = balance.availableBalance,
|
||||
currencyCode = balance.currency,
|
||||
customerWalletAddress = productInstance.cardWalletAddress,
|
||||
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return CustomerInfo(
|
||||
productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
|
||||
kycStatus = response?.kyc?.status,
|
||||
cardInfo = cardInfo,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getOrderStatus(): Either<UniversalError, OrderStatus> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress()
|
||||
val orderId: String = tangemPayStorage.getOrderId(walletAddress)
|
||||
?: return@runWithErrorLogs OrderStatus.NOT_ISSUED
|
||||
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getOrder(authHeader, orderId)
|
||||
}.result ?: error("Order result is null")
|
||||
|
||||
when (result.status) {
|
||||
OrderStatus.NEW.apiName -> OrderStatus.NEW
|
||||
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
|
||||
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
|
||||
else -> OrderStatus.CANCELED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import com.tangem.pagination.fetcher.CursorBatchFetcher
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val INITIAL_CURSOR = "initial_cursor_key"
|
||||
private const val TAG = "TangemPay: TangemPayTxHistoryRepository:"
|
||||
|
||||
internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
||||
private val requestPerformer: TangemPayRequestPerformer,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemPayTxHistoryRepository {
|
||||
|
||||
override fun getTxHistoryBatchFlow(
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
): TangemPayTxHistoryListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
private fun createFetcher(
|
||||
batchSize: Int,
|
||||
): BatchFetcher<TangemPayTxHistoryListConfig, List<TangemPayTxHistoryItem>> {
|
||||
return CursorBatchFetcher(
|
||||
prefetchDistance = batchSize,
|
||||
batchSize = batchSize,
|
||||
subFetcher = { request, _, _ ->
|
||||
val items = loadItems(config = request.params, cursor = request.cursor, limit = request.limit)
|
||||
BatchFetchResult.Success(
|
||||
data = items,
|
||||
last = items.size < request.limit,
|
||||
empty = items.isEmpty(),
|
||||
)
|
||||
},
|
||||
cursorFromItem = { item -> item.id }, // last item’s id becomes next cursor
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadItems(
|
||||
config: TangemPayTxHistoryListConfig,
|
||||
cursor: String?,
|
||||
limit: Int,
|
||||
): List<TangemPayTxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor),
|
||||
skipCache = config.refresh,
|
||||
block = { fetch(customerWalletAddress = config.customerWalletAddress, cursor = cursor, pageSize = limit) },
|
||||
)
|
||||
|
||||
return txHistoryItemsStore.getSyncOrNull(
|
||||
key = config.customerWalletAddress,
|
||||
cursor = cursor ?: INITIAL_CURSOR,
|
||||
).orEmpty()
|
||||
}
|
||||
|
||||
private fun getCacheKey(customerWalletAddress: String, cursor: String?): String {
|
||||
return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}"
|
||||
}
|
||||
|
||||
private suspend fun fetch(customerWalletAddress: String, cursor: String?, pageSize: Int) {
|
||||
requestPerformer.runWithErrorLogs(TAG) {
|
||||
val result = requestPerformer.request { authHeader ->
|
||||
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
|
||||
}.result
|
||||
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions)
|
||||
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +1,110 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.networks.repository.NetworksRepository
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* For TangemPay Customer Wallet auth we are using polygon address
|
||||
*/
|
||||
private const val POL_VALUE = "coin⟨POLYGON⟩polygon-ecosystem-token"
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class TangemPayRequestPerformer @Inject constructor(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val userWalletsRepository: UserWalletsListRepository,
|
||||
private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val networksRepository: NetworksRepository,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) {
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
private var customerWalletAddress: String? = null
|
||||
|
||||
private val refreshTokensMutex = Mutex()
|
||||
private var refreshTokensJob: Deferred<Either<UniversalError, VisaAuthTokens>>? = null
|
||||
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
|
||||
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> T): Either<UniversalError, T> = either {
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind()
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
suspend fun <T : Any> runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either<UniversalError, T> {
|
||||
return try {
|
||||
val result = requestBlock()
|
||||
Either.Right(result)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e("$tag: $exception")
|
||||
Either.Left(mapError(exception))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokens,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") },
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getWallets(): Flow<List<UserWallet>> = if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.map { requireNotNull(it) }
|
||||
} else {
|
||||
userWalletsListManager.userWallets
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> performRequest(
|
||||
requestBlock: suspend (header: String) -> T,
|
||||
refreshTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>)? = null,
|
||||
): Either<UniversalError, T> = either {
|
||||
runCatching {
|
||||
requestBlock("Bearer ${getAccessTokens().bind().accessToken}")
|
||||
}.getOrElse { error ->
|
||||
when (error) {
|
||||
is ApiResponseError.HttpException -> {
|
||||
if (refreshTokens != null && error.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) {
|
||||
refreshOrJoin(refreshTokens).bind()
|
||||
performRequest(requestBlock, refreshTokens = null).bind()
|
||||
} else {
|
||||
raise(mapHttpError(error))
|
||||
}
|
||||
}
|
||||
else -> raise(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
getTokens: (suspend () -> VisaAuthTokens),
|
||||
refreshTokens: (suspend () -> VisaAuthTokens)? = null,
|
||||
): T = runCatching {
|
||||
requestBlock("Bearer ${getTokens().accessToken}").getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) {
|
||||
refreshOrJoin(refreshTokens)
|
||||
performRequest(requestBlock, refreshTokens = null, getTokens = getTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshOrJoin(
|
||||
refreshTokens: suspend () -> Either<UniversalError, VisaAuthTokens>,
|
||||
): Either<UniversalError, VisaAuthTokens> {
|
||||
val jobToAwait: Deferred<Either<UniversalError, VisaAuthTokens>> =
|
||||
private suspend fun refreshOrJoin(refreshTokens: suspend () -> VisaAuthTokens): VisaAuthTokens {
|
||||
val jobToAwait: Deferred<VisaAuthTokens> =
|
||||
refreshTokensMutex.withLock {
|
||||
val current = refreshTokensJob
|
||||
if (current == null || current.isCompleted) {
|
||||
|
|
@ -87,8 +117,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
val result = try {
|
||||
jobToAwait.await()
|
||||
} catch (ignore: Throwable) {
|
||||
Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
} finally {
|
||||
refreshTokensMutex.withLock {
|
||||
if (refreshTokensJob === jobToAwait && jobToAwait.isCompleted) {
|
||||
|
|
@ -99,57 +127,67 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
return result
|
||||
}
|
||||
|
||||
private suspend fun getCustomerWalletAddress(): Either<UniversalError, String> = either {
|
||||
customerWalletAddress
|
||||
?: tangemPayStorage.getCustomerWalletAddress()
|
||||
?: fetchAuthInputData().bind().address
|
||||
suspend fun getCustomerWalletAddress(): String = customerWalletAddress ?: fetchAuthInputData().address
|
||||
|
||||
private suspend fun getAccessTokens(): VisaAuthTokens {
|
||||
return getAccessTokensIfSaved() ?: fetchTokens()
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
val address = getCustomerWalletAddress().bind()
|
||||
tangemPayStorage.getAuthTokens(address) ?: fetchTokens().bind()
|
||||
private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? {
|
||||
return tangemPayStorage.getAuthTokens(getCustomerWalletAddress())
|
||||
}
|
||||
|
||||
private fun mapHttpError(throwable: ApiResponseError.HttpException): UniversalError {
|
||||
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: throwable.code.numericCode
|
||||
}.map {
|
||||
VisaApiError.fromBackendError(it)
|
||||
}.getOrElse {
|
||||
VisaApiError.UnknownWithoutCode
|
||||
}
|
||||
}
|
||||
private suspend fun fetchAuthInputData(): AuthInputData {
|
||||
val userWallets = getWallets()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first()
|
||||
val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
?: error("Cannot find cold user wallet")
|
||||
|
||||
private suspend fun fetchAuthInputData(): Either<UniversalError, AuthInputData> = either {
|
||||
val wallet = userWalletsRepository.userWalletsSync().find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE))
|
||||
.getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
val blockchain = Blockchain.Polygon
|
||||
val derivationPath = getDerivationPath(blockchain, wallet)
|
||||
val address = networksRepository.getNetworkAddresses(wallet.walletId, Network.RawID(blockchain.id))
|
||||
.find { it.cryptoCurrency.network.derivationPath.value == derivationPath }?.address
|
||||
?: error("Cannot get polygon address")
|
||||
|
||||
customerWalletAddress = address
|
||||
tangemPayStorage.storeCustomerWalletAddress(address)
|
||||
|
||||
AuthInputData(address, wallet.cardId)
|
||||
return AuthInputData(address, wallet.cardId)
|
||||
}
|
||||
|
||||
private suspend fun fetchTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
val inputData = fetchAuthInputData().bind()
|
||||
private fun getDerivationPath(blockchain: Blockchain, wallet: UserWallet) =
|
||||
blockchain.derivationPath(wallet.derivationStyleProvider.getDerivationStyle())?.rawPath
|
||||
?: error("Cannot get derivation path")
|
||||
|
||||
private suspend fun fetchTokens(): VisaAuthTokens {
|
||||
val inputData = fetchAuthInputData()
|
||||
val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId)
|
||||
.getOrNull()
|
||||
?: return Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
.getOrNull() ?: error("Cannot fetch tokens")
|
||||
tangemPayStorage.storeAuthTokens(inputData.address, tokens)
|
||||
tokens
|
||||
return tokens
|
||||
}
|
||||
|
||||
private suspend fun refreshAuthTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
val customerWalletAddress = getCustomerWalletAddress().bind()
|
||||
val refreshToken = getAccessTokens().bind().refreshToken.value
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull()
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
private suspend fun refreshAuthTokens(): VisaAuthTokens {
|
||||
val customerWalletAddress = getCustomerWalletAddress()
|
||||
val refreshToken = getAccessTokens().refreshToken.value
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens")
|
||||
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
|
||||
tokens
|
||||
return tokens
|
||||
}
|
||||
|
||||
private fun mapError(throwable: Throwable): UniversalError {
|
||||
return if (throwable is ApiResponseError.HttpException) {
|
||||
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: throwable.code.numericCode
|
||||
}.map {
|
||||
VisaApiError.fromBackendError(it)
|
||||
}.getOrElse {
|
||||
VisaApiError.UnknownWithoutCode
|
||||
}
|
||||
} else {
|
||||
VisaApiError.UnknownWithoutCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object TangemPayTxHistoryItemConverter :
|
||||
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem> {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem {
|
||||
val spend = value.spend
|
||||
val collateral = value.collateral
|
||||
val payment = value.payment
|
||||
val fee = value.fee
|
||||
|
||||
return TangemPayTxHistoryItem(
|
||||
id = value.id,
|
||||
date = when {
|
||||
spend != null -> spend.postedAt
|
||||
collateral != null -> collateral.postedAt
|
||||
payment != null -> payment.postedAt
|
||||
fee != null -> fee.postedAt
|
||||
else -> null
|
||||
},
|
||||
amount = when {
|
||||
spend != null -> spend.amount
|
||||
collateral != null -> collateral.amount
|
||||
payment != null -> payment.amount
|
||||
fee != null -> fee.amount
|
||||
else -> null
|
||||
},
|
||||
merchantName = when {
|
||||
spend != null -> spend.merchantName
|
||||
else -> null
|
||||
},
|
||||
status = when {
|
||||
spend != null -> spend.status
|
||||
payment != null -> payment.status
|
||||
else -> null
|
||||
},
|
||||
currency = when {
|
||||
spend != null -> spend.currency
|
||||
collateral != null -> collateral.currency
|
||||
payment != null -> payment.currency
|
||||
fee != null -> fee.currency
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object VisaTxHistoryItemConverter : Converter<VisaTxHistoryResponse.Transaction, VisaTxHistoryItem> {
|
||||
|
||||
override fun convert(value: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
|
||||
return VisaTxHistoryItem(
|
||||
id = value.transactionId.toString(),
|
||||
date = value.transactionDt,
|
||||
amount = value.blockchainAmount,
|
||||
fiatAmount = value.transactionAmount,
|
||||
merchantName = value.merchantName,
|
||||
status = value.transactionStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(value.transactionCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
|
||||
internal class VisaTxHistoryItemFactory {
|
||||
|
||||
fun create(transaction: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
|
||||
return VisaTxHistoryItem(
|
||||
id = transaction.transactionId.toString(),
|
||||
date = transaction.transactionDt,
|
||||
amount = transaction.blockchainAmount,
|
||||
fiatAmount = transaction.transactionAmount,
|
||||
merchantName = transaction.merchantName,
|
||||
status = transaction.transactionStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,6 @@ internal class VisaTxHistoryPagingSource(
|
|||
val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse,
|
||||
) : PagingSource<Int, VisaTxHistoryItem>() {
|
||||
|
||||
private val itemsFactory = VisaTxHistoryItemFactory()
|
||||
|
||||
private val cardPublicKey = params.cardPublicKey
|
||||
private val pageSize = params.pageSize
|
||||
private val isRefresh = params.isRefresh
|
||||
|
|
@ -82,7 +80,7 @@ internal class VisaTxHistoryPagingSource(
|
|||
|
||||
pagedItems.update {
|
||||
it.toMutableMap().apply {
|
||||
this[offset] = response.transactions.map(itemsFactory::create)
|
||||
this[offset] = response.transactions.map(VisaTxHistoryItemConverter::convert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import com.tangem.blockchain.extensions.SimpleResult
|
|||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection
|
||||
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest
|
||||
import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.walletmanager.utils.*
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
|
|
@ -50,6 +52,7 @@ import kotlinx.coroutines.withContext
|
|||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.util.EnumSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
|
|
@ -71,7 +74,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
private val requirementsConditionConverter by lazy { SdkRequirementsConditionConverter() }
|
||||
private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory() }
|
||||
|
||||
private val initMutex = Mutex()
|
||||
private val wmInitializationMutexes = ConcurrentHashMap<String, Mutex>()
|
||||
|
||||
override suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -363,7 +366,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
blockchain: Blockchain,
|
||||
derivationPath: String?,
|
||||
): WalletManager? {
|
||||
initMutex.withLock {
|
||||
getWmInitializationMutex(blockchain, derivationPath).withLock {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
|
||||
var walletManager = walletManagersStore.getSyncOrNull(
|
||||
|
|
@ -496,18 +499,26 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
) ?: error("Wallet manager not found")
|
||||
|
||||
val destination = estimationFeeAddressFactory.makeAddress(blockchain)
|
||||
val destination = when (amount.type) {
|
||||
is AmountType.Token -> estimationFeeAddressFactory.makeAddress(blockchain)
|
||||
is AmountType.TokenYieldSupply -> walletManager.getYieldModuleAddress()
|
||||
else -> return@withContext null
|
||||
}
|
||||
|
||||
val callData = if (amount.type is AmountType.Token) {
|
||||
SmartContractCallDataProviderFactory.getTokenTransferCallData(
|
||||
val callData = when (val amountType = amount.type) {
|
||||
is AmountType.Token -> SmartContractCallDataProviderFactory.getTokenTransferCallData(
|
||||
destinationAddress = destination,
|
||||
amount = amount,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
is AmountType.TokenYieldSupply -> YieldSupplyContractCallDataProviderFactory.getSendCallData(
|
||||
tokenContractAddress = amountType.token.contractAddress,
|
||||
destinationAddress = estimationFeeAddressFactory.makeAddress(blockchain),
|
||||
amount = amount,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
(walletManager as? TransactionSender)?.estimateFee(
|
||||
|
|
@ -728,6 +739,17 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED
|
||||
}
|
||||
|
||||
private fun getWmInitializationMutex(blockchain: Blockchain, derivationPath: String?): Mutex {
|
||||
val key = createMutexMapKey(blockchain, derivationPath)
|
||||
return wmInitializationMutexes.computeIfAbsent(key) {
|
||||
Mutex()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMutexMapKey(blockchain: Blockchain, derivationPath: String?): String {
|
||||
return blockchain.toNetworkId() + "|" + derivationPath
|
||||
}
|
||||
|
||||
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
|
||||
if (tokens.isEmpty()) return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.yieldsupply.providers.ethereum.factory.EthereumYieldSupplyDeployCallData
|
||||
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData
|
||||
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyExitCallData
|
||||
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Address
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -40,7 +44,18 @@ internal class TransactionDataToTxHistoryItemConverter(
|
|||
TransactionStatus.Confirmed -> TxInfo.TransactionStatus.Confirmed
|
||||
TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed
|
||||
},
|
||||
type = TxInfo.TransactionType.Transfer,
|
||||
type = when (val extras = value.extras) {
|
||||
is EthereumTransactionExtras -> {
|
||||
when (extras.callData) {
|
||||
is EthereumYieldSupplyDeployCallData,
|
||||
is EthereumYieldSupplyEnterCallData,
|
||||
-> TxInfo.TransactionType.YieldSupply.Enter
|
||||
is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit
|
||||
else -> TxInfo.TransactionType.Transfer
|
||||
}
|
||||
}
|
||||
else -> TxInfo.TransactionType.Transfer
|
||||
},
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,9 @@ internal class UpdateWalletManagerResultFactoryTest {
|
|||
),
|
||||
),
|
||||
currenciesAmounts = setOf(
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(value = BigDecimal.ZERO), // default for demo
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(
|
||||
value = BigDecimal.ZERO,
|
||||
), // default for demo
|
||||
),
|
||||
currentTransactions = emptySet(),
|
||||
),
|
||||
|
|
@ -272,7 +274,9 @@ internal class UpdateWalletManagerResultFactoryTest {
|
|||
),
|
||||
),
|
||||
currenciesAmounts = setOf(
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(value = BigDecimal.ONE), // used demo amount
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(
|
||||
value = BigDecimal.ONE,
|
||||
), // used demo amount
|
||||
),
|
||||
currentTransactions = emptySet(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -415,6 +415,6 @@ internal class DefaultWalletsRepository(
|
|||
else -> ActivatePromoCodeError.ActivationFailed
|
||||
}
|
||||
return@fold error.left()
|
||||
},)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||
|
|
@ -16,7 +16,6 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.set
|
||||
|
||||
class DefaultHotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
|
|
|
|||
|
|
@ -114,62 +114,64 @@ class DefaultWalletsRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() = runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val wallet2Id = "fedcba0987654321"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
WalletResponse(
|
||||
id = wallet2Id,
|
||||
notifyStatus = false,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val wallet2Id = "fedcba0987654321"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
WalletResponse(
|
||||
id = wallet2Id,
|
||||
notifyStatus = false,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
|
||||
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = true)
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = true)
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id)
|
||||
assertThat(result[1].isNotificationsEnabled).isFalse()
|
||||
// THEN
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id)
|
||||
assertThat(result[1].isNotificationsEnabled).isFalse()
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 2) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 2) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() = runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val applicationId = "test_app_id"
|
||||
val wallet1Id = "1234567890abcdef"
|
||||
val walletResponses = listOf(
|
||||
WalletResponse(
|
||||
id = wallet1Id,
|
||||
notifyStatus = true,
|
||||
),
|
||||
)
|
||||
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = false)
|
||||
// WHEN
|
||||
val result = repository.getWalletsInfo(applicationId, updateCache = false)
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
// THEN
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
|
||||
assertThat(result[0].isNotificationsEnabled).isTrue()
|
||||
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 0) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
|
||||
coVerify(exactly = 0) { preferencesDataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest {
|
||||
|
|
@ -271,8 +273,8 @@ class DefaultWalletsRepositoryTest {
|
|||
// GIVEN
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
|
@ -288,8 +290,8 @@ class DefaultWalletsRepositoryTest {
|
|||
// GIVEN
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ dependencies {
|
|||
|
||||
/** Domain */
|
||||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.legacy)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.yield.supply
|
||||
|
||||
import com.tangem.domain.yield.supply.YieldSupplyError
|
||||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||
|
||||
internal data object DefaultYieldSupplyErrorResolver : YieldSupplyErrorResolver {
|
||||
override fun resolve(throwable: Throwable): YieldSupplyError {
|
||||
return when (throwable) {
|
||||
is YieldSupplyError -> throwable
|
||||
else -> YieldSupplyError.DataError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.data.yield.supply
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||
import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter
|
||||
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultYieldSupplyMarketRepository(
|
||||
private val yieldSupplyApi: YieldSupplyApi,
|
||||
private val store: YieldMarketsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : YieldSupplyMarketRepository {
|
||||
|
||||
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
|
||||
store.getSyncOrNull()
|
||||
}
|
||||
|
||||
override suspend fun updateMarkets(): List<YieldMarketToken> = withContext(dispatchers.io) {
|
||||
val response = yieldSupplyApi.getYieldMarkets().getOrThrow()
|
||||
val domain = response.marketDtos.map(YieldMarketTokenConverter::convert)
|
||||
store.store(domain)
|
||||
domain
|
||||
}
|
||||
|
||||
override fun getMarketsFlow(): Flow<List<YieldMarketToken>> = store.get()
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.tangem.domain.utils.convertToSdkAmount
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -30,6 +31,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
override suspend fun createEnterTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
maxNetworkFee: BigDecimal,
|
||||
): List<TransactionData.Uncompiled> {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
|
|
@ -51,27 +53,23 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
) ?: error("Calculated yield contract address is null")
|
||||
|
||||
val yieldTokenStatus = cryptoCurrencyStatus.value.yieldSupplyStatus ?: getYieldTokenStatus(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
val maxNetworkFee = maxNetworkFee.convertToSdkAmount(cryptoCurrencyStatus)
|
||||
|
||||
return buildEnterTransactions(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
existingYieldContractAddress = existingYieldContractAddress,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
existingYieldAddress = existingYieldContractAddress,
|
||||
calculatedYieldContractAddress = calculatedYieldContractAddress,
|
||||
yieldTokenStatus = yieldTokenStatus,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createExitTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
yieldSupplyStatus: YieldSupplyStatus,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
fee: Fee?,
|
||||
): TransactionData.Uncompiled = withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency as CryptoCurrency.Token
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -87,51 +85,80 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = walletManager.getYieldContract(),
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
destinationAddress = walletManager.getYieldModuleAddress(),
|
||||
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
fee = fee,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildEnterTransactions(
|
||||
override suspend fun getProtocolBalance(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): BigDecimal? =
|
||||
withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
runCatching {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = cryptoCurrency.network.toBlockchain(),
|
||||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
walletManager.getProtocolBalance(
|
||||
token = Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
)
|
||||
}.onFailure(Timber::e).getOrThrow()
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private suspend fun buildEnterTransactions(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
existingYieldContractAddress: String?,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
existingYieldAddress: String?,
|
||||
calculatedYieldContractAddress: String,
|
||||
yieldTokenStatus: YieldSupplyStatus?,
|
||||
maxNetworkFee: Amount,
|
||||
): MutableList<TransactionData.Uncompiled> {
|
||||
val enterTransactions = mutableListOf<TransactionData.Uncompiled>()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency as CryptoCurrency.Token
|
||||
val yieldSupplyStatus = getYieldTokenStatus(walletManager, cryptoCurrency)
|
||||
|
||||
val amount = getEnterAmount(cryptoCurrency, yieldSupplyStatus)
|
||||
|
||||
val emptyContractAddress = existingYieldAddress == null || existingYieldAddress == EthereumUtils.ZERO_ADDRESS
|
||||
|
||||
when {
|
||||
existingYieldContractAddress == null || existingYieldContractAddress == EthereumUtils.ZERO_ADDRESS -> {
|
||||
yieldSupplyStatus == null || emptyContractAddress -> {
|
||||
enterTransactions.add(
|
||||
createDeployTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
amount = amount,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
),
|
||||
)
|
||||
}
|
||||
yieldTokenStatus == null -> error("Yield token status is null")
|
||||
!yieldTokenStatus.isInitialized -> enterTransactions.add(
|
||||
!yieldSupplyStatus.isInitialized -> enterTransactions.add(
|
||||
createInitTokenTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
amount = amount,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
),
|
||||
)
|
||||
!yieldTokenStatus.isActive -> enterTransactions.add(
|
||||
!yieldSupplyStatus.isActive -> enterTransactions.add(
|
||||
createReactivateTokenTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
amount = amount,
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
if (yieldTokenStatus?.isAllowedToSpend == false) {
|
||||
if (yieldSupplyStatus?.isAllowedToSpend != true) {
|
||||
enterTransactions.add(
|
||||
createTransaction(
|
||||
walletManager = walletManager,
|
||||
|
|
@ -141,20 +168,22 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
amount = null,
|
||||
),
|
||||
destinationAddress = cryptoCurrency.contractAddress,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
enterTransactions.add(
|
||||
createEnterTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldSupplyStatus = yieldTokenStatus,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
),
|
||||
)
|
||||
if (cryptoCurrencyStatus.value.amount.orZero() > BigDecimal.ZERO) {
|
||||
enterTransactions.add(
|
||||
createEnterTransaction(
|
||||
walletManager = walletManager,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
amount = amount,
|
||||
yieldContractAddress = calculatedYieldContractAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return enterTransactions
|
||||
}
|
||||
|
|
@ -170,12 +199,11 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
blockchain = cryptoCurrency.network.toBlockchain(),
|
||||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
walletManager.calculateYieldContract()
|
||||
}.onFailure(Timber::e)
|
||||
.getOrNull()
|
||||
walletManager.calculateYieldModuleAddress()
|
||||
}.onFailure(Timber::e).getOrThrow()
|
||||
}
|
||||
|
||||
private suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? =
|
||||
override suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? =
|
||||
withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
runCatching {
|
||||
|
|
@ -184,36 +212,42 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
blockchain = cryptoCurrency.network.toBlockchain(),
|
||||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
walletManager.getYieldContract()
|
||||
}.onFailure(Timber::e)
|
||||
.getOrNull()
|
||||
walletManager.getYieldModuleAddress()
|
||||
}.onFailure(Timber::e).getOrThrow()
|
||||
}
|
||||
|
||||
private suspend fun getYieldTokenStatus(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
): YieldSupplyStatus? = withContext(dispatchers.io) {
|
||||
require(cryptoCurrency is CryptoCurrency.Token)
|
||||
runCatching {
|
||||
val sdkSupplyStatus = walletManager.getYieldSupplyStatus(cryptoCurrency.contractAddress)
|
||||
val isAllowedToSpend = walletManager.isAllowedToSpend(cryptoCurrency.contractAddress)
|
||||
val isAllowedToSpend = walletManager.isAllowedToSpend(
|
||||
Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
)
|
||||
|
||||
YieldSupplyStatus(
|
||||
isActive = sdkSupplyStatus?.isActive == true,
|
||||
isInitialized = sdkSupplyStatus?.isInitialized == true,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
)
|
||||
}.onFailure(Timber::e).getOrNull()
|
||||
}.onFailure(Timber::e).getOrThrow()
|
||||
}
|
||||
|
||||
private fun createDeployTransaction(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
amount: Amount,
|
||||
maxNetworkFee: Amount,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
|
||||
tokenContractAddress = cryptoCurrency.contractAddress,
|
||||
walletAddress = walletManager.wallet.address,
|
||||
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
|
||||
val factoryContractAddress = walletManager.getYieldSupplyContractAddresses()?.factoryContractAddress
|
||||
|
|
@ -224,7 +258,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = factoryContractAddress,
|
||||
yieldSupplyStatus = null,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -233,11 +267,12 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
yieldContractAddress: String,
|
||||
yieldSupplyStatus: YieldSupplyStatus,
|
||||
amount: Amount,
|
||||
maxNetworkFee: Amount,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
|
||||
tokenContractAddress = cryptoCurrency.contractAddress,
|
||||
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
|
||||
return createTransaction(
|
||||
|
|
@ -245,7 +280,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = yieldContractAddress,
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -254,11 +289,12 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
yieldContractAddress: String,
|
||||
yieldSupplyStatus: YieldSupplyStatus,
|
||||
amount: Amount,
|
||||
maxNetworkFee: Amount,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
|
||||
tokenContractAddress = cryptoCurrency.contractAddress,
|
||||
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = maxNetworkFee,
|
||||
)
|
||||
|
||||
return createTransaction(
|
||||
|
|
@ -266,7 +302,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = yieldContractAddress,
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -274,7 +310,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
private fun createEnterTransaction(
|
||||
walletManager: WalletManager,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
yieldSupplyStatus: YieldSupplyStatus?,
|
||||
amount: Amount,
|
||||
yieldContractAddress: String,
|
||||
): TransactionData.Uncompiled {
|
||||
val callData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(
|
||||
|
|
@ -286,7 +322,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
callData = callData,
|
||||
destinationAddress = yieldContractAddress,
|
||||
yieldSupplyStatus = yieldSupplyStatus,
|
||||
amount = amount,
|
||||
fee = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -297,7 +333,7 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
cryptoCurrency: CryptoCurrency,
|
||||
callData: SmartContractCallData,
|
||||
destinationAddress: String,
|
||||
yieldSupplyStatus: YieldSupplyStatus?,
|
||||
amount: Amount,
|
||||
fee: Fee?,
|
||||
): TransactionData.Uncompiled {
|
||||
requireNotNull(cryptoCurrency as? CryptoCurrency.Token)
|
||||
|
|
@ -308,8 +344,6 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
blockchain = blockchain,
|
||||
)
|
||||
|
||||
val amount = getYieldSupplyAmount(cryptoCurrency, yieldSupplyStatus)
|
||||
|
||||
return if (fee != null) {
|
||||
walletManager.createTransaction(
|
||||
amount = amount,
|
||||
|
|
@ -350,22 +384,19 @@ internal class DefaultYieldSupplyTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getYieldSupplyAmount(cryptoCurrency: CryptoCurrency.Token, yieldSupplyStatus: YieldSupplyStatus?) =
|
||||
BigDecimal.ZERO.convertToSdkAmount(
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
amountType = AmountType.TokenYieldSupply(
|
||||
token = Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
isActive = yieldSupplyStatus?.isActive ?: false,
|
||||
isInitialized = yieldSupplyStatus?.isInitialized ?: false,
|
||||
isAllowedToSpend = yieldSupplyStatus?.isAllowedToSpend ?: false,
|
||||
private fun getEnterAmount(cryptoCurrency: CryptoCurrency.Token, yieldSupplyStatus: YieldSupplyStatus?) = Amount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = BigDecimal.ZERO,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
type = AmountType.TokenYieldSupply(
|
||||
token = Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO for TESTNET only
|
||||
}
|
||||
isActive = yieldSupplyStatus?.isActive ?: false,
|
||||
isInitialized = yieldSupplyStatus?.isInitialized ?: false,
|
||||
isAllowedToSpend = yieldSupplyStatus?.isAllowedToSpend ?: false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.data.yield.supply.converters
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object YieldMarketTokenConverter : Converter<YieldMarketsResponse.MarketDto, YieldMarketToken> {
|
||||
override fun convert(value: YieldMarketsResponse.MarketDto): YieldMarketToken {
|
||||
return YieldMarketToken(
|
||||
tokenAddress = value.tokenAddress,
|
||||
tokenSymbol = value.tokenSymbol,
|
||||
tokenName = value.tokenName,
|
||||
apy = value.apy,
|
||||
totalSupplied = value.totalSupplied,
|
||||
totalBorrowed = value.totalBorrowed,
|
||||
liquidityRate = value.liquidityRate,
|
||||
borrowRate = value.borrowRate,
|
||||
utilizationRate = value.utilizationRate,
|
||||
isActive = value.isActive,
|
||||
ltv = value.ltv,
|
||||
liquidationThreshold = value.liquidationThreshold,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
package com.tangem.data.yield.supply.di
|
||||
|
||||
import com.tangem.data.yield.supply.DefaultYieldSupplyMarketRepository
|
||||
import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver
|
||||
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
|
||||
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
|
||||
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
|
||||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -25,4 +31,24 @@ internal object YieldSupplyDataModule {
|
|||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyMarketRepository(
|
||||
yieldSupplyApi: YieldSupplyApi,
|
||||
store: YieldMarketsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): YieldSupplyMarketRepository {
|
||||
return DefaultYieldSupplyMarketRepository(
|
||||
yieldSupplyApi = yieldSupplyApi,
|
||||
store = store,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyErrorResolver(): YieldSupplyErrorResolver {
|
||||
return DefaultYieldSupplyErrorResolver
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,9 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYieldSupplyStatus
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
@ -26,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach
|
|||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYieldSupplyStatus
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultYieldSupplyTransactionRepositoryTest {
|
||||
|
|
@ -69,12 +68,16 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `createEnterTransactions returns deploy-approve-enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns EthereumUtils.ZERO_ADDRESS
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns EthereumUtils.ZERO_ADDRESS
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns null
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns false
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldModuleAddress() } returns yieldContractAddress
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 3 transactions are returned: deploy, approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -84,7 +87,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
|
||||
walletAddress = walletManager.wallet.address,
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -115,15 +118,20 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `createEnterTransactions returns init-approve-enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns false
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 3 transactions are returned: init token, approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -132,7 +140,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
// Check transaction - init token
|
||||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -163,15 +171,20 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `createEnterTransactions returns reactivate-approve-enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns false
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = true,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 3 transactions are returned: reactivate token, approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -180,7 +193,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
// Check transaction - reactivate token
|
||||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -211,8 +224,8 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `createEnterTransactions returns reactivate-enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = true,
|
||||
|
|
@ -220,7 +233,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
)
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns true
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that 2 transactions are returned: approve, enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -229,7 +246,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
// Check transaction - reactivate token
|
||||
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
|
||||
tokenContractAddress = mockedContractAddress,
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
|
||||
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
)
|
||||
val firstTransaction = result.first()
|
||||
|
||||
|
|
@ -248,8 +265,8 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `createEnterTransactions returns enter transactions`() = runTest {
|
||||
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.calculateYieldModuleAddress() } returns yieldContractAddress
|
||||
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
|
||||
isActive = true,
|
||||
isInitialized = true,
|
||||
|
|
@ -257,7 +274,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
)
|
||||
coEvery { walletManager.isAllowedToSpend(any()) } returns true
|
||||
|
||||
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
|
||||
val result = repository.createEnterTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxNetworkFee = BigDecimal.TEN,
|
||||
)
|
||||
|
||||
// Assert that transaction is returned: enter
|
||||
Truth.assertThat(result).isNotNull()
|
||||
|
|
@ -277,9 +298,7 @@ class DefaultYieldSupplyTransactionRepositoryTest {
|
|||
val expectedCallData =
|
||||
YieldSupplyContractCallDataProviderFactory.getExitCallData(mockedContractAddress)
|
||||
|
||||
val yieldSupplyStatus = mockk<YieldSupplyStatus>(relaxed = true)
|
||||
|
||||
val result = repository.createExitTransaction(userWalletId, cryptoCurrency, yieldSupplyStatus, null)
|
||||
val result = repository.createExitTransaction(userWalletId, cryptoCurrencyStatus, null)
|
||||
|
||||
Truth.assertThat(result).isNotNull()
|
||||
Truth.assertThat(result.extras).isInstanceOf(EthereumTransactionExtras::class.java)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue