Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-14 16:03:31 +03:00
commit 76bf94f718
407 changed files with 9866 additions and 3473 deletions

View file

@ -70,8 +70,4 @@ internal class DefaultCardSdkConfigRepository(
override fun setLinkedTerminal(isLinked: Boolean?) {
sdk.config.linkedTerminal = isLinked
}
override fun setTangemApiProdEnvFlag(flag: Boolean) {
sdk.config.isTangemAttestationProdEnv = flag
}
}

View file

@ -20,7 +20,18 @@ interface CardCryptoCurrencyFactory {
* @param network network
*/
@Throws
suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency>
suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency> {
return create(userWalletId = userWalletId, networks = setOf(network))[network].orEmpty()
}
/**
* Universal method for creating list of [CryptoCurrency] in [networks] for any card
*
* @param userWalletId user wallet id that determines type of card
* @param networks networks
*/
@Throws
suspend fun create(userWalletId: UserWalletId, networks: Set<Network>): Map<Network, List<CryptoCurrency>>
/**
* Universal method for creating list of [CryptoCurrency] in [network] for any card

View file

@ -2,7 +2,6 @@ package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.local.token.UserTokensResponseStore
@ -36,40 +35,45 @@ internal class DefaultCardCryptoCurrencyFactory(
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency> {
override suspend fun create(
userWalletId: UserWalletId,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
// multi-currency wallet
if (userWallet !is UserWallet.Cold || userWallet.isMultiCurrency) {
return getMultiWalletCurrencies(userWallet = userWallet, networks = setOf(network))[network].orEmpty()
return getMultiWalletCurrencies(userWallet = userWallet, networks = networks)
}
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
if (cardBlockchain != blockchain) return emptyList()
val cardNetworkId = userWallet.scanResponse.cardTypesResolver.getBlockchain().toNetworkId()
val cardNetwork = networks.firstOrNull { it.backendId == cardNetworkId }
if (cardNetwork == null) return emptyMap()
// single-currency wallet with token (NODL)
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
val currencies = createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
return mapOf(cardNetwork to currencies)
}
// single-currency wallet
return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
val primaryCurrency = createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
return mapOf(cardNetwork to listOf(primaryCurrency))
}
override suspend fun createByRawId(userWalletId: UserWalletId, networkRawId: Network.RawID): List<CryptoCurrency> {
override suspend fun createByRawId(userWalletId: UserWalletId, network: Network.RawID): List<CryptoCurrency> {
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
val blockchain = networkRawId.toBlockchain()
val blockchain = network.toBlockchain()
// multi-currency wallet
if (userWallet.isMultiCurrency || userWallet !is UserWallet.Cold) {
return getMultiWalletCurrenciesByRawId(
userWallet = userWallet,
rawIds = setOf(networkRawId),
)[networkRawId].orEmpty()
rawIds = setOf(network),
)[network].orEmpty()
}
// check if the blockchain of single-currency wallet is the same as network
@ -143,13 +147,15 @@ internal class DefaultCardCryptoCurrencyFactory(
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
return responseCryptoCurrenciesFactory.createCurrencies(
val existingNetworkWithCurrencies = responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
)
.groupBy(CryptoCurrency::network)
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
}
private suspend fun getMultiWalletCurrenciesByRawId(

View file

@ -266,7 +266,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = createUserTokensResponse(),
expected = Result.success(emptyMap()),
expected = Result.success(
setOf(ethereum.network, bitcoin.network).associateWith { emptyList() },
),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),

View file

@ -7,11 +7,10 @@ import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -22,7 +21,6 @@ import javax.inject.Inject
* Default implementation of [MultiNetworkStatusFetcher]
*
* @property networksStatusesStore networks statuses store
* @property userWalletsStore user wallets store
* @property cardCryptoCurrencyFactory card crypto currency factory
* @property commonNetworkStatusFetcher common network status fetcher
* @property dispatchers dispatchers
@ -32,7 +30,6 @@ import javax.inject.Inject
@Suppress("LongParameterList")
internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
private val networksStatusesStore: NetworksStatusesStore,
private val userWalletsStore: UserWalletsStore,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
private val dispatchers: CoroutineDispatcherProvider,
@ -41,8 +38,8 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks)
val userWallet = catch(
block = { userWalletsStore.getSyncStrict(key = params.userWalletId) },
val networksCurrencies = catch(
block = { createNetworksCurrenciesMap(params) },
catch = {
networksStatusesStore.setSourceAsOnlyCache(
userWalletId = params.userWalletId,
@ -53,31 +50,6 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
},
)
val cardTypesResolver = userWallet.requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY]
val isWalletSupported = with(cardTypesResolver) {
isMultiwalletAllowed() || isSingleWalletWithToken()
}
ensure(isWalletSupported) {
networksStatusesStore.setSourceAsOnlyCache(
userWalletId = params.userWalletId,
networks = params.networks,
)
IllegalStateException("User wallet is not multi-currency")
}
val networksCurrencies = if (cardTypesResolver.isMultiwalletAllowed()) {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(
userWallet = userWallet,
networks = params.networks,
)
} else {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.scanResponse,
)
.groupBy { it.network }
}
val result = coroutineScope {
params.networks
.map { network ->
@ -98,4 +70,13 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
IllegalStateException("Failed to fetch network statuses")
}
}
private suspend fun createNetworksCurrenciesMap(
params: MultiNetworkStatusFetcher.Params,
): Map<Network, List<CryptoCurrency>> {
return cardCryptoCurrencyFactory.create(
userWalletId = params.userWalletId,
networks = params.networks,
)
}
}

View file

@ -1,47 +1,27 @@
package com.tangem.data.networks.single
import arrow.core.Either
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
/**
* Default implementation of [SingleNetworkStatusFetcher]
*
* @property commonNetworkStatusFetcher common network status fetcher
* @property networksStatusesStore networks statuses store
* @property cardCryptoCurrencyFactory card crypto currency factory
* @property dispatchers dispatchers
* @property multiNetworkStatusFetcher multi network status fetcher
*
[REDACTED_AUTHOR]
*/
internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
private val networksStatusesStore: NetworksStatusesStore,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val dispatchers: CoroutineDispatcherProvider,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
) : SingleNetworkStatusFetcher {
override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either<Throwable, Unit> {
return Either.catchOn(dispatchers.default) {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
val networkCurrencies = cardCryptoCurrencyFactory.create(
return multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = params.userWalletId,
network = params.network,
)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = networkCurrencies.toSet(),
)
.onLeft { throw it }
}
networks = setOf(params.network),
),
)
}
}

View file

@ -1,19 +1,15 @@
package com.tangem.data.networks.multi
import arrow.core.Either
import com.google.common.truth.Truth
import arrow.core.left
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.common.test.utils.assertEither
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
@ -29,13 +25,11 @@ import org.junit.jupiter.api.TestInstance
internal class DefaultMultiNetworkStatusFetcherTest {
private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true)
private val userWalletsStore: UserWalletsStore = mockk()
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
private val fetcher = DefaultMultiNetworkStatusFetcher(
networksStatusesStore = networksStatusesStore,
userWalletsStore = userWalletsStore,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
commonNetworkStatusFetcher = commonNetworkStatusFetcher,
dispatchers = TestingCoroutineDispatcherProvider(),
@ -43,16 +37,17 @@ internal class DefaultMultiNetworkStatusFetcherTest {
@BeforeEach
fun resetMocks() {
clearMocks(networksStatusesStore, userWalletsStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher)
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher)
}
@Test
fun `fetch successfully for multi-currency card`() = runTest {
fun `fetch successfully`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
val params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOf(ethereum.network, cardano.network),
)
val networksCurrencies = mapOf(
ethereum.network to listOf(ethereum),
cardano.network to listOf(cardano),
@ -60,16 +55,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
val ethereumFetcherResult = Either.Right(Unit)
val cardanoFetcherResult = Either.Right(Unit)
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet.Cold::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true
coEvery {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
} returns networksCurrencies
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
@ -92,14 +78,11 @@ internal class DefaultMultiNetworkStatusFetcherTest {
// Assert
val expected = Either.Right(Unit)
Truth.assertThat(actual).isEqualTo(expected)
assertEither(actual, expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
cardCryptoCurrencyFactory.create(params.userWalletId, params.networks)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
@ -114,75 +97,17 @@ internal class DefaultMultiNetworkStatusFetcherTest {
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
}
}
@Test
fun `fetch successfully for single-currency card with token`() = runTest {
// Arrange
val networks = setOf(ethereum.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
val networksCurrencies = listOf(ethereum)
val ethereumFetcherResult = Either.Right(Unit)
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet.Cold::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false
coEvery { cardTypesResolver.isSingleWalletWithToken() } returns true
coEvery {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
} returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
} returns ethereumFetcherResult
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Right(Unit)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any())
}
}
@Test
fun `fetch failure if one of them fails`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
val params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOf(ethereum.network, cardano.network),
)
val networksCurrencies = mapOf(
ethereum.network to listOf(ethereum),
cardano.network to listOf(cardano),
@ -190,16 +115,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
val ethereumFetcherResult = Either.Left(IllegalStateException())
val cardanoFetcherResult = Either.Right(Unit)
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet.Cold::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true
coEvery {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
} returns networksCurrencies
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
@ -222,15 +138,11 @@ internal class DefaultMultiNetworkStatusFetcherTest {
// Assert
val expected = Either.Left(IllegalStateException("Failed to fetch network statuses"))
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
assertEither(actual, expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
cardCryptoCurrencyFactory.create(params.userWalletId, params.networks)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
@ -245,18 +157,17 @@ internal class DefaultMultiNetworkStatusFetcherTest {
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
}
}
@Test
fun `fetch failure if all of them fails`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
val params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOf(ethereum.network, cardano.network),
)
val networksCurrencies = mapOf(
ethereum.network to listOf(ethereum),
cardano.network to listOf(cardano),
@ -264,16 +175,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
val ethereumFetcherResult = Either.Left(IllegalStateException("ethereum"))
val cardanoFetcherResult = Either.Left(IllegalStateException("cardano"))
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet.Cold::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true
coEvery {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
} returns networksCurrencies
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
@ -296,15 +198,11 @@ internal class DefaultMultiNetworkStatusFetcherTest {
// Assert
val expected = Either.Left(IllegalStateException("Failed to fetch network statuses"))
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
assertEither(actual, expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
cardCryptoCurrencyFactory.create(params.userWalletId, params.networks)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
@ -319,85 +217,35 @@ internal class DefaultMultiNetworkStatusFetcherTest {
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
}
}
@Test
fun `fetch failure if userWalletsStore throws exception`() = runTest {
fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWalletStoreException = IllegalStateException()
val params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOf(ethereum.network, cardano.network),
)
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } throws userWalletStoreException
coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit
val factoryException = IllegalStateException()
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } throws factoryException
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(userWalletStoreException)
Truth.assertThat(actual).isEqualTo(expected)
val expected = factoryException.left()
assertEither(actual, expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardCryptoCurrencyFactory.create(params.userWalletId, params.networks)
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any())
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
commonNetworkStatusFetcher.fetch(any(), any(), any())
}
}
@Test
fun `fetch failure if card is single-currency`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet.Cold::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false
coEvery { cardTypesResolver.isSingleWalletWithToken() } returns false
coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(IllegalStateException("User wallet is not multi-currency"))
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardTypesResolver.isSingleWalletWithToken()
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any())
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
commonNetworkStatusFetcher.fetch(any(), any(), any())
}
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) }
}
private companion object {

View file

@ -2,16 +2,15 @@ package com.tangem.data.networks.single
import arrow.core.Either
import arrow.core.left
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.common.test.utils.assertEither
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@ -23,114 +22,51 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultSingleNetworkStatusFetcherTest {
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true)
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk()
private val fetcher = DefaultSingleNetworkStatusFetcher(
commonNetworkStatusFetcher = commonNetworkStatusFetcher,
networksStatusesStore = networksStatusesStore,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val fetcher = DefaultSingleNetworkStatusFetcher(multiNetworkStatusFetcher = multiNetworkStatusFetcher)
@BeforeEach
fun resetMocks() {
clearMocks(commonNetworkStatusFetcher, networksStatusesStore, cardCryptoCurrencyFactory)
clearMocks(multiNetworkStatusFetcher)
}
@Test
fun `fetch successfully`() = runTest {
// Arrange
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network)
val networkCurrencies = listOf(ethereum)
val commonFetcherResult = Either.Right(Unit)
val multiParams = MultiNetworkStatusFetcher.Params(userWalletId, setOf(params.network))
val multiFetcherResult = Either.Right(Unit)
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = networkCurrencies.toSet(),
)
} returns commonFetcherResult
coEvery { multiNetworkStatusFetcher(params = multiParams) } returns multiFetcherResult
// Act
val actual = fetcher(params)
// Assert
val expected = commonFetcherResult
val expected = multiFetcherResult
assertEither(actual, expected)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.network)
cardCryptoCurrencyFactory.create(params.userWalletId, params.network)
commonNetworkStatusFetcher.fetch(params.userWalletId, params.network, setOf(ethereum))
}
coVerify(exactly = 1) { multiNetworkStatusFetcher(params = multiParams) }
}
@Test
fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest {
fun `fetch failure`() = runTest {
// Arrange
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network)
val factoryException = IllegalStateException()
val multiParams = MultiNetworkStatusFetcher.Params(userWalletId, setOf(params.network))
val multiFetcherResult = IllegalStateException("Error").left()
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws factoryException
coEvery { multiNetworkStatusFetcher(params = multiParams) } returns multiFetcherResult
// Act
val actual = fetcher(params)
// Arrange
val expected = Either.Left(factoryException)
Truth.assertThat(actual).isEqualTo(expected)
val expected = multiFetcherResult
assertEither(actual, expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network)
}
coVerify(inverse = true) {
commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any())
}
}
@Test
fun `fetch failure if commonNetworkStatusFetcher returns exception`() = runTest {
// Arrange
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network)
val networkCurrencies = listOf(ethereum)
val commonFetcherResult = IllegalStateException().left()
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = networkCurrencies.toSet(),
)
} returns commonFetcherResult
// Act
val actual = fetcher(params)
// Arrange
val expected = commonFetcherResult
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = setOf(ethereum),
)
}
coVerify(exactly = 1) { multiNetworkStatusFetcher(params = multiParams) }
}
private companion object {

View file

@ -29,7 +29,6 @@ import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -50,7 +49,6 @@ internal class DefaultNFTRepository @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val dispatchers: CoroutineDispatcherProvider,
private val userWalletsStore: UserWalletsStore,
private val nftFeatureToggles: NFTFeatureToggles,
private val networkFactory: NetworkFactory,
private val excludedBlockchains: ExcludedBlockchains,
resources: Resources,
@ -545,13 +543,9 @@ internal class DefaultNFTRepository @Inject constructor(
private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean {
val scanResponse = userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().scanResponse
val blockchain = Blockchain.fromNetworkId(backendId)
return when {
blockchain == null -> false
blockchain.isEvm() && !nftFeatureToggles.isNFTEVMEnabled -> false
blockchain == Blockchain.Solana && !nftFeatureToggles.isNFTSolanaEnabled -> false
else -> blockchain.canHandleNFTs() &&
scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains)
}
val blockchain = Blockchain.fromNetworkId(backendId) ?: return false
return blockchain.canHandleNFTs() &&
scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains)
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -32,7 +31,6 @@ internal object NFTDataModule {
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
userWalletsStore: UserWalletsStore,
nftFeatureToggles: NFTFeatureToggles,
networkFactory: NetworkFactory,
): NFTRepository = DefaultNFTRepository(
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
@ -41,7 +39,6 @@ internal object NFTDataModule {
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
userWalletsStore = userWalletsStore,
nftFeatureToggles = nftFeatureToggles,
networkFactory = networkFactory,
resources = context.resources,
)

View file

@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.qrscanning.models.QrResult
import com.tangem.domain.qrscanning.models.RawQrResult
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -17,17 +18,17 @@ import java.net.URLDecoder
internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
private data class QrScanningEvent(val type: SourceType, val qrCode: String)
private data class QrScanningEvent(val qrCode: RawQrResult)
private val scannedEvents = MutableSharedFlow<QrScanningEvent>(replay = 1)
override suspend fun emitResult(type: SourceType, qrCode: String) {
scannedEvents.emit(QrScanningEvent(type, qrCode))
override suspend fun emitResult(qrCode: RawQrResult) {
scannedEvents.emit(QrScanningEvent(qrCode))
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun subscribeToScanningResults(type: SourceType) = scannedEvents
.filter { it.type == type }
.filter { it.qrCode.requestSource == type }
.map { it.qrCode }
.onEach {
yield() // if we have more than one sub, we must allow them to collect emitted value

View file

@ -86,7 +86,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
val actual = fetcher(params)
// Assert
val expected = IllegalStateException("${this::class.simpleName} supports only multi-currency wallet").left()
val expected = IllegalStateException(
"${DefaultMultiWalletCryptoCurrenciesFetcher::class.simpleName} supports only multi-currency wallet",
).left()
assertEither(actual, expected)
verifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) }

View file

@ -36,7 +36,7 @@ import java.math.BigInteger
internal class DefaultTransactionRepository(
private val walletManagersFacade: WalletManagersFacade,
private val walletManagersStore: WalletManagersStore,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
private val dispatchers: CoroutineDispatcherProvider,
) : TransactionRepository {
override suspend fun createTransaction(
@ -47,7 +47,7 @@ internal class DefaultTransactionRepository(
userWalletId: UserWalletId,
network: Network,
txExtras: TransactionExtras?,
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
): TransactionData.Uncompiled = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
@ -84,7 +84,7 @@ internal class DefaultTransactionRepository(
userWalletId: UserWalletId,
network: Network,
nonce: BigInteger?,
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
): TransactionData.Uncompiled = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
@ -138,7 +138,7 @@ internal class DefaultTransactionRepository(
spenderAddress: String,
userWalletId: UserWalletId,
network: Network,
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
): TransactionData.Uncompiled = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val extras = createTransactionDataExtras(
@ -171,7 +171,7 @@ internal class DefaultTransactionRepository(
destinationAddress: String,
userWalletId: UserWalletId,
network: Network,
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
): TransactionData.Uncompiled = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
// For now transfer one nft asset at a time
@ -229,7 +229,7 @@ internal class DefaultTransactionRepository(
destination: String,
userWalletId: UserWalletId,
network: Network,
): Result<Unit> = withContext(coroutineDispatcherProvider.io) {
): Result<Unit> = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
@ -260,7 +260,7 @@ internal class DefaultTransactionRepository(
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
) = withContext(coroutineDispatcherProvider.io) {
) = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
@ -276,7 +276,7 @@ internal class DefaultTransactionRepository(
userWalletId: UserWalletId,
network: Network,
sendMode: TransactionSender.MultipleTransactionSendMode,
) = withContext(coroutineDispatcherProvider.io) {
) = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
@ -366,13 +366,9 @@ internal class DefaultTransactionRepository(
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): Result<ByteArray> = withContext(coroutineDispatcherProvider.io) {
) = withContext(dispatchers.io) {
val preparer = getPreparer(network, userWalletId)
when (val prepareForSend = preparer.prepareForSend(transactionData, signer)) {
is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error)
is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data)
}
preparer.prepareForSend(transactionData, signer)
}
override suspend fun prepareForSendMultiple(
@ -380,13 +376,9 @@ internal class DefaultTransactionRepository(
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): Result<List<ByteArray>> = withContext(coroutineDispatcherProvider.io) {
) = withContext(dispatchers.io) {
val preparer = getPreparer(network, userWalletId)
when (val prepareForSend = preparer.prepareForSendMultiple(transactionData, signer)) {
is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error)
is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data)
}
preparer.prepareForSendMultiple(transactionData, signer)
}
private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer {

View file

@ -24,12 +24,12 @@ internal object TransactionDataModule {
fun providesTransactionRepository(
walletManagersFacade: WalletManagersFacade,
walletManagersStore: WalletManagersStore,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
dispatchers: CoroutineDispatcherProvider,
): TransactionRepository {
return DefaultTransactionRepository(
walletManagersFacade = walletManagersFacade,
walletManagersStore = walletManagersStore,
coroutineDispatcherProvider = coroutineDispatcherProvider,
dispatchers = dispatchers,
)
}

View file

@ -167,6 +167,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
return when (env) {
ApiEnvironment.DEV,
ApiEnvironment.STAGE,
ApiEnvironment.MOCK,
-> rsaPublicKey.dev
ApiEnvironment.PROD -> rsaPublicKey.prod
}

View file

@ -19,6 +19,8 @@ import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.WcRequestService
@ -70,7 +72,9 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun defaultWcPairUseCase(): WcPairService = DefaultWcPairService()
fun defaultWcPairUseCase(sessionsManager: DefaultWcSessionsManager): WcPairService = DefaultWcPairService(
sessionsManager,
)
@Provides
@Singleton
@ -88,6 +92,7 @@ internal object WalletConnectDataModule {
legacyStore: WalletConnectSessionsRepository,
getWallets: GetWalletsUseCase,
associateNetworks: AssociateNetworksDelegate,
analytics: AnalyticsEventHandler,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcSessionsManager(
@ -96,6 +101,7 @@ internal object WalletConnectDataModule {
legacyStore = legacyStore,
getWallets = getWallets,
associateNetworks = associateNetworks,
analytics = analytics,
scope = scope,
)
}
@ -169,10 +175,14 @@ internal object WalletConnectDataModule {
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
getWallets: GetWalletsUseCase,
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateNetworksDelegate = AssociateNetworksDelegate(
namespaceConverters = namespaceConverters,
getWallets = getWallets,
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides

View file

@ -48,7 +48,7 @@ internal class DefaultWcInitializeUseCase(
application = application,
metaData = appMetaData,
) { error ->
Timber.e("Error while initializing client: $error")
Timber.tag(WC_TAG).e("Error while initializing client: $error")
}
WalletKit.initialize(
@ -60,7 +60,7 @@ internal class DefaultWcInitializeUseCase(
Timber.tag(WC_TAG).i("onWcSdkInit")
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")
Timber.tag(WC_TAG).e("Error while initializing Web3Wallet: $error")
},
)
}

View file

@ -5,6 +5,7 @@ import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase
@ -27,7 +28,7 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor(
override val walletAddress: String
get() = context.accountAddress
override suspend fun approve(): Either<Throwable, Unit> {
override suspend fun approve(): Either<WcRequestError, String> {
return respondService.respond(rawSdkRequest, "")
}

View file

@ -19,6 +19,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.error.parseTangemSdkError
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
@ -59,7 +60,7 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor(
?: return
val signedHash = signUseCase(hashToSign, userWallet, network)
.onLeft { emit(state.toResult(it.left())) }
.onLeft { emit(state.toResult(parseTangemSdkError(it).left())) }
.getOrNull() ?: return
val respond = prepareToSendMessageData(signedHash, hashToSign, walletManager)

View file

@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.network.ethereum
import arrow.core.left
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
import com.tangem.blockchain.common.Amount as BlockchainAmount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
@ -17,6 +16,7 @@ import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcApprovedAmount
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.method.*
@ -24,13 +24,16 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.*
import com.tangem.blockchain.common.Amount as BlockchainAmount
@Suppress("LongParameterList")
internal class WcEthSendTransactionUseCase @AssistedInject constructor(
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SendTransaction,
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val sendTransaction: SendTransactionUseCase,
private val ethTxHelper: WcEthTxHelper,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<WcEthTxAction, TransactionData>(),
WcTransactionUseCase,
@ -38,10 +41,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
private var dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
private var dAppFee: Fee? = null
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
blockAidDelegate.getSecurityStatus(
@ -52,7 +52,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
accountAddress = context.accountAddress,
).map { lce ->
lce.map { result ->
val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result)
val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result)
?: return@map BlockAidTransactionCheck.Result.Plain(result)
val tokenInfo = amount.tokenInfo
this@WcEthSendTransactionUseCase.approvalAmount = WcApprovedAmount(
@ -80,8 +80,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = sendTransaction(state.signModel, wallet, network)
.onLeft { error ->
val sendError = IllegalArgumentException(error.toString()) // todo(wc) use domain error
emit(state.toResult(sendError.left()))
emit(state.toResult(parseSendError(error).left()))
}
.getOrNull() ?: return
val respondResult = respondService.respond(rawSdkRequest, hash.formatHex())
@ -112,7 +111,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(newState)
}
override fun dAppFee(): Fee.Ethereum.Legacy? {
override fun dAppFee(): Fee? {
return dAppFee
}
@ -121,8 +120,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee(),
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
network = context.network,
txParams = method.transaction,
) ?: return@flow

View file

@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.network.ethereum
import arrow.core.left
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
import com.tangem.blockchain.common.Amount as BlockchainAmount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
@ -17,22 +16,22 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcApprovedAmount
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.method.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import com.tangem.blockchain.common.Amount as BlockchainAmount
@Suppress("LongParameterList")
internal class WcEthSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
private val ethTxHelper: WcEthTxHelper,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SignTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
@ -42,10 +41,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
private var dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
private var dAppFee: Fee? = null
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
@ -55,7 +51,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
accountAddress = context.accountAddress,
).map { lce ->
lce.map { result ->
val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result)
val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result)
?: return@map BlockAidTransactionCheck.Result.Plain(result)
val tokenInfo = amount.tokenInfo
if (!amount.isUnlimited) {
@ -82,7 +78,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
val hash = prepareForSend(state.signModel, wallet, network)
.map { it.toHexString().formatHex() }
.onLeft { error ->
emit(state.toResult(error.left()))
emit(state.toResult(parseSendError(error).left()))
}
.getOrNull()
?: return
@ -119,15 +115,16 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee(),
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
network = context.network,
txParams = method.transaction,
) ?: return@flow
emitAll(delegate.invoke(transactionData))
}
override fun dAppFee(): Fee.Ethereum.Legacy? {
override fun dAppFee(): Fee? {
return dAppFee
}

View file

@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.error.parseTangemSdkError
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
@ -51,7 +52,7 @@ internal class WcEthSignTypedDataUseCase @AssistedInject constructor(
?: return
val signedHash = signUseCase(hashToSign, userWallet, network)
.onLeft { emit(state.toResult(it.left())) }
.onLeft { emit(state.toResult(parseTangemSdkError(it).left())) }
.getOrNull() ?: return
val respond = prepareToSendMessageData(signedHash, hashToSign, walletManager)

View file

@ -7,40 +7,43 @@ import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.HEX_PREFIX
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.blockchain.extensions.hexToBigInteger
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.extensions.hexToBytes
import com.tangem.data.common.currency.getCoinId
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase
import com.tangem.domain.walletconnect.model.WcApprovedAmount
import com.tangem.domain.walletconnect.model.WcEthTransactionParams
import java.math.BigDecimal
import com.tangem.domain.wallets.models.UserWallet
import javax.inject.Inject
internal object WcEthTxHelper {
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
internal class WcEthTxHelper @Inject constructor(
private val getSingleCryptoCurrency: GetSingleCryptoCurrencyStatusUseCase,
private val ethSpecificFee: GetEthSpecificFeeUseCase,
) {
fun getDAppFee(network: Network, txParams: WcEthTransactionParams): Fee.Ethereum.Legacy? {
val gasLimit = txParams.gas?.hexToBigDecimal() ?: return null
val gasPrice = txParams.gasPrice?.hexToBigDecimal() ?: return null
val blockchain = network.toBlockchain()
var feeDecimal = (gasLimit * gasPrice)
.movePointLeft(blockchain.decimals())
if (blockchain == Blockchain.Mantle) {
feeDecimal = feeDecimal.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
}
val feeAmount = Amount(feeDecimal, blockchain)
return Fee.Ethereum.Legacy(feeAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger())
suspend fun getDAppFee(txParams: WcEthTransactionParams, userWallet: UserWallet, network: Network): Fee? {
val gasLimit = txParams.gas?.hexToBigInteger() ?: return null
val gasPrice = txParams.gasPrice?.hexToBigInteger()
val coinId = getCoinId(network, network.toBlockchain().toCoinId())
val currency = getSingleCryptoCurrency.invokeMultiWalletSync(userWallet.walletId, coinId)
.map { it.currency }
.getOrNull() ?: return null
return ethSpecificFee(userWallet, currency, gasLimit, gasPrice)
.map { it.minimum }
.getOrNull()
}
fun createTransactionData(
dAppFee: Fee.Ethereum.Legacy?,
dAppFee: Fee?,
network: Network,
txParams: WcEthTransactionParams,
): TransactionData.Uncompiled? {

View file

@ -12,6 +12,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.error.parseTangemSdkError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
@ -40,7 +41,7 @@ internal class WcSolanaMessageSignUseCase @AssistedInject constructor(
val userWallet = session.wallet
val signedHash = signUseCase(hashToSign, userWallet, network)
.onLeft { emit(state.toResult(it.left())) }
.onLeft { emit(state.toResult(parseTangemSdkError(it).left())) }
.getOrNull() ?: return
val respond = "{ signature: \"${signedHash.encodeBase58()}\" }"

View file

@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
import com.tangem.domain.walletconnect.usecase.method.WcListTransactionUseCase
@ -47,7 +48,7 @@ internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor(
override suspend fun SignCollector<List<TransactionData>>.onSign(state: WcSignState<List<TransactionData>>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(error.left()))
emit(state.toResult(parseSendError(error).left()))
}
.getOrNull()
?: return

View file

@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
import com.tangem.domain.walletconnect.usecase.method.WcSignState
@ -45,7 +46,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor(
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(error.left()))
emit(state.toResult(parseSendError(error).left()))
}
.getOrNull()
?: return

View file

@ -2,13 +2,18 @@ package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.Wallet.Model.Namespace
import com.tangem.data.common.currency.isCustomCoin
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
@ -16,10 +21,12 @@ internal class AssociateNetworksDelegate(
private val namespaceConverters: Set<WcNamespaceConverter>,
private val getWallets: GetWalletsUseCase,
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend fun associate(wallet: UserWallet, namespaces: Map<String, Namespace.Session>): Set<Network> {
val walletNetworks = getWalletNetworks(wallet)
val walletNetworks = getWalletNetworks(userWalletId = wallet.walletId)
val namespacesSet = namespaces.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
return namespacesSet.mapNotNullTo(mutableSetOf()) { chainId ->
val wcNetwork = namespaceConverters
@ -33,19 +40,25 @@ internal class AssociateNetworksDelegate(
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
val requiredNamespaces: Set<String> = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces: Set<String> = sessionProposal.optionalNamespaces.setOfChainId()
// remove duplicates
.subtract(requiredNamespaces)
return userWallets
.associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) }
return userWallets.associateWith { wallet ->
mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces, sessionProposal)
}
}
@Suppress("ComplexCondition")
private suspend fun mapNetworksForWallet(
wallet: UserWallet,
requiredNamespaces: Set<String>,
optionalNamespaces: Set<String>,
sessionProposal: Wallet.Model.SessionProposal,
): ProposalNetwork {
val walletNetworks = getWalletNetworks(wallet)
val walletNetworks = getWalletNetworks(userWalletId = wallet.walletId)
val unknownRequired = mutableSetOf<String>()
val unknownOptional = mutableSetOf<String>()
val missingRequired = mutableSetOf<Network>()
val required = mutableSetOf<Network>()
val available = mutableSetOf<Network>()
@ -58,7 +71,8 @@ internal class AssociateNetworksDelegate(
return@forEach
}
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
if (walletNetwork == null) {
if (walletNetwork == null || isCustomCoin(walletNetwork)) {
missingRequired.add(wcNetwork)
} else {
required.add(walletNetwork)
@ -66,15 +80,23 @@ internal class AssociateNetworksDelegate(
}
optionalNamespaces.forEach { chainId ->
val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
?: return@forEach
if (wcNetwork == null) {
unknownOptional.add(missingNetworkName(chainId))
return@forEach
}
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
if (walletNetwork != null) {
if (walletNetwork != null && !isCustomCoin(walletNetwork)) {
available.add(walletNetwork)
} else {
notAdded.add(wcNetwork)
}
}
if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedBlockchains(unknownRequired)
if (unknownRequired.isNotEmpty()) {
throw WcPairError.UnsupportedBlockchains(unknownRequired, sessionProposal.name)
}
if (unknownOptional.isNotEmpty() && required.isEmpty() && available.isEmpty() && missingRequired.isEmpty()) {
throw WcPairError.UnsupportedBlockchains(unknownOptional, sessionProposal.name)
}
return ProposalNetwork(
wallet = wallet,
missingRequired = missingRequired,
@ -84,10 +106,18 @@ internal class AssociateNetworksDelegate(
)
}
private suspend fun getWalletNetworks(wallet: UserWallet): List<Network> =
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
private suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
)
.orEmpty()
} else {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
.map(CryptoCurrency.Coin::network)
}
private fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()

View file

@ -1,17 +1,45 @@
package com.tangem.data.walletconnect.pair
import androidx.core.net.toUri
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject
class DefaultWcPairService @Inject constructor() : WcPairService {
class DefaultWcPairService @Inject constructor(
private val sessionsManager: WcSessionsManager,
) : WcPairService {
private val _pairFlow: Channel<WcPairRequest> = Channel(Channel.BUFFERED)
override val pairFlow: Flow<WcPairRequest> = _pairFlow.receiveAsFlow()
override val pairFlow: Flow<WcPairRequest> = _pairFlow
.receiveAsFlow()
.filter(::filterDeeplinkRequestWithSessionTopic)
override fun pair(request: WcPairRequest) {
_pairFlow.trySend(request)
}
// some dApp sends deeplink with session request
// we filter session exist, but start dApp pair flow if unexist
private suspend fun filterDeeplinkRequestWithSessionTopic(request: WcPairRequest): Boolean {
when (request.source) {
WcPairRequest.Source.QR,
WcPairRequest.Source.CLIPBOARD,
WcPairRequest.Source.ETC,
-> return true
WcPairRequest.Source.DEEPLINK -> Unit
}
val isExistSession = existSessionTopic(request.uri).getOrNull() ?: false
return !isExistSession
}
private suspend fun existSessionTopic(uri: String) = runCatching {
val sessionTopic = uri.toUri().getQueryParameter("sessionTopic") ?: return@runCatching false
val isExistSession = sessionsManager.findSessionByTopic(sessionTopic) != null
return@runCatching isExistSession
}
}

View file

@ -8,10 +8,10 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.reown.walletkit.client.Wallet
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.repository.WcSessionsManager
@ -24,8 +24,6 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import timber.log.Timber
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
@Suppress("LongParameterList")
internal class DefaultWcPairUseCase @AssistedInject constructor(
private val sessionsManager: WcSessionsManager,
@ -39,6 +37,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
private val onCallTerminalAction = Channel<TerminalAction>()
@Suppress("LongMethod")
override operator fun invoke(): Flow<WcPairState> {
val (uri: String, source: WcPairRequest.Source) = pairRequest
return flow {
@ -49,14 +48,15 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
val sdkSessionProposal = sdkDelegate.pair(uri)
.onLeft {
Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest")
analytics.send(WcAnalyticEvents.PairFailed)
emit(WcPairState.Error(it))
}
.getOrNull() ?: return@flow
// check unsupported dApps, just local constant for now, finish if unsupported
if (sdkSessionProposal.name in unsupportedDApps) {
if (UnsupportedDApps.list.any { sdkSessionProposal.url.contains(it, ignoreCase = true) }) {
Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}")
val error = WcPairState.Error(WcPairError.UnsupportedDomain)
val error = WcPairState.Error(WcPairError.UnsupportedDApp(sdkSessionProposal.name))
emit(error)
return@flow
}
@ -78,7 +78,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}
// finish flow if rejected above
if (sessionForApprove == null) {
analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession))
analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession.dAppMetaData))
sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey)
return@flow
}
@ -157,7 +157,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
analytics.send(
WcAnalyticEvents.PairRequested(
network = requestedNetworks,
verificationInfo.name,
verificationInfo,
),
)
val appMetaData = WcAppMetaData(

View file

@ -0,0 +1,16 @@
package com.tangem.data.walletconnect.pair
/**
* These dApps are not supported by WC due to various issues with signing formats
* @see <a href = "https://www.notion.so/tangem/WalletConnect-1935d34eb678805fb9aff2e046dc1dfa?source=copy_link#1a15d34eb6788073925ce9c2b9fd12a5">Documentation<a/>
*/
object UnsupportedDApps {
val list = listOf(
"dydx.trade",
"dydx.exchange",
"pro.apex.exchange",
"services.dfx.swiss",
"sandbox.game",
"app.paradex.trade",
)
}

View file

@ -5,59 +5,94 @@ import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.domain.walletconnect.model.WcPairError
import kotlinx.coroutines.async
import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
internal class WcPairSdkDelegate : WcSdkObserver {
private val onSessionProposal = Channel<Wallet.Model.SessionProposal>()
private val onSdkErrorCallback = Channel<Wallet.Model.Error>()
private val onSessionSettleResponse = Channel<Wallet.Model.SettledSessionResponse>()
suspend fun pair(url: String): Either<WcPairError, Wallet.Model.SessionProposal> = coroutineScope {
suspend fun proposalCallback() = onSessionProposal
.receiveAsFlow()
.first()
val proposalCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { proposalCallback() } }
val pairCall = async { sdkPair(url) }
val proposal = async { withTimeout(20.seconds) { proposalCallback() } }
pairCall.await().onLeft {
proposal.cancel()
proposalCallback.cancel()
return@coroutineScope it.left()
}
proposal.await().right()
proposalCallback.await()
}
private suspend fun proposalCallback() = callbackFlow {
// wait first onSessionProposal callback
launch {
val sessionProposal = onSessionProposal.receiveAsFlow().first()
trySend(sessionProposal.right())
channel.close()
}
// OR
// wait first onError callback
launch {
val error = onSdkErrorCallback.receiveAsFlow().first()
trySend(error.throwable.toPairError().left())
channel.close()
}
awaitClose()
}.first()
suspend fun approve(
sessionApprove: Wallet.Params.SessionApprove,
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> = coroutineScope {
suspend fun approveCallback() = onSessionSettleResponse
.receiveAsFlow()
.first()
val approveCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { approveCallback() } }
val approveCall = async { sdkApprove(sessionApprove) }
val approveCallback = async { withTimeout(20.seconds) { approveCallback() } }
approveCall.await()
.onLeft {
approveCallback.cancel()
return@coroutineScope it.left()
}
when (val result = approveCallback.await()) {
is Wallet.Model.SettledSessionResponse.Result -> result.right()
is Wallet.Model.SettledSessionResponse.Error ->
WcPairError.ApprovalFailed(result.errorMessage).left()
}
return@coroutineScope approveCallback.await().fold(
ifLeft = { it.left() },
ifRight = { result ->
when (result) {
is Wallet.Model.SettledSessionResponse.Result -> result.right()
is Wallet.Model.SettledSessionResponse.Error ->
ApprovalFailed(result.errorMessage).left()
}
},
)
}
private suspend fun approveCallback() = callbackFlow<Either<WcPairError, Wallet.Model.SettledSessionResponse>> {
// wait first onSessionSettleResponse callback
launch {
val settledSessionResponse = onSessionSettleResponse.receiveAsFlow().first()
trySend(settledSessionResponse.right())
channel.close()
}
// OR
// wait first onError callback
launch {
val error = onSdkErrorCallback.receiveAsFlow().first()
trySend(error.throwable.toApproveError())
channel.close()
}
awaitClose()
}.first()
fun rejectSession(proposerPublicKey: String) {
Timber.tag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey")
WalletKit.rejectSession(
params = Wallet.Params.SessionReject(
proposerPublicKey = proposerPublicKey,
@ -68,6 +103,10 @@ internal class WcPairSdkDelegate : WcSdkObserver {
)
}
override fun onError(error: Wallet.Model.Error) {
onSdkErrorCallback.trySend(error)
}
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
@ -85,8 +124,8 @@ internal class WcPairSdkDelegate : WcSdkObserver {
return suspendCancellableCoroutine { continuation ->
WalletKit.approveSession(
params = sessionApprove,
onSuccess = { continuation.resume(Unit.right()) },
onError = { continuation.resume(it.throwable.toApproveError()) },
onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) },
onError = { if (continuation.isActive) continuation.resume(it.throwable.toApproveError()) },
)
}
}
@ -95,8 +134,8 @@ internal class WcPairSdkDelegate : WcSdkObserver {
return suspendCancellableCoroutine { continuation ->
WalletKit.pair(
params = Wallet.Params.Pair(uri),
onSuccess = { continuation.resume(Unit.right()) },
onError = { continuation.resume(it.throwable.toPairError().left()) },
onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) },
onError = { if (continuation.isActive) continuation.resume(it.throwable.toPairError().left()) },
)
}
}
@ -109,7 +148,12 @@ internal class WcPairSdkDelegate : WcSdkObserver {
private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left()
companion object {
private const val CALLBACK_TIMEOUT = 30
// com.reown.android.pairing.engine.domain.PairingEngine.pair
private val pairingExpiredMessages = listOf("Pairing URI expired", "Pairing expired")
private val pairingExpiredMessages = listOf(
"Pairing URI expired",
"Pairing expired",
"No proposal or pending session authenticate request for pairing topic",
)
}
}

View file

@ -6,6 +6,7 @@ import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
@ -13,7 +14,7 @@ import kotlin.coroutines.resume
internal class DefaultWcRespondService : WcRespondService {
override suspend fun respond(request: WcSdkSessionRequest, response: String): Either<Throwable, Unit> =
override suspend fun respond(request: WcSdkSessionRequest, response: String): Either<WcRequestError, String> =
suspendCancellableCoroutine { continuation ->
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
@ -24,12 +25,21 @@ internal class DefaultWcRespondService : WcRespondService {
),
),
onSuccess = {
if (continuation.isCompleted) return@respondSessionRequest
Timber.tag(WC_TAG).i("Successful respond for request $request")
continuation.resume(Unit.right())
val result = when (val response = it.jsonRpcResponse) {
is Wallet.Model.JsonRpcResponse.JsonRpcError -> WcRequestError.WcRespondError(
code = response.code,
message = response.message,
).left()
is Wallet.Model.JsonRpcResponse.JsonRpcResult -> response.result.right()
}
continuation.resume(result)
},
onError = {
if (continuation.isCompleted) return@respondSessionRequest
Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request")
continuation.resume(it.throwable.left())
continuation.resume(WcRequestError.UnknownError(it.throwable).left())
},
)
}

View file

@ -1,9 +1,10 @@
package com.tangem.data.walletconnect.respond
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
interface WcRespondService {
suspend fun respond(request: WcSdkSessionRequest, response: String): Either<Throwable, Unit>
suspend fun respond(request: WcSdkSessionRequest, response: String): Either<WcRequestError, String>
fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String = "")
}

View file

@ -6,11 +6,13 @@ import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionDTO
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
@ -24,18 +26,19 @@ import kotlinx.coroutines.flow.*
import timber.log.Timber
import kotlin.coroutines.resume
@Suppress("LongParameterList")
internal class DefaultWcSessionsManager(
private val store: WalletConnectStore,
private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val associateNetworks: AssociateNetworksDelegate,
private val analytics: AnalyticsEventHandler,
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
private val oneTimeMigration = MutableStateFlow(true)
private val oneTimeSessionExtend = MutableStateFlow(true)
override val sessions: Flow<Map<UserWallet, List<WcSession>>>
get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore }
@ -50,10 +53,6 @@ internal class DefaultWcSessionsManager(
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
val someRemove = removeUnknownSessions(inStore, associatedSessions)
if (someRemove) return@transform // ignore emit, wait next one
if (oneTimeSessionExtend.value) {
oneTimeSessionExtend.value = false
scope.launch { extendAliveSessions(associatedSessions) }
}
emit(associatedSessions.groupBy { it.wallet })
}
.distinctUntilChanged()
@ -61,8 +60,8 @@ internal class DefaultWcSessionsManager(
override fun onWcSdkInit() {
oneTimeMigration.value = true
oneTimeSessionExtend.value = true
listenOnSessionDelete()
extendSessions()
}
override suspend fun saveSession(session: WcSession) {
@ -73,6 +72,7 @@ internal class DefaultWcSessionsManager(
val topic = session.sdkModel.topic
val sdkCall = sdkDisconnectSession(topic)
.onRight { onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = "")) }
analytics.send(WcAnalyticEvents.SessionDisconnected(session.sdkModel.appMetaData))
return sdkCall
}
@ -154,17 +154,20 @@ internal class DefaultWcSessionsManager(
return haveSomeUnknown
}
private suspend fun extendAliveSessions(sessions: List<WcSession>) = coroutineScope {
val jobs = sessions.map { launch { sdkSessionExtend(it.sdkModel.topic) } }
jobs.joinAll()
private fun extendSessions() {
scope.launch(dispatchers.io) {
val topics: List<String> = WalletKit.getListOfActiveSessions().map { it.topic }
val jobs = topics.map { topic -> launch { sdkSessionExtend(topic) } }
jobs.joinAll()
}
}
private suspend fun sdkDisconnectSession(topic: String): Either<Throwable, Unit> {
return suspendCancellableCoroutine { continuation ->
WalletKit.disconnectSession(
params = Wallet.Params.SessionDisconnect(topic),
onSuccess = { continuation.resume(Unit.right()) },
onError = { continuation.resume(it.throwable.left()) },
onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) },
onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) },
)
}
}
@ -173,8 +176,8 @@ internal class DefaultWcSessionsManager(
return suspendCancellableCoroutine { continuation ->
WalletKit.extendSession(
params = Wallet.Params.SessionExtend(topic),
onSuccess = { continuation.resume(Unit.right()) },
onError = { continuation.resume(it.throwable.left()) },
onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) },
onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) },
)
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.data.walletconnect.sign
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.models.network.Network
import com.tangem.utils.converter.Converter
import javax.inject.Inject
internal class BlockAidChainNameConverter @Inject constructor() : Converter<Network, String> {
@Suppress("CyclomaticComplexMethod")
override fun convert(value: Network): String {
return when (Blockchain.fromNetworkId(value.backendId)) {
Blockchain.Arbitrum -> "arbitrum"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalanche-fuji"
Blockchain.Binance, Blockchain.BSC -> "bsc"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum-sepolia"
Blockchain.Polygon -> "polygon"
Blockchain.Solana -> "mainnet"
Blockchain.Gnosis -> "gnosis"
Blockchain.Optimism -> "optimism"
Blockchain.ZkSyncEra -> "zksync"
Blockchain.ZkSyncEraTestnet -> "zksync-sepolia"
Blockchain.Base -> "base"
Blockchain.BaseTestnet -> "base-sepolia"
Blockchain.Blast, Blockchain.BlastTestnet -> "blast"
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain"
Blockchain.Scroll -> "scroll"
else -> value.name
}
}
}

View file

@ -1,16 +1,12 @@
package com.tangem.data.walletconnect.sign
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
object SignStateConverter {
internal fun <M> preSign(signModel: M) = WcSignState(signModel, WcSignStep.PreSign)
internal fun <M> signing(signModel: M) = WcSignState(signModel, WcSignStep.Signing)
internal fun <M> result(result: Either<Throwable, Unit>, signModel: M) =
WcSignState(signModel, WcSignStep.Result(result))
internal fun <M> WcSignState<M>.toPreSign(signModel: M = this.signModel) = copy(
signModel = signModel,
domainStep = WcSignStep.PreSign,
@ -21,8 +17,9 @@ object SignStateConverter {
signModel = signModel,
)
internal fun <M> WcSignState<M>.toResult(result: Either<Throwable, Unit>, signModel: M = this.signModel) = copy(
domainStep = WcSignStep.Result(result),
signModel = signModel,
)
internal fun <M> WcSignState<M>.toResult(result: Either<WcRequestError, String>, signModel: M = this.signModel) =
copy(
domainStep = WcSignStep.Result(result),
signModel = signModel,
)
}

View file

@ -6,6 +6,8 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import kotlinx.coroutines.Job
@ -62,18 +64,19 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
fun signFlow() = flow { onSign(state.updateAndGet { it.toSigning() }) }
.onEach { newState -> state.update { newState } }
.catch { exception ->
val errorResult = state.value.toResult(exception.left())
val errorResult = state.value
.toResult(WcRequestError.UnknownError(exception).left())
state.update { errorResult }
}
.onEach { state ->
val step = state.domainStep as? WcSignStep.Result ?: return@onEach
val event = step.result.fold(
ifLeft = {
ifLeft = { error ->
WcAnalyticEvents.SignatureRequestFailed(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
it.message.orEmpty(),
errorCode = error.code() ?: error::class.simpleName.orEmpty(),
)
},
ifRight = {

View file

@ -1,6 +1,7 @@
package com.tangem.data.walletconnect.utils
import com.domain.blockaid.models.transaction.*
import com.tangem.data.walletconnect.sign.BlockAidChainNameConverter
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
@ -16,6 +17,7 @@ import javax.inject.Inject
internal class BlockAidVerificationDelegate @Inject constructor(
private val blockAidVerifier: BlockAidVerifier,
private val blockAidChainNameConverter: BlockAidChainNameConverter,
) {
fun getSecurityStatus(
@ -47,7 +49,7 @@ internal class BlockAidVerificationDelegate @Inject constructor(
}?.let { params ->
blockAidVerifier.verifyTransaction(
TransactionData(
chain = network.name,
chain = blockAidChainNameConverter.convert(network),
accountAddress = accountAddress,
method = rawSdkRequest.request.method,
domainUrl = session.sdkModel.appMetaData.url,

View file

@ -58,8 +58,8 @@ internal class DefaultWcPairUseCaseTest {
relayData = "",
)
private val unsupportedDApp = "Apex Pro"
private val unsupportedSdkProposal get() = sdkProposal.copy(name = unsupportedDApp)
private val unsupportedDAppUrl = "dydx.trade"
private val unsupportedSdkProposal get() = sdkProposal.copy(url = unsupportedDAppUrl)
private val sessionForApprove: WcSessionApprove
get() = WcSessionApprove(
@ -196,7 +196,7 @@ internal class DefaultWcPairUseCaseTest {
@Test
fun `success pair and reject unsupported dApp`() = runTest {
coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right()
val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDomain)
val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp(unsupportedSdkProposal.name))
val useCase = useCaseFactory()
useCase.invoke().test {

View file

@ -10,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.sign.*
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
@ -32,6 +33,7 @@ internal class WcSignUseCaseDelegateTest {
object : FinalActionCollector<TestSignModel> {}
private val initSignModel = TestSignModel()
private val analytics: AnalyticsEventHandler = mockk<AnalyticsEventHandler>(relaxed = true)
private val simpleResult = "hex".right()
private val rawRequestMock = WcSdkSessionRequest(
topic = "",
chainId = "",
@ -64,14 +66,14 @@ internal class WcSignUseCaseDelegateTest {
private val initState = WcSignState(initSignModel, WcSignStep.PreSign)
private val signing = initState.toSigning()
private val result = signing.toResult(Unit.right())
private val testException = RuntimeException("test")
private val result = signing.toResult(simpleResult)
private val testException = WcRequestError.UnknownError(RuntimeException("test"))
private val successSign: suspend FlowCollector<WcSignState<TestSignModel>>.(
currentState: WcSignState<TestSignModel>,
) -> Unit = { state ->
delay(2)
emit(state.toResult(Unit.right()))
emit(state.toResult(simpleResult))
}
private val failedSign: suspend FlowCollector<WcSignState<TestSignModel>>.(
@ -161,8 +163,9 @@ internal class WcSignUseCaseDelegateTest {
@Test
fun `failed sign and catch unknown exception`() = runTest {
val exception = RuntimeException("asd")
val expectedErrorState = signing.toResult(exception.left())
val exception = RuntimeException("test")
val testException = WcRequestError.UnknownError(exception)
val expectedErrorState = signing.toResult(testException.left())
finalActionCollector = object : FinalActionCollector<TestSignModel> {
override suspend fun SignCollector<TestSignModel>.onSign(state: WcSignState<TestSignModel>) {
delay(2)