Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-21 18:34:38 +03:00
commit 438126dc82
665 changed files with 16174 additions and 6245 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

@ -2,7 +2,6 @@ package com.tangem.data.common.currency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -20,7 +19,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
@ -46,23 +56,23 @@ interface CardCryptoCurrencyFactory {
/**
* Create default coins for multi currency card
*
* @param scanResponse scan response
* @param userWallet user wallet
*/
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin>
fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List<CryptoCurrency.Coin>
/**
* Create primary currency for single currency card
*
* @param scanResponse scan response
* @param userWallet user wallet
*/
@Throws
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency
fun createPrimaryCurrencyForSingleCurrencyCard(userWallet: UserWallet.Cold): CryptoCurrency
/**
* Create currencies for single currency card with token (like, NODL)
*
* @param scanResponse scan response
* @param userWallet user wallet
*/
@Throws
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency>
fun createCurrenciesForSingleCurrencyCardWithToken(userWallet: UserWallet.Cold): List<CryptoCurrency>
}

View file

@ -8,7 +8,7 @@ import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
@ -46,7 +46,7 @@ class CryptoCurrencyFactory(
sdkToken: SdkToken,
blockchain: Blockchain,
extraDerivationPath: String?,
scanResponse: ScanResponse,
userWallet: UserWallet,
): CryptoCurrency.Token? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
@ -56,7 +56,7 @@ class CryptoCurrencyFactory(
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
val id = getTokenId(network, sdkToken)
@ -76,7 +76,7 @@ class CryptoCurrencyFactory(
fun createCoin(
blockchain: Blockchain,
extraDerivationPath: String?,
scanResponse: ScanResponse,
userWallet: UserWallet,
): CryptoCurrency.Coin? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
@ -86,15 +86,15 @@ class CryptoCurrencyFactory(
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
return createCoin(network)
}
fun createCoin(networkId: String, extraDerivationPath: String?, scanResponse: ScanResponse): CryptoCurrency.Coin? {
fun createCoin(networkId: String, extraDerivationPath: String?, userWallet: UserWallet): CryptoCurrency.Coin? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown
return createCoin(blockchain, extraDerivationPath, scanResponse)
return createCoin(blockchain, extraDerivationPath, userWallet)
}
fun createCoin(network: Network): CryptoCurrency.Coin {
@ -115,7 +115,7 @@ class CryptoCurrencyFactory(
token: Token,
networkId: String,
extraDerivationPath: String?,
scanResponse: ScanResponse,
userWallet: UserWallet,
): CryptoCurrency.Token? {
val sdkToken = SdkToken(
name = token.name,
@ -129,7 +129,7 @@ class CryptoCurrencyFactory(
sdkToken = sdkToken,
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
)
}
@ -141,7 +141,8 @@ class CryptoCurrencyFactory(
decimals = cryptoCurrency.decimals,
id = cryptoCurrency.id.rawCurrencyId?.value,
)
val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown
val blockchain =
Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
id = id,

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
@ -12,11 +11,9 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
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.models.requireColdWallet
/**
* Default implementation of factory for creating list of [CryptoCurrency] for selected card
@ -36,40 +33,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)
return mapOf(cardNetwork to currencies)
}
// single-currency wallet
return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
val primaryCurrency = createPrimaryCurrencyForSingleCurrencyCard(userWallet)
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
@ -78,11 +80,11 @@ internal class DefaultCardCryptoCurrencyFactory(
// single-currency wallet with token (NODL)
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
return createCurrenciesForSingleCurrencyCardWithToken(userWallet)
}
// single-currency wallet
return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
return createPrimaryCurrencyForSingleCurrencyCard(userWallet).let(::listOf)
}
override suspend fun createCurrenciesForMultiCurrencyCard(
@ -94,44 +96,54 @@ internal class DefaultCardCryptoCurrencyFactory(
return getMultiWalletCurrencies(userWallet = userWallet, networks = networks)
}
override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
require(scanResponse.cardTypesResolver.isMultiwalletAllowed()) { "It isn't multi-currency wallet" }
override fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List<CryptoCurrency.Coin> {
require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" }
val card = scanResponse.card
val blockchains = when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
if (card.isTestCard) {
blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() }
}
blockchainsInternal
}
is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
scanResponse = scanResponse,
userWallet = userWallet,
)
}
}
override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
require(scanResponse.cardTypesResolver.isSingleWallet()) { "It isn't single-currency wallet" }
override fun createPrimaryCurrencyForSingleCurrencyCard(userWallet: UserWallet.Cold): CryptoCurrency {
require(userWallet.scanResponse.cardTypesResolver.isSingleWallet()) {
"It isn't single-currency wallet"
}
return with(getSingleWalletCurrencies(scanResponse)) {
return with(getSingleWalletCurrencies(userWallet)) {
primaryToken ?: coin
}
}
override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
require(scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
override fun createCurrenciesForSingleCurrencyCardWithToken(userWallet: UserWallet.Cold): List<CryptoCurrency> {
require(userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
"It isn't single-currency wallet with token"
}
return with(getSingleWalletCurrencies(scanResponse)) {
return with(getSingleWalletCurrencies(userWallet)) {
listOfNotNull(coin, primaryToken)
}
}
@ -143,13 +155,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]
userWallet = userWallet,
)
.groupBy(CryptoCurrency::network)
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
}
private suspend fun getMultiWalletCurrenciesByRawId(
@ -163,19 +177,19 @@ internal class DefaultCardCryptoCurrencyFactory(
return responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token -> token.networkId in networkIds },
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
)
.groupBy { it.network.id.rawId }
}
private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies {
val resolver = scanResponse.cardTypesResolver
private fun getSingleWalletCurrencies(userWallet: UserWallet.Cold): SingleWalletCurrencies {
val resolver = userWallet.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
userWallet = userWallet,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
@ -185,12 +199,15 @@ internal class DefaultCardCryptoCurrencyFactory(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
userWallet = userWallet,
)
}
return SingleWalletCurrencies(coin = coin, primaryToken = primaryToken)
}
private data class SingleWalletCurrencies(val coin: CryptoCurrency, val primaryToken: CryptoCurrency?)
private data class SingleWalletCurrencies(
val coin: CryptoCurrency,
val primaryToken: CryptoCurrency?,
)
}

View file

@ -8,7 +8,7 @@ import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
import javax.inject.Inject
import com.tangem.blockchain.common.Token as SdkToken
@ -17,41 +17,41 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
private val networkFactory: NetworkFactory,
) {
fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency {
fun createCurrency(currencyId: String, response: UserTokensResponse, userWallet: UserWallet): CryptoCurrency {
return response.tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.mapNotNull { createCurrency(it, userWallet) }
.first { it.id.value == currencyId }
}
fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List<CryptoCurrency> {
return createCurrencies(tokens = response.tokens, scanResponse = scanResponse)
fun createCurrencies(response: UserTokensResponse, userWallet: UserWallet): List<CryptoCurrency> {
return createCurrencies(tokens = response.tokens, userWallet = userWallet)
}
fun createCurrencies(tokens: List<UserTokensResponse.Token>, scanResponse: ScanResponse): List<CryptoCurrency> {
fun createCurrencies(tokens: List<UserTokensResponse.Token>, userWallet: UserWallet): List<CryptoCurrency> {
return tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.mapNotNull { createCurrency(it, userWallet) }
.distinctBy(CryptoCurrency::id)
.toList()
}
fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? {
fun createCurrency(responseToken: UserTokensResponse.Token, userWallet: UserWallet): CryptoCurrency? {
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
return null
}
if (scanResponse.cardTypesResolver.isTestCard()) {
if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isTestCard()) {
blockchain = blockchain.getTestnetVersion() ?: blockchain
}
val sdkToken = createSdkToken(responseToken)
return if (sdkToken == null) {
createCoin(blockchain, responseToken, scanResponse)
createCoin(blockchain, responseToken, userWallet)
} else {
createToken(blockchain, sdkToken, responseToken.derivationPath, scanResponse)
createToken(blockchain, sdkToken, responseToken.derivationPath, userWallet)
}
}
@ -70,12 +70,12 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
private fun createCoin(
blockchain: Blockchain,
responseToken: UserTokensResponse.Token,
scanResponse: ScanResponse,
userWallet: UserWallet,
): CryptoCurrency.Coin? {
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = responseToken.derivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
return CryptoCurrency.Coin(
@ -105,12 +105,12 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
blockchain: Blockchain,
sdkToken: Token,
responseDerivationPath: String?,
scanResponse: ScanResponse,
userWallet: UserWallet,
): CryptoCurrency.Token? {
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = responseDerivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
val id = getTokenId(network, sdkToken)

View file

@ -8,10 +8,9 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
import javax.inject.Inject
@ -31,19 +30,18 @@ class NetworkFactory @Inject constructor(
*
* @param blockchain blockchain
* @param extraDerivationPath extra derivation path
* @param scanResponse scan response
* @param userWallet user wallet
*/
fun create(blockchain: Blockchain, extraDerivationPath: String?, scanResponse: ScanResponse): Network? {
fun create(blockchain: Blockchain, extraDerivationPath: String?, userWallet: UserWallet): Network? {
return create(
blockchain = blockchain,
derivationPath = createDerivationPath(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
cardDerivationStyleProvider = userWallet.derivationStyleProvider,
),
canHandleTokens = scanResponse.card.canHandleToken(
canHandleTokens = userWallet.canHandleToken(
blockchain = blockchain,
cardTypesResolver = scanResponse.cardTypesResolver,
excludedBlockchains = excludedBlockchains,
),
)
@ -54,17 +52,16 @@ class NetworkFactory @Inject constructor(
*
* @param networkId network id
* @param derivationPath derivation path
* @param scanResponse scan response
* @param userWallet user wallet
*/
fun create(networkId: Network.ID, derivationPath: Network.DerivationPath, scanResponse: ScanResponse): Network? {
fun create(networkId: Network.ID, derivationPath: Network.DerivationPath, userWallet: UserWallet): Network? {
val blockchain = networkId.toBlockchain()
return create(
blockchain = blockchain,
derivationPath = derivationPath,
canHandleTokens = scanResponse.card.canHandleToken(
canHandleTokens = userWallet.canHandleToken(
blockchain = blockchain,
cardTypesResolver = scanResponse.cardTypesResolver,
excludedBlockchains = excludedBlockchains,
),
)

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(),
@ -306,7 +308,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
val multiWallet = model.multiWallet
// Act
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val actual = factory.createDefaultCoinsForMultiCurrencyWallet(multiWallet)
// Assert
val expected = model.expected
@ -353,7 +355,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
// Act
val actual = runCatching {
factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
factory.createPrimaryCurrencyForSingleCurrencyCard(singleWallet)
}
// Assert
@ -403,7 +405,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
// Act
val actual = runCatching {
factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = userWallet.scanResponse)
factory.createCurrenciesForSingleCurrencyCardWithToken(userWallet)
}
// Assert
@ -552,7 +554,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
sdkToken = userWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)!!
}

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.configs.GenericCardConfig
@ -12,6 +13,7 @@ import com.tangem.domain.common.configs.MultiWalletCardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Nested
@ -43,7 +45,7 @@ class NetworkFactoryTest {
val actual = networkFactory.create(
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
)
// Assert
@ -59,14 +61,14 @@ class NetworkFactoryTest {
networkFactory.create(
blockchain = model.blockchain,
extraDerivationPath = model.extraDerivationPath,
scanResponse = model.scanResponse,
userWallet = model.userWallet,
)
}
is CreateTestModel.Second -> {
networkFactory.create(
networkId = model.networkId,
derivationPath = model.derivationPath,
scanResponse = model.scanResponse,
userWallet = model.userWallet,
)
}
is CreateTestModel.Third -> {
@ -89,27 +91,27 @@ class NetworkFactoryTest {
CreateTestModel.First(
blockchain = Blockchain.Unknown,
extraDerivationPath = null, // never-mind
scanResponse = createMultiWalletScanResponse(), // never-mind
userWallet = createUserWallet(),
expected = null,
),
createFirst(
extraDerivationPath = null,
scanResponse = createGenericScanResponse(), // default derivation path is null
userWallet = createUserWallet(createGenericScanResponse()), // default derivation path is null
expectedDerivationPath = Network.DerivationPath.None,
),
createFirst(
extraDerivationPath = null,
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
expectedDerivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0"), // use default
),
createFirst(
extraDerivationPath = "m/44'/60'/0'/0/0", // as default
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
expectedDerivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0"),
),
createFirst(
extraDerivationPath = "m/84'/0'/0'/0/0",
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
expectedDerivationPath = Network.DerivationPath.Custom(value = "m/84'/0'/0'/0/0"),
),
// endregion
@ -118,23 +120,23 @@ class NetworkFactoryTest {
CreateTestModel.Second(
networkId = Network.ID(value = "1", derivationPath = Network.DerivationPath.None),
derivationPath = Network.DerivationPath.None, // never-mind
scanResponse = createMultiWalletScanResponse(), // never-mind
userWallet = createUserWallet(), // never-mind
expected = null,
),
createSecond(
scanResponse = createGenericScanResponse(), // default derivation path is null
userWallet = createUserWallet(createGenericScanResponse()), // default derivation path is null
derivationPath = Network.DerivationPath.None,
),
createSecond(
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
derivationPath = Network.DerivationPath.None,
),
createSecond(
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0"),
),
createSecond(
scanResponse = createMultiWalletScanResponse(),
userWallet = createUserWallet(),
derivationPath = Network.DerivationPath.Custom(value = "m/84'/0'/0'/0/0"),
),
// endregion
@ -176,13 +178,13 @@ class NetworkFactoryTest {
private fun createFirst(
extraDerivationPath: String?,
scanResponse: ScanResponse,
userWallet: UserWallet,
expectedDerivationPath: Network.DerivationPath,
): CreateTestModel.First {
return CreateTestModel.First(
blockchain = Blockchain.Ethereum,
extraDerivationPath = extraDerivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
expected = MockCryptoCurrencyFactory().ethereum.network.copy(
id = Network.ID(
value = Blockchain.Ethereum.id,
@ -195,13 +197,13 @@ class NetworkFactoryTest {
}
private fun createSecond(
scanResponse: ScanResponse,
userWallet: UserWallet,
derivationPath: Network.DerivationPath,
): CreateTestModel.Second {
return CreateTestModel.Second(
networkId = Network.ID(value = Blockchain.Ethereum.id, derivationPath = derivationPath),
derivationPath = derivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
expected = MockCryptoCurrencyFactory().ethereum.network.copy(
id = Network.ID(
value = Blockchain.Ethereum.id,
@ -232,6 +234,10 @@ class NetworkFactoryTest {
)
}
private fun createUserWallet(scanResponse: ScanResponse = createMultiWalletScanResponse()): UserWallet.Cold {
return MockUserWalletFactory.create(scanResponse)
}
private fun createMultiWalletScanResponse(): ScanResponse {
return MockScanResponseFactory.create(
cardConfig = MultiWalletCardConfig,
@ -254,14 +260,14 @@ class NetworkFactoryTest {
data class First(
val blockchain: Blockchain,
val extraDerivationPath: String?,
val scanResponse: ScanResponse,
val userWallet: UserWallet,
override val expected: Network?,
) : CreateTestModel
data class Second(
val networkId: Network.ID,
val derivationPath: Network.DerivationPath,
val scanResponse: ScanResponse,
val userWallet: UserWallet,
override val expected: Network?,
) : CreateTestModel

View file

@ -95,7 +95,7 @@ internal class DefaultCustomTokensRepository(
networkFactory.create(
networkId = networkId,
derivationPath = derivationPath,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
),
) {
"Network [$networkId] not found while finding token"
@ -106,8 +106,8 @@ internal class DefaultCustomTokensRepository(
symbol = null,
)
val supportedTokenNetworkIds = userWallet.scanResponse.card
.supportedBlockchains(userWallet.scanResponse.cardTypesResolver, excludedBlockchains)
val supportedTokenNetworkIds = userWallet
.supportedBlockchains(excludedBlockchains = excludedBlockchains)
.filter(Blockchain::canHandleTokens)
.map(Blockchain::toNetworkId)
@ -151,7 +151,7 @@ internal class DefaultCustomTokensRepository(
networkFactory.create(
networkId = networkId,
derivationPath = derivationPath,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
),
) {
"Network [$networkId] not found while creating coin"
@ -188,7 +188,7 @@ internal class DefaultCustomTokensRepository(
networkFactory.create(
networkId = networkId,
derivationPath = derivationPath,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
),
) {
"Network [$networkId] not found while creating custom token"
@ -261,7 +261,7 @@ internal class DefaultCustomTokensRepository(
networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
userWallet = userWallet,
)
} else {
null

View file

@ -31,7 +31,6 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
@ -142,13 +141,13 @@ internal class DefaultManageTokensRepository(
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
scanResponse = userWallet?.requireColdWallet()?.scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
)
}
@ -178,7 +177,7 @@ internal class DefaultManageTokensRepository(
testnetTokensConfig
},
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
)
return BatchFetchResult.Success(
@ -190,8 +189,8 @@ internal class DefaultManageTokensRepository(
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
userTokensResponseFactory.createUserTokensResponse(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(
userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(
userWallet = userWallet,
),
isGroupedByNetwork = false,
isSortedByBalance = false,

View file

@ -15,13 +15,12 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
internal class ManagedCryptoCurrencyFactory(
@ -32,20 +31,20 @@ internal class ManagedCryptoCurrencyFactory(
fun create(
coinsResponse: CoinsResponse,
tokensResponse: UserTokensResponse?,
scanResponse: ScanResponse?,
userWallet: UserWallet?,
): List<ManagedCryptoCurrency> {
return coinsResponse.coins.mapNotNull { coin ->
createToken(coin, tokensResponse, coinsResponse.imageHost, scanResponse)
createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet)
}
}
fun createWithCustomTokens(
coinsResponse: CoinsResponse,
tokensResponse: UserTokensResponse,
scanResponse: ScanResponse,
userWallet: UserWallet,
): List<ManagedCryptoCurrency> {
val customTokens = createCustomTokens(tokensResponse, scanResponse)
val tokens = create(coinsResponse, tokensResponse, scanResponse)
val customTokens = createCustomTokens(tokensResponse, userWallet)
val tokens = create(coinsResponse, tokensResponse, userWallet)
return customTokens + tokens
}
@ -53,10 +52,10 @@ internal class ManagedCryptoCurrencyFactory(
fun createTestnetWithCustomTokens(
testnetTokensConfig: TestnetTokensConfig,
tokensResponse: UserTokensResponse?,
scanResponse: ScanResponse,
userWallet: UserWallet,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse
?.let { createCustomTokens(it, scanResponse) }
?.let { createCustomTokens(it, userWallet) }
?: emptyList()
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
ManagedCryptoCurrency.Token(
@ -69,10 +68,10 @@ internal class ManagedCryptoCurrencyFactory(
networkId = network.id,
contractAddress = network.address,
decimals = network.decimalCount,
scanResponse = scanResponse,
userWallet = userWallet,
)
} ?: emptyList(),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, scanResponse),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet),
)
}
@ -81,28 +80,28 @@ internal class ManagedCryptoCurrencyFactory(
private fun createCustomTokens(
tokensResponse: UserTokensResponse,
scanResponse: ScanResponse,
userWallet: UserWallet,
): List<ManagedCryptoCurrency> = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, scanResponse)
maybeCreateCustomToken(token, userWallet)
}
private fun maybeCreateCustomToken(
token: UserTokensResponse.Token,
scanResponse: ScanResponse,
userWallet: UserWallet,
): ManagedCryptoCurrency? {
val blockchain = Blockchain.fromNetworkId(token.networkId)
?.takeUnless { it in excludedBlockchains }
?: return null
if (!checkIsCustomToken(token, blockchain, scanResponse.derivationStyleProvider)) {
if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) {
return null
}
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
val contractAddress = token.contractAddress
@ -135,7 +134,7 @@ internal class ManagedCryptoCurrencyFactory(
coinResponse: CoinsResponse.Coin,
tokensResponse: UserTokensResponse?,
imageHost: String?,
scanResponse: ScanResponse?,
userWallet: UserWallet?,
): ManagedCryptoCurrency? {
if (coinResponse.networks.isEmpty() || !coinResponse.active) return null
@ -146,7 +145,7 @@ internal class ManagedCryptoCurrencyFactory(
networkId = network.networkId,
contractAddress = network.contractAddress,
decimals = network.decimalCount?.toInt(),
scanResponse = scanResponse,
userWallet = userWallet,
)
}
.ifEmpty { return null }
@ -157,7 +156,7 @@ internal class ManagedCryptoCurrencyFactory(
symbol = coinResponse.symbol,
iconUrl = getIconUrl(coinResponse.id, imageHost),
availableNetworks = availableNetworks,
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, scanResponse),
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet),
)
}
@ -165,7 +164,7 @@ internal class ManagedCryptoCurrencyFactory(
networkId: String,
contractAddress: String?,
decimals: Int?,
scanResponse: ScanResponse?,
userWallet: UserWallet?,
extraDerivationPath: String? = null,
): SourceNetwork? {
val blockchain = Blockchain.fromNetworkId(networkId)
@ -175,10 +174,8 @@ internal class ManagedCryptoCurrencyFactory(
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = scanResponse?.derivationStyleProvider,
canHandleTokens = scanResponse?.let {
it.card.canHandleToken(blockchain, it.cardTypesResolver, excludedBlockchains)
} ?: false, // use card specific check if available
derivationStyleProvider = userWallet?.derivationStyleProvider,
canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) ?: false,
) ?: return null
return when {
@ -207,7 +204,7 @@ internal class ManagedCryptoCurrencyFactory(
private fun findAddedInNetworks(
currencyId: String,
tokensResponse: UserTokensResponse?,
scanResponse: ScanResponse?,
userWallet: UserWallet?,
): Set<Network> {
if (tokensResponse == null) return emptySet()
@ -220,10 +217,8 @@ internal class ManagedCryptoCurrencyFactory(
networkFactory.create(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
derivationStyleProvider = scanResponse?.derivationStyleProvider,
canHandleTokens = scanResponse?.let {
it.card.canHandleToken(blockchain, it.cardTypesResolver, excludedBlockchains)
} ?: true,
derivationStyleProvider = userWallet?.derivationStyleProvider,
canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) ?: true,
)
} else {
null

View file

@ -24,7 +24,6 @@ import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.pagination.*
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -259,13 +258,13 @@ internal class DefaultMarketsTokenRepository(
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
)
} else {
val currencyNetwork = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
) ?: return null
cryptoCurrencyFactory.createToken(

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

@ -5,7 +5,6 @@ import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -48,7 +47,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor(
val network = networkFactory.create(
networkId = status.id,
derivationPath = status.id.derivationPath,
scanResponse = userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
) ?: return@mapNotNullTo null
NetworkStatus(network = network, value = status.value)

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

@ -67,7 +67,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.first().network
@ -75,7 +75,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.last().network
@ -94,12 +94,12 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
}
@ -130,7 +130,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.first().network
@ -138,7 +138,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.last().network
@ -146,7 +146,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = updatedSimpleStatuses.first().id,
derivationPath = updatedSimpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns updatedStatuses.first().network
@ -154,7 +154,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = updatedSimpleStatuses.last().id,
derivationPath = updatedSimpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns updatedStatuses.last().network
// endregion
@ -177,12 +177,12 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
@ -202,12 +202,12 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = updatedSimpleStatuses.first().id,
derivationPath = updatedSimpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
networkFactory.create(
networkId = updatedSimpleStatuses.last().id,
derivationPath = updatedSimpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
}
@ -232,7 +232,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.first().network
@ -240,7 +240,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.last().network
// endregion
@ -263,12 +263,12 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
@ -312,14 +312,14 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.first().network
every {
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns statuses.last().network
// endregion
@ -336,7 +336,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
verifyOrder(inverse = true) {
userWalletsStore.getSyncOrNull(any())
networkFactory.create(networkId = any(), derivationPath = any(), scanResponse = any())
networkFactory.create(networkId = any(), derivationPath = any(), userWallet = any())
}
// Act 2 (emit)
@ -353,12 +353,12 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
}
@ -410,12 +410,12 @@ internal class DefaultMultiNetworkStatusProducerTest {
networkFactory.create(
networkId = simpleStatuses.first().id,
derivationPath = simpleStatuses.first().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
networkFactory.create(
networkId = simpleStatuses.last().id,
derivationPath = simpleStatuses.last().id.derivationPath,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
}
@ -429,10 +429,10 @@ internal class DefaultMultiNetworkStatusProducerTest {
val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse)
val ethNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).ethereum.network.copy(
val ethNetwork = MockCryptoCurrencyFactory(userWallet = userWallet).ethereum.network.copy(
canHandleTokens = true,
)
val cardanoNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).cardano.network
val cardanoNetwork = MockCryptoCurrencyFactory(userWallet = userWallet).cardano.network
}
}

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,
@ -210,7 +208,7 @@ internal class DefaultNFTRepository @Inject constructor(
networkFactory.create(
blockchain = it,
extraDerivationPath = null,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
)
}
.filter { it.canHandleNFTs(userWalletId) }
@ -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

@ -17,7 +17,6 @@ dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.deepLinks.global)
implementation(projects.core.analytics)
/** Common modules */

View file

@ -27,7 +27,6 @@ import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.runCatching
@ -93,7 +92,7 @@ internal class DefaultHotCryptoRepository(
?: error("UserWalletId [$userWalletId] not found")
HotCryptoCurrencyConverter(
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
imageHost = it.imageHost,
excludedBlockchains = excludedBlockchains,
)

View file

@ -10,22 +10,22 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
/**
* Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency]
*
* @property scanResponse scan response
* @property userWallet scan response
* @property imageHost image host
* @param excludedBlockchains excluded blockchains
*
[REDACTED_AUTHOR]
*/
internal class HotCryptoCurrencyConverter(
private val scanResponse: ScanResponse,
private val userWallet: UserWallet,
private val imageHost: String?,
excludedBlockchains: ExcludedBlockchains,
) : Converter<HotCryptoResponse.Token, HotCryptoCurrency?> {
@ -81,7 +81,7 @@ internal class HotCryptoCurrencyConverter(
return networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
userWallet = userWallet,
)
}

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

@ -2,7 +2,6 @@ package com.tangem.data.staking
import android.util.Base64
import arrow.core.getOrElse
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
@ -15,8 +14,6 @@ import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toMigratedCoinId
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.YieldBalanceListConverter
import com.tangem.data.staking.converters.YieldConverter
import com.tangem.data.staking.converters.action.ActionStatusConverter
@ -27,16 +24,13 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionStatusCo
import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.data.staking.utils.StakingIdFactory.Companion.integrationIdMap
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.request.*
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.datasource.local.token.converter.TokenConverter
@ -68,9 +62,7 @@ import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import java.math.BigDecimal
import kotlin.time.Duration.Companion.seconds
@ -79,9 +71,7 @@ import kotlin.time.Duration.Companion.seconds
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingYieldsStore: StakingYieldsStore,
private val stakingBalanceStore: StakingBalanceStore,
private val stakingBalanceStoreV2: YieldsBalancesStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val getUserWalletUseCase: GetUserWalletUseCase,
@ -107,12 +97,8 @@ internal class DefaultStakingRepository(
private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) }
private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) }
override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) {
rawNetworkId.plus(rawCurrencyId)
}
override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? {
return integrationIdMap.getOrDefault(getIntegrationKey(cryptoCurrencyId), null)
return stakingIdFactory.createIntegrationId(currencyId = cryptoCurrencyId)
}
override suspend fun fetchEnabledYields() {
@ -398,107 +384,6 @@ internal class DefaultStakingRepository(
}
}
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) = withContext(dispatchers.io) {
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network)
if (integrationId == null || address.isNullOrBlank()) {
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
Timber.w(
"IntegrationId or address is null fetching ${cryptoCurrency.name} staking balance",
)
return@invokeOnExpire
}
val requestBody = getBalanceRequestData(address, integrationId)
safeApiCall(
call = {
val result = stakeKitApi.getSingleYieldBalance(
integrationId = requestBody.integrationId,
body = requestBody,
).bind()
stakingBalanceStore.store(
userWalletId = userWalletId,
stakingID = StakingBalanceStore.StakingID(
integrationId = requestBody.integrationId,
address = address,
),
item = YieldBalanceWrapperDTO(
balances = result,
integrationId = requestBody.integrationId,
addresses = requestBody.addresses,
),
)
},
onError = {
stakingBalanceStore.storeSingleYieldBalance(
userWalletId = userWalletId,
item = YieldBalance.Error(integrationId = requestBody.integrationId, address = address),
)
},
)
},
)
}
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
launch(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(
userWalletId = userWalletId,
stakingID = StakingBalanceStore.StakingID(integrationId = integrationId, address = address),
)
.distinctUntilChanged()
.collectLatest {
if (it != null) {
send(it)
} else {
FirebaseCrashlytics.getInstance()
.log("No yield balance available for currency ${cryptoCurrency.id.value}")
send(YieldBalance.Error(integrationId, address))
}
}
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency)
}
}.cancellable()
override suspend fun getSingleYieldBalanceSyncLegacy(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldBalance = withContext(dispatchers.io) {
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.getSyncOrNull(
userWalletId = userWalletId,
stakingID = StakingBalanceStore.StakingID(integrationId = integrationId, address = address),
)
?: YieldBalance.Error(integrationId, address)
}
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
@ -513,129 +398,6 @@ internal class DefaultStakingRepository(
?: YieldBalance.Error(integrationId = stakingId.integrationId, address = stakingId.address)
}
@Suppress("LongMethod")
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (refresh) {
stakingBalanceStore.refresh(
userWalletId = userWalletId,
stakingIds = cryptoCurrencies.mapStakingId(userWalletId),
)
}
val yieldDTOs = withTimeoutOrNull(YIELDS_WATITING_TIMEOUT) {
runCatching { stakingYieldsStore.get().firstOrNull() }.getOrNull()
}
if (yieldDTOs == null) {
Timber.i("No enabled yields for $userWalletId")
stakingBalanceStore.store(userWalletId, emptySet())
return@withContext
}
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val yields = YieldConverter.convertListIgnoreErrors(
input = yieldDTOs,
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null && yields.any { it.id == integrationId }) {
addresses to integrationId
} else {
null
}
}
.flatMap { (addresses, integrationId) ->
addresses.map { address -> address to integrationId }
}
.map { getBalanceRequestData(it.first.value, it.second) }
.ifEmpty {
stakingBalanceStore.store(userWalletId, emptySet())
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
return@invokeOnExpire
}
val yieldBalances = safeApiCall(
call = {
stakeKitApi
.getMultipleYieldBalances(availableCurrencies)
.bind()
},
onError = {
Timber.e(it, "Unable to fetch yield balances")
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
emptySet()
},
)
stakingBalanceStore.store(userWalletId, yieldBalances)
},
)
}
private suspend fun List<CryptoCurrency>.mapStakingId(
userWalletId: UserWalletId,
): List<StakingBalanceStore.StakingID> {
return this
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null) {
addresses to integrationId
} else {
null
}
}
.flatMap { (addresses, integrationId) ->
addresses.map { address ->
StakingBalanceStore.StakingID(
integrationId = integrationId,
address = address.value,
)
}
}
}
override fun getMultiYieldBalanceUpdates(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> {
return flow {
stakingBalanceStore.get(
userWalletId = userWalletId,
stakingIds = cryptoCurrencies.mapStakingId(userWalletId),
)
.map(YieldBalanceListConverter::convert)
.collect { emit(it) }
}
}
override suspend fun getMultiYieldBalanceSyncLegacy(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
stakingBalanceStore.getSyncOrNull(userWalletId, cryptoCurrencies.mapStakingId(userWalletId))
?.let(YieldBalanceListConverter::convert)
?: YieldBalanceList.Error
}
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
@ -724,7 +486,9 @@ internal class DefaultStakingRepository(
}
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval {
return when (getIntegrationKey(cryptoCurrency.id)) {
val integrationId = stakingIdFactory.createIntegrationId(currencyId = cryptoCurrency.id)
return when (integrationId) {
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(),
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId(),
-> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
@ -773,22 +537,6 @@ internal class DefaultStakingRepository(
}
}
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
return YieldBalanceRequestBody(
addresses = Address(
address = address,
additionalAddresses = null, // todo fill additional addresses metadata if needed
explorerUrl = "", // todo fill exporer url [REDACTED_JIRA]
),
args = YieldBalanceRequestBody.YieldBalanceRequestArgs(
validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA]
),
integrationId = integrationId,
)
}
private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}"
private fun getTronResource(network: Network): TronResource? {
val blockchain = Blockchain.fromNetworkId(network.backendId)

View file

@ -3,7 +3,6 @@ package com.tangem.data.staking.di
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.DefaultStakingActionRepository
import com.tangem.data.staking.DefaultStakingErrorResolver
import com.tangem.data.staking.DefaultStakingRepository
@ -17,7 +16,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitEr
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
@ -42,9 +40,7 @@ internal object StakingDataModule {
fun provideStakingRepository(
stakeKitApi: StakeKitApi,
stakingYieldsStore: StakingYieldsStore,
stakingBalanceStore: StakingBalanceStore,
yieldsBalancesStore: YieldsBalancesStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
getUserWalletUseCase: GetUserWalletUseCase,
@ -55,9 +51,7 @@ internal object StakingDataModule {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingYieldsStore = stakingYieldsStore,
stakingBalanceStore = stakingBalanceStore,
stakingBalanceStoreV2 = yieldsBalancesStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
walletManagersFacade = walletManagersFacade,
getUserWalletUseCase = getUserWalletUseCase,

View file

@ -55,7 +55,7 @@ internal class StakingIdFactory @Inject constructor(
private const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking"
// uncomment items as implementation is ready
val integrationIdMap = mapOf(
private val integrationIdMap = mapOf(
Blockchain.TON.toDefaultKey() to TON_INTEGRATION_ID,
Blockchain.Solana.toDefaultKey() to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.toDefaultKey() to COSMOS_INTEGRATION_ID,

View file

@ -13,6 +13,7 @@ internal class DefaultSwapErrorResolver(
is ApiResponseError.HttpException -> {
expressErrorConverter.convert(throwable.errorBody.orEmpty())
}
is ExpressError -> throwable
else -> ExpressError.UnknownError
}
}

View file

@ -1,19 +1,28 @@
package com.tangem.data.swap
import com.squareup.moshi.Moshi
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.swap.converter.SwapDataConverter
import com.tangem.data.swap.converter.SwapStatusConverter
import com.tangem.data.swap.converter.TokenInfoConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.swap.models.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.wallets.models.UserWallet
@ -24,19 +33,26 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class DefaultSwapRepositoryV2 @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressRepository: ExpressRepository,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val currencyStatusOperations: BaseCurrencyStatusOperations,
private val dataSignatureVerifier: DataSignatureVerifier,
@NetworkMoshi moshi: Moshi,
) : SwapRepositoryV2 {
private val swapDataConverter = SwapDataConverter()
private val tokenInfoConverter = TokenInfoConverter()
private val exchangeStatusConverter = SwapStatusConverter()
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
override suspend fun getPairs(
userWallet: UserWallet,
@ -153,6 +169,115 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
)
}
override suspend fun getSwapData(
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrencyStatus: CryptoCurrencyStatus,
fromAmount: String,
toAddress: String?,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
): SwapDataModel = withContext(coroutineDispatcher.io) {
val requestId = UUID.randomUUID().toString()
val fromCryptoCurrency = fromCryptoCurrencyStatus.currency
val toCryptoCurrency = toCryptoCurrencyStatus.currency
val refundData = when (expressProvider.type) {
ExpressProviderType.CEX,
ExpressProviderType.DEX_BRIDGE,
ExpressProviderType.DEX,
-> SwapRefundData(
refundAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value,
refundExtraId = null, // currently always null
)
else -> null
}
val response = tangemExpressApi.getExchangeData(
fromContractAddress = fromCryptoCurrency.getContractAddress(),
toContractAddress = toCryptoCurrency.getContractAddress(),
fromNetwork = fromCryptoCurrency.network.backendId,
toNetwork = toCryptoCurrency.network.backendId,
fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
toAddress = toAddress ?: toCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
fromDecimals = fromCryptoCurrency.decimals,
toDecimals = toCryptoCurrency.decimals,
fromAmount = fromAmount,
providerId = expressProvider.providerId,
rateType = rateType.name.lowercase(),
requestId = requestId,
refundAddress = refundData?.refundAddress,
refundExtraId = refundData?.refundExtraId,
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)
?: throw ExpressError.UnknownError
if (txDetails.requestId != requestId) {
throw ExpressError.InvalidRequestIdError()
}
if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) {
throw ExpressError.InvalidPayoutAddressError()
}
swapDataConverter.convert(
ExchangeDataResponseWithTxDetails(
dataResponse = response,
txDetails = txDetails,
),
)
} else {
throw ExpressError.InvalidSignatureError()
}
}
override suspend fun swapTransactionSent(
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
toAddress: String,
txId: String,
txHash: String,
txExtraId: String?,
) {
withContext(coroutineDispatcher.io) {
tangemExpressApi.exchangeSent(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
body = ExchangeSentRequestBody(
txId = txId,
fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId,
fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
payinAddress = toAddress,
payinExtraId = txExtraId,
txHash = txHash,
),
).getOrThrow()
}
}
override suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): SwapStatusModel =
withContext(coroutineDispatcher.io) {
exchangeStatusConverter.convert(
tangemExpressApi
.getExchangeStatus(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
txId = txId,
)
.getOrThrow(),
)
}
private suspend fun CoroutineScope.getPairsInternal(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
@ -200,7 +325,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
},
)
private suspend fun CoroutineScope.createPairModelOnly(
private suspend fun createPairModelOnly(
currencyFrom: CryptoCurrency?,
currencyTo: CryptoCurrency?,
userWalletId: UserWalletId,
@ -229,6 +354,15 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
}
}
private fun parseTxDetails(txDetailsJson: String): TxDetails? {
return try {
txDetailsMoshiAdapter.fromJson(txDetailsJson)
} catch (e: IOException) {
Timber.e(e, "error parsing txDetailsJson")
null
}
}
private fun CryptoCurrency.getContractAddress(): String {
return when (this) {
is CryptoCurrency.Token -> this.contractAddress

View file

@ -21,7 +21,6 @@ import com.tangem.domain.swap.models.SwapTransactionListModel
import com.tangem.domain.swap.models.SwapTransactionModel
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
@ -119,7 +118,7 @@ internal class DefaultSwapTransactionRepository(
currencyTxs?.mapNotNull {
listConverter.convertBack(
value = it,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
txStatuses = txStatuses,
)
}

View file

@ -0,0 +1,67 @@
package com.tangem.data.swap.converter
import com.tangem.datasource.api.express.models.response.ExchangeDataResponse
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.express.models.response.TxType
import com.tangem.domain.swap.models.SwapDataModel
import com.tangem.domain.swap.models.SwapDataTransactionModel
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class SwapDataConverter : Converter<ExchangeDataResponseWithTxDetails, SwapDataModel> {
override fun convert(value: ExchangeDataResponseWithTxDetails): SwapDataModel {
val data = value.dataResponse
return SwapDataModel(
toTokenAmount = requireNotNull(data.toAmount.toBigDecimalOrNull()?.movePointLeft(data.toDecimals)),
transaction = convertTransaction(value.txDetails, data),
)
}
private fun convertTransaction(
transactionDto: TxDetails,
dataResponse: ExchangeDataResponse,
): SwapDataTransactionModel {
val fromAmount = requireNotNull(
dataResponse.fromAmount.toBigDecimalOrNull()?.movePointLeft(dataResponse.fromDecimals),
)
val toAmount = requireNotNull(
dataResponse.toAmount.toBigDecimalOrNull()?.movePointLeft(dataResponse.toDecimals),
)
return if (transactionDto.txType == TxType.SWAP) {
val otherNativeFeeWei = transactionDto.otherNativeFee?.let {
if (it == "0") {
BigDecimal.ZERO
} else {
requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" }
}
}
SwapDataTransactionModel.DEX(
fromAmount = fromAmount,
toAmount = toAmount,
txValue = transactionDto.txValue,
txId = dataResponse.txId,
txTo = transactionDto.txTo,
txFrom = requireNotNull(transactionDto.txFrom),
txData = requireNotNull(transactionDto.txData),
txExtraId = transactionDto.txExtraId,
otherNativeFeeWei = otherNativeFeeWei,
gas = transactionDto.gas?.toBigIntegerOrNull() ?: error("gas is empty"),
)
} else {
SwapDataTransactionModel.CEX(
fromAmount = fromAmount,
toAmount = toAmount,
txValue = transactionDto.txValue,
txId = dataResponse.txId,
txTo = transactionDto.txTo,
externalTxId = requireNotNull(transactionDto.externalTxId),
externalTxUrl = requireNotNull(transactionDto.externalTxUrl),
txExtraIdName = transactionDto.txExtraIdName,
txExtraId = transactionDto.txExtraId,
)
}
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.data.swap.converter.transaction
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.swap.models.SwapStatusDTO
import com.tangem.data.swap.models.SwapTransactionDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.swap.models.SwapTransactionModel
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.converter.TwoWayConverter
internal class SavedSwapTransactionConverter(
@ -35,14 +35,14 @@ internal class SavedSwapTransactionConverter(
fun convertBack(
value: SwapTransactionDTO,
scanResponse: ScanResponse,
userWallet: UserWallet,
txStatuses: Map<String, SwapStatusDTO>,
): SwapTransactionModel {
val status = txStatuses[value.txId]
val refundCurrency = status?.refundTokensResponse?.let { id ->
responseCryptoCurrenciesFactory.createCurrency(
responseToken = id,
scanResponse = scanResponse,
userWallet = userWallet,
)
}
val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency)

View file

@ -6,8 +6,8 @@ import com.tangem.data.swap.models.SwapStatusDTO
import com.tangem.data.swap.models.SwapTransactionDTO
import com.tangem.data.swap.models.SwapTransactionListDTO
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.swap.models.SwapTransactionListModel
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.converter.Converter
@ -35,7 +35,7 @@ internal class SavedSwapTransactionListConverter(
fun convertBack(
value: SwapTransactionListDTO,
scanResponse: ScanResponse,
userWallet: UserWallet,
txStatuses: Map<String, SwapStatusDTO>,
): SwapTransactionListModel? {
val fromToken = value.fromTokensResponse
@ -45,16 +45,16 @@ internal class SavedSwapTransactionListConverter(
} else {
val fromCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency(
responseToken = fromToken,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
val toCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency(
responseToken = toToken,
scanResponse = scanResponse,
userWallet = userWallet,
) ?: return null
return SwapTransactionListModel(
transactions = value.transactions.map { tx ->
savedSwapTransactionConverter.convertBack(tx, scanResponse, txStatuses)
savedSwapTransactionConverter.convertBack(tx, userWallet, txStatuses)
},
userWalletId = value.userWalletId,
fromCryptoCurrencyId = value.fromCryptoCurrencyId,

View file

@ -8,6 +8,7 @@ import com.tangem.data.swap.DefaultSwapRepositoryV2
import com.tangem.data.swap.DefaultSwapTransactionRepository
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
@ -43,6 +44,8 @@ internal object SwapDataModule {
coroutineDispatcher: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
currencyStatusOperations: BaseCurrencyStatusOperations,
dataSignatureVerifier: DataSignatureVerifier,
@NetworkMoshi moshi: Moshi,
): SwapRepositoryV2 {
return DefaultSwapRepositoryV2(
tangemExpressApi = tangemExpressApi,
@ -50,6 +53,8 @@ internal object SwapDataModule {
coroutineDispatcher = coroutineDispatcher,
appPreferencesStore = appPreferencesStore,
currencyStatusOperations = currencyStatusOperations,
dataSignatureVerifier = dataSignatureVerifier,
moshi = moshi,
)
}

View file

@ -16,7 +16,6 @@ import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params
import com.tangem.domain.wallets.models.UserWallet
@ -60,7 +59,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) {
createDefaultUserTokensResponse(scanResponse = userWallet.scanResponse)
createDefaultUserTokensResponse(userWallet = userWallet)
} else {
safeApiCall(
call = {
@ -99,7 +98,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
val userWalletId = userWallet.walletId
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
?: createDefaultUserTokensResponse(scanResponse = userWallet.requireColdWallet().scanResponse)
?: createDefaultUserTokensResponse(userWallet = userWallet)
if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId")
@ -121,9 +120,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
expressServiceLoader.update(userWallet = userWallet, userTokens = tokens)
}
private fun createDefaultUserTokensResponse(scanResponse: ScanResponse): UserTokensResponse {
private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
return userTokensResponseFactory.createUserTokensResponse(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse),
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet),
isGroupedByNetwork = false,
isSortedByBalance = false,
)

View file

@ -48,7 +48,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr
responseCryptoCurrenciesFactory.createCurrencies(
response = response,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
).toSet()
}
.onEmpty { emit(fallback) }

View file

@ -239,7 +239,8 @@ internal class DefaultCurrenciesRepository(
)
val token = userTokensResponseFactory.createResponseToken(currency)
val updatedResponse = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token })
val updatedResponse =
savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token })
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
@ -284,10 +285,11 @@ internal class DefaultCurrenciesRepository(
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
userWallet.requireColdWallet()
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(
userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
)
fetchExpressAssetsByNetworkIds(
@ -309,11 +311,12 @@ internal class DefaultCurrenciesRepository(
val scanResponse = userWallet.requireColdWallet().scanResponse
val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = scanResponse)
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet)
} else {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = scanResponse).run {
listOf(this)
}
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)
.run {
listOf(this)
}
}
fetchExpressAssetsByNetworkIds(
@ -332,10 +335,11 @@ internal class DefaultCurrenciesRepository(
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
userWallet.requireColdWallet()
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
@ -375,7 +379,10 @@ internal class DefaultCurrenciesRepository(
},
)
responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse)
responseCryptoCurrenciesFactory.createCurrencies(
storedTokens,
userWallet = userWallet,
)
}
override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) =
@ -390,7 +397,10 @@ internal class DefaultCurrenciesRepository(
},
)
responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse)
responseCryptoCurrenciesFactory.createCurrencies(
storedTokens,
userWallet = userWallet,
)
}
override suspend fun getMultiCurrencyWalletCurrency(
@ -415,7 +425,7 @@ internal class DefaultCurrenciesRepository(
responseCryptoCurrenciesFactory.createCurrency(
currencyId = id,
response = response,
scanResponse = userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
)
}
@ -449,7 +459,7 @@ internal class DefaultCurrenciesRepository(
val coin = responseCryptoCurrenciesFactory.createCurrency(
responseToken = storedCoin,
scanResponse = userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
)
coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
@ -493,12 +503,15 @@ internal class DefaultCurrenciesRepository(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Boolean {
val blockchain = cryptoCurrencyStatus.currency.network.toBlockchain()
val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
val isBitcoinBlockchain =
blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
return when {
cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain -> {
val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing }
val outgoingTransactions =
cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing }
outgoingTransactions.isNotEmpty()
}
blockchain.isEvm() -> false
blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet -> false
else -> {
@ -536,6 +549,7 @@ internal class DefaultCurrenciesRepository(
balance = balance,
)
}
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
}
}
@ -563,7 +577,8 @@ internal class DefaultCurrenciesRepository(
.coins
.firstOrNull()
?: error("Token not found")
val network = foundToken.networks.firstOrNull { it.networkId == networkId } ?: error("Network not found")
val network = foundToken.networks.firstOrNull { it.networkId == networkId }
?: error("Network not found")
CryptoCurrencyFactory.Token(
symbol = foundToken.symbol,
name = foundToken.name,
@ -576,7 +591,7 @@ internal class DefaultCurrenciesRepository(
token = token,
networkId = networkId,
extraDerivationPath = null,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
) ?: error("Unable to create token")
}
@ -604,7 +619,6 @@ internal class DefaultCurrenciesRepository(
userWallet: UserWallet,
currencyRawId: CryptoCurrency.RawID,
): Flow<List<CryptoCurrency>> {
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
return when {
userWallet.isMultiCurrency -> {
getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
@ -614,23 +628,25 @@ internal class DefaultCurrenciesRepository(
responseCryptoCurrenciesFactory.createCurrencies(
response = storedTokens.copy(tokens = filterResponse),
scanResponse = userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
)
}
}
else -> {
val currencies = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId)
} else {
val currency =
getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId)
if (currency.id.rawCurrencyId == currencyRawId) {
listOf(currency)
else -> {
val currencies =
if (userWallet.requireColdWallet().scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId)
} else {
emptyList()
val currency =
getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId)
if (currency.id.rawCurrencyId == currencyRawId) {
listOf(currency)
} else {
emptyList()
}
}
}
flow {
emit(currencies)
}
@ -656,15 +672,15 @@ internal class DefaultCurrenciesRepository(
}
}
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver {
return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY]
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? {
return (userWalletsStore.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
responseCryptoCurrenciesFactory.createCurrencies(
response = storedTokens,
scanResponse = userWallet.requireColdWallet().scanResponse,
userWallet = userWallet,
)
}
}
@ -754,7 +770,10 @@ internal class DefaultCurrenciesRepository(
?: createDefaultUserTokensResponse(userWallet = userWallet)
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
Timber.w(
e,
"Requested currencies could not be found in the remote store for: $userWalletId",
)
userTokensSaver.push(userWalletId, response)
} else {
@ -766,8 +785,8 @@ internal class DefaultCurrenciesRepository(
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
userTokensResponseFactory.createUserTokensResponse(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(
userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(
userWallet = userWallet,
),
isGroupedByNetwork = false,
isSortedByBalance = false,
@ -786,9 +805,11 @@ internal class DefaultCurrenciesRepository(
!userWallet.isMultiCurrency && isMultiCurrencyWalletExpected -> {
"Multi currency wallet expected, but single currency wallet was found: $userWalletId"
}
userWallet.isMultiCurrency && !isMultiCurrencyWalletExpected -> {
"Single currency wallet expected, but multi currency wallet was found: $userWalletId"
}
else -> null
}

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) }
@ -123,7 +125,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null
every {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
} returns defaultCoins
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
@ -139,7 +141,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
coVerifyOrder {
userWalletsStore.getSyncStrict(key = params.userWalletId)
userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = mockUserWallet.scanResponse)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
@ -185,7 +187,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
@ -227,7 +229,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
@ -265,7 +267,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null
coEvery {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
} returns defaultCoins
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
@ -333,7 +335,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
coVerify(inverse = true) {
userTokensSaver.push(userWalletId = any(), response = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
@ -375,7 +377,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null
coEvery {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
} returns defaultCoins
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
@ -392,7 +394,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userWalletsStore.getSyncStrict(key = params.userWalletId)
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
@ -445,7 +447,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}

View file

@ -72,7 +72,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
}
verify(inverse = true) {
responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any())
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any())
}
}
@ -114,14 +114,14 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns cryptoCurrencies.toList()
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = updatedUserTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns updatedCryptoCurrencies.toList()
@ -143,7 +143,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
userTokensResponseStore.get(params.userWalletId)
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
@ -161,7 +161,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
verifyOrder {
responseCryptoCurrenciesFactory.createCurrencies(
response = updatedUserTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
}
@ -185,7 +185,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns cryptoCurrencies.toList()
@ -207,7 +207,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
userTokensResponseStore.get(params.userWalletId)
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
@ -251,7 +251,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
} returns cryptoCurrencies.toList()
@ -282,7 +282,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
verifyOrder {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
scanResponse = userWallet.scanResponse,
userWallet = userWallet,
)
}
}
@ -307,7 +307,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
}
verify(inverse = true) {
responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any())
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any())
}
}
@ -335,7 +335,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
verify(inverse = true) {
userTokensResponseStore.get(any())
responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any())
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any())
}
}

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

@ -166,7 +166,9 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
val rsaPublicKey = visaLibLoader.getOrCreateConfig().rsaPublicKey
return when (env) {
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.STAGE,
ApiEnvironment.MOCK,
-> rsaPublicKey.dev
ApiEnvironment.PROD -> rsaPublicKey.prod
}

View file

@ -51,6 +51,7 @@ dependencies {
/* Other */
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.jodatime)
implementation(projects.domain.blockaid)
implementation(projects.domain.blockaid.models)
implementation(projects.libs.crypto)

View file

@ -16,9 +16,12 @@ import com.tangem.data.walletconnect.respond.DefaultWcRespondService
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
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 +73,9 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun defaultWcPairUseCase(): WcPairService = DefaultWcPairService()
fun defaultWcPairUseCase(sessionsManager: DefaultWcSessionsManager): WcPairService = DefaultWcPairService(
sessionsManager,
)
@Provides
@Singleton
@ -87,7 +92,8 @@ internal object WalletConnectDataModule {
dispatchers: CoroutineDispatcherProvider,
legacyStore: WalletConnectSessionsRepository,
getWallets: GetWalletsUseCase,
associateNetworks: AssociateNetworksDelegate,
wcNetworksConverter: WcNetworksConverter,
analytics: AnalyticsEventHandler,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcSessionsManager(
@ -95,7 +101,8 @@ internal object WalletConnectDataModule {
dispatchers = dispatchers,
legacyStore = legacyStore,
getWallets = getWallets,
associateNetworks = associateNetworks,
wcNetworksConverter = wcNetworksConverter,
analytics = analytics,
scope = scope,
)
}
@ -127,11 +134,11 @@ internal object WalletConnectDataModule {
@SdkMoshi moshi: Moshi,
sessionsManager: WcSessionsManager,
factories: WcEthNetwork.Factories,
namespaceConverter: WcEthNetwork.NamespaceConverter,
walletManagersFacade: WalletManagersFacade,
wcNetworksConverter: WcNetworksConverter,
): WcEthNetwork = WcEthNetwork(
moshi = moshi,
namespaceConverter = namespaceConverter,
networksConverter = wcNetworksConverter,
sessionsManager = sessionsManager,
factories = factories,
walletManagersFacade = walletManagersFacade,
@ -141,7 +148,7 @@ internal object WalletConnectDataModule {
@Singleton
fun wcSolanaNetwork(
@SdkMoshi moshi: Moshi,
namespaceConverter: WcSolanaNetwork.NamespaceConverter,
wcNetworksConverter: WcNetworksConverter,
sessionsManager: WcSessionsManager,
factories: WcSolanaNetwork.Factories,
walletManagersFacade: WalletManagersFacade,
@ -149,7 +156,7 @@ internal object WalletConnectDataModule {
moshi = moshi,
sessionsManager = sessionsManager,
factories = factories,
namespaceConverter = namespaceConverter,
networksConverter = wcNetworksConverter,
walletManagersFacade = walletManagersFacade,
)
@ -158,9 +165,27 @@ internal object WalletConnectDataModule {
fun caipNamespaceDelegate(
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
walletManagersFacade: WalletManagersFacade,
wcNetworksConverter: WcNetworksConverter,
): CaipNamespaceDelegate = CaipNamespaceDelegate(
namespaceConverters = namespaceConverters,
walletManagersFacade = walletManagersFacade,
wcNetworksConverter = wcNetworksConverter,
)
@Provides
@Singleton
fun wcNetworksConverter(
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): WcNetworksConverter = WcNetworksConverter(
namespaceConverters = namespaceConverters,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@ -169,10 +194,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

@ -1,5 +1,9 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
@ -10,6 +14,7 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -23,7 +28,7 @@ internal class WcEthNetwork(
private val moshi: Moshi,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
private val namespaceConverter: NamespaceConverter,
private val networksConverter: WcNetworksConverter,
private val walletManagersFacade: WalletManagersFacade,
) : WcRequestToUseCaseConverter {
@ -34,23 +39,43 @@ internal class WcEthNetwork(
}
@Suppress("CyclomaticComplexMethod")
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val name = toWcMethodName(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val method: WcEthMethod = name.toMethod(request, session.wallet) ?: return null
val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val walletManagerAddress = walletManagersFacade.getDefaultAddress(session.wallet.walletId, network).orEmpty()
override suspend fun toUseCase(
request: WcSdkSessionRequest,
): Either<WcRequestError.HandleMethodError, WcMethodUseCase> {
fun error(message: String) = WcRequestError.HandleMethodError(message).left()
val name = toWcMethodName(request) ?: return error("Unknown method name")
val session = sessionsManager.findSessionByTopic(request.topic)
?: return error("Failed to find session for topic ${request.topic}")
val wallet = session.wallet
val chainId = request.chainId.orEmpty()
val method: WcEthMethod = name.toMethod(request, wallet)
.getOrElse { return error(it.message.orEmpty()) }
?: return error("Failed to parse $name")
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
val accountAddress = when (method) {
is WcEthMethod.MessageSign -> method.account
is WcEthMethod.SendTransaction -> method.transaction.from
is WcEthMethod.SignTransaction -> method.transaction.from
is WcEthMethod.SignTypedData -> method.account
is WcEthMethod.AddEthereumChain -> walletManagerAddress
is WcEthMethod.AddEthereumChain ->
anyExistNetwork()
?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() }
.orEmpty()
}
val walletNetwork = when (method) {
is WcEthMethod.SignTypedData,
is WcEthMethod.MessageSign,
is WcEthMethod.SendTransaction,
is WcEthMethod.SignTransaction,
-> networksConverter.findWalletNetworkForRequest(request, session, accountAddress)
is WcEthMethod.AddEthereumChain -> anyExistNetwork()
} ?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val context = WcMethodUseCaseContext(
session = session,
rawSdkRequest = request,
network = network,
network = walletNetwork,
accountAddress = accountAddress,
)
return when (method) {
@ -59,10 +84,13 @@ internal class WcEthNetwork(
is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method)
is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method)
}
}.right()
}
private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest, wallet: UserWallet): WcEthMethod? {
private suspend fun WcEthMethodName.toMethod(
request: WcSdkSessionRequest,
wallet: UserWallet,
): Either<Throwable, WcEthMethod?> {
val rawParams = request.request.params
return when (this) {
WcEthMethodName.EthSign,
@ -74,39 +102,48 @@ internal class WcEthNetwork(
WcEthMethodName.SignTransaction,
WcEthMethodName.SendTransaction,
-> moshi.fromJson<List<WcEthTransactionParams>>(rawParams)
.getOrElse { return it.left() }
?.firstOrNull()
?.let {
if (this == WcEthMethodName.SignTransaction) {
WcEthMethod.SignTransaction(transaction = it)
WcEthMethod.SignTransaction(transaction = it).right()
} else {
WcEthMethod.SendTransaction(transaction = it)
WcEthMethod.SendTransaction(transaction = it).right()
}
}
?: return null.right()
WcEthMethodName.AddEthereumChain -> moshi.fromJson<List<WcEthAddChain>>(rawParams)
.getOrElse { return it.left() }
?.firstOrNull()
?.let {
val newNetwork = namespaceConverter
.toNetwork(it.chainId, wallet) ?: return null
WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork)
val newNetwork = networksConverter
.mainOrAnyWalletNetworkForRequest(it.chainId, wallet)
?: return null.right()
WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork).right()
}
?: null.right()
}
}
private fun WcEthMethodName.parseMessageSign(rawParams: String): WcEthMethod.MessageSign? {
val list = moshi.fromJson<List<String>>(rawParams) ?: return null
private fun WcEthMethodName.parseMessageSign(rawParams: String): Either<Throwable, WcEthMethod.MessageSign?> {
val list = moshi.fromJson<List<String>>(rawParams)
.getOrElse { return it.left() }
?: return null.right()
val accountIndex = if (this == WcEthMethodName.EthSign) 0 else 1
val messageIndex = if (this == WcEthMethodName.EthSign) 1 else 0
val account = list.getOrNull(accountIndex) ?: return null
val message = list.getOrNull(messageIndex) ?: return null
val account = list.getOrNull(accountIndex) ?: return null.right()
val message = list.getOrNull(messageIndex) ?: return null.right()
val humanMsg = LegacySdkHelper.hexToAscii(message).orEmpty()
return WcEthMethod.MessageSign(account = account, rawMessage = message, humanMsg = humanMsg)
return WcEthMethod.MessageSign(account = account, rawMessage = message, humanMsg = humanMsg).right()
}
private fun parseTypeData(params: String): WcEthMethod.SignTypedData? {
private fun parseTypeData(params: String): Either<Throwable, WcEthMethod.SignTypedData?> {
val account = params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2))
val data = params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1)
val parsedParams = moshi.fromJson<WcEthSignTypedDataParams>(data) ?: return null
return WcEthMethod.SignTypedData(params = parsedParams, account = account, dataForSign = data)
val parsedParams = moshi.fromJson<WcEthSignTypedDataParams>(data)
.getOrElse { return it.left() }
?: return null.right()
return WcEthMethod.SignTypedData(params = parsedParams, account = account, dataForSign = data).right()
}
internal class NamespaceConverter(

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

@ -16,4 +16,7 @@ internal data class WcSolanaSignMessageRequest(
internal data class WcSolanaSignTransactionRequest(
@Json(name = "transaction")
val transaction: String,
@Json(name = "feePayer")
val feePayer: String?,
)

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

@ -1,5 +1,9 @@
package com.tangem.data.walletconnect.network.solana
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.decodeBase58
@ -12,8 +16,9 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -21,13 +26,12 @@ import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import jakarta.inject.Inject
import timber.log.Timber
internal class WcSolanaNetwork(
private val moshi: Moshi,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
private val namespaceConverter: NamespaceConverter,
private val networksConverter: WcNetworksConverter,
private val walletManagersFacade: WalletManagersFacade,
) : WcRequestToUseCaseConverter {
@ -37,32 +41,45 @@ internal class WcSolanaNetwork(
return name
}
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val name = toWcMethodName(request) ?: return null
val method: WcSolanaMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val accountAddress = getAccountAddress(session, network)
@Suppress("CyclomaticComplexMethod")
override suspend fun toUseCase(
request: WcSdkSessionRequest,
): Either<WcRequestError.HandleMethodError, WcMethodUseCase> {
fun error(message: String) = WcRequestError.HandleMethodError(message).left()
val name = toWcMethodName(request) ?: return error("Unknown method name")
val method: WcSolanaMethod = name.toMethod(request)
.getOrElse { return error(it.message.orEmpty()) }
?: return error("Failed to parse $name")
val session = sessionsManager.findSessionByTopic(request.topic)
?: return error("Failed to find session for topic ${request.topic}")
val wallet = session.wallet
val chainId = request.chainId.orEmpty()
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
suspend fun anyAddress() = anyExistNetwork()
?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() }
.orEmpty()
val accountAddress = when (method) {
is WcSolanaMethod.SignAllTransaction -> anyAddress()
is WcSolanaMethod.SignMessage -> anyAddress()
is WcSolanaMethod.SignTransaction -> method.address ?: anyAddress()
}
val walletNetwork = networksConverter
.findWalletNetworkForRequest(request, session, accountAddress)
?: anyExistNetwork()
?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val context = WcMethodUseCaseContext(
session = session,
rawSdkRequest = request,
network = network,
accountAddress = accountAddress.orEmpty(),
network = walletNetwork,
accountAddress = accountAddress,
)
return when (method) {
is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method)
is WcSolanaMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcSolanaMethod.SignAllTransaction -> factories.signAllTransaction.create(context, method)
}
}
private suspend fun getAccountAddress(session: WcSession, network: Network): String? {
return try {
walletManagersFacade.getDefaultAddress(session.wallet.walletId, network)
} catch (exception: Exception) {
Timber.e(exception)
null
}
}.right()
}
internal class NamespaceConverter @Inject constructor(
@ -95,23 +112,26 @@ internal class WcSolanaNetwork(
}
}
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? {
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): Either<Throwable, WcSolanaMethod?> {
val rawParams = request.request.params
return when (this) {
WcSolanaMethodName.SignMessage -> moshi.fromJson<WcSolanaSignMessageRequest>(rawParams)?.let { request ->
val humanMsg = request.message.decodeBase58()?.toHexString().orEmpty()
WcSolanaMethod.SignMessage(
pubKey = request.publicKey,
rawMessage = request.message,
humanMsg = humanMsg,
)
}
WcSolanaMethodName.SignMessage -> moshi.fromJson<WcSolanaSignMessageRequest>(rawParams)
.getOrElse { return it.left() }
?.let { request ->
val humanMsg = request.message.decodeBase58()?.toHexString().orEmpty()
WcSolanaMethod.SignMessage(
pubKey = request.publicKey,
rawMessage = request.message,
humanMsg = humanMsg,
)
}
WcSolanaMethodName.SignTransaction -> moshi.fromJson<WcSolanaSignTransactionRequest>(rawParams)
?.let { request -> WcSolanaMethod.SignTransaction(request.transaction) }
WcSolanaMethodName.SendAllTransaction -> moshi.fromJson<List<String>>(rawParams)?.let { list ->
WcSolanaMethod.SignAllTransaction(list)
}
}
.getOrElse { return it.left() }
?.let { request -> WcSolanaMethod.SignTransaction(request.transaction, request.feePayer) }
WcSolanaMethodName.SendAllTransaction -> moshi.fromJson<List<String>>(rawParams)
.getOrElse { return it.left() }
?.let { list -> WcSolanaMethod.SignAllTransaction(list) }
}.right()
}
internal class Factories @Inject constructor(

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

@ -5,10 +5,14 @@ import com.reown.walletkit.client.Wallet.Model.Namespace
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,36 +20,34 @@ 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 namespacesSet = namespaces.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
return namespacesSet.mapNotNullTo(mutableSetOf()) { chainId ->
val wcNetwork = namespaceConverters
.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@mapNotNullTo null
walletNetworks.find { network -> wcNetwork.id == network.id }
}
}
@Throws(WcPairError.UnsupportedBlockchains::class)
suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map<UserWallet, ProposalNetwork> {
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>()
@ -57,7 +59,8 @@ internal class AssociateNetworksDelegate(
unknownRequired.add(missingNetworkName(chainId))
return@forEach
}
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
val walletNetwork = walletNetworks.find { network -> wcNetwork.rawId == network.rawId }
if (walletNetwork == null) {
missingRequired.add(wcNetwork)
} else {
@ -66,15 +69,23 @@ internal class AssociateNetworksDelegate(
}
optionalNamespaces.forEach { chainId ->
val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
?: return@forEach
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
if (wcNetwork == null) {
unknownOptional.add(missingNetworkName(chainId))
return@forEach
}
val walletNetwork = walletNetworks.find { network -> wcNetwork.rawId == network.rawId }
if (walletNetwork != null) {
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 +95,20 @@ 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)
// flatten all derivation
.distinctBy { it.rawId }
}
private fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()

View file

@ -1,48 +1,47 @@
package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
internal class CaipNamespaceDelegate(
private val namespaceConverters: Set<WcNamespaceConverter>,
private val walletManagersFacade: WalletManagersFacade,
private val wcNetworksConverter: WcNetworksConverter,
) {
suspend fun associate(
sessionProposal: Wallet.Model.SessionProposal,
userWallet: UserWallet,
networks: List<Network>,
sessionForApprove: WcSessionApprove,
): Map<String, Wallet.Model.Namespace.Session> {
val userWallet = sessionForApprove.wallet
val result = mutableMapOf<String, Session>()
networks.map { network ->
val address = walletManagersFacade.getDefaultAddress(userWallet.walletId, network)
val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) }
requireNotNull(chainId)
requireNotNull(address)
CAIP10(chainId = chainId, accountAddress = address)
}.forEach { account ->
val namespaceKey = account.chainId.namespace
val session = result.getOrPut(namespaceKey) { Session() }
val requiredNamespaces = sessionProposal.requiredNamespaces
val optionalNamespaces = sessionProposal.optionalNamespaces
val methods = buildSet {
requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) }
optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) }
wcNetworksConverter.convertNetworksForApprove(sessionForApprove)
.mapNotNull { createCAIP10(userWallet.walletId, it) }
.forEach { account ->
val namespaceKey = account.chainId.namespace
val session = result.getOrPut(namespaceKey) { Session() }
val requiredNamespaces = sessionProposal.requiredNamespaces
val optionalNamespaces = sessionProposal.optionalNamespaces
val methods = buildSet {
requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) }
optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) }
}
val events = buildSet {
requiredNamespaces[namespaceKey]?.events?.let { addAll(it) }
optionalNamespaces[namespaceKey]?.events?.let { addAll(it) }
}
session.chains.add(account.chainId.raw)
session.accounts.add(account.raw)
session.methods.addAll(methods)
session.events.addAll(events)
}
val events = buildSet {
requiredNamespaces[namespaceKey]?.events?.let { addAll(it) }
optionalNamespaces[namespaceKey]?.events?.let { addAll(it) }
}
session.chains.add(account.chainId.raw)
session.accounts.add(account.raw)
session.methods.addAll(methods)
session.events.addAll(events)
}
return result.mapValues { (_, session) ->
Wallet.Model.Namespace.Session(
chains = session.chains.toList(),
@ -53,6 +52,13 @@ internal class CaipNamespaceDelegate(
}
}
private suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? {
val address = walletManagersFacade.getDefaultAddress(userWalletId, network)
val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) }
if (chainId == null || address == null) return null
return CAIP10(chainId = chainId, accountAddress = address)
}
private data class Session(
val chains: MutableSet<String> = mutableSetOf(),
val accounts: MutableSet<String> = mutableSetOf(),

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
@ -22,10 +22,9 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import org.joda.time.DateTime
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 +38,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 +49,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 +79,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
}
@ -94,6 +95,8 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
sdkModel = WcSdkSessionConverter.convert(settledSession.session),
securityStatus = proposalState.dAppSession.securityStatus,
networks = sessionForApprove.network.toSet(),
connectingTime = DateTime.now().millis,
showWalletInfo = proposalState.dAppSession.proposalNetwork.keys.size > 1,
)
sessionsManager.saveSession(newSession)
analytics.send(
@ -134,8 +137,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> {
val namespaces = caipNamespaceDelegate.associate(
sdkSessionProposal,
sessionForApprove.wallet,
sessionForApprove.network,
sessionForApprove,
)
val sessionApprove = Wallet.Params.SessionApprove(
proposerPublicKey = sdkSessionProposal.proposerPublicKey,
@ -157,7 +159,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

@ -3,10 +3,13 @@ package com.tangem.data.walletconnect.request
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
import timber.log.Timber
import javax.inject.Inject
internal class DefaultWcRequestUseCaseFactory @Inject constructor(
@ -17,7 +20,17 @@ internal class DefaultWcRequestUseCaseFactory @Inject constructor(
override suspend fun <T : WcMethodUseCase> createUseCase(
request: WcSdkSessionRequest,
): Either<WcMethod.Unsupported, T> {
val useCase = requestConverters.firstNotNullOfOrNull { converter -> converter.toUseCase(request) }
return (useCase as? T)?.let { useCase.right() } ?: WcMethod.Unsupported(request).left()
val useCase = requestConverters
.find { it.toWcMethodName(request) != null }
?.toUseCase(request)
?: WcRequestError.HandleMethodError("Failed to create WcUseCase").left()
return useCase.fold(
ifLeft = {
Timber.tag(WC_TAG).e("$it")
WcMethod.Unsupported(request).left()
},
ifRight = { (it as? T)?.right() ?: WcMethod.Unsupported(request).left() },
)
}
}

View file

@ -1,18 +1,27 @@
package com.tangem.data.walletconnect.request
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.domain.walletconnect.model.WcMethodName
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
interface WcRequestToUseCaseConverter {
fun toWcMethodName(request: WcSdkSessionRequest): WcMethodName?
suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase?
suspend fun toUseCase(request: WcSdkSessionRequest): Either<WcRequestError.HandleMethodError, WcMethodUseCase>
companion object {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> Moshi.fromJson(params: String): T? =
runCatching { this.adapter<T>().fromJson(params) }.getOrNull()
inline fun <reified T> Moshi.fromJson(params: String): Either<Throwable, T?> {
return runCatching { this.adapter<T>().fromJson(params) }
.fold(
onSuccess = { it.right() },
onFailure = { it.left() },
)
}
}
}

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,26 @@ internal class DefaultWcRespondService : WcRespondService {
),
),
onSuccess = {
Timber.tag(WC_TAG).i("Successful respond for request $request")
continuation.resume(Unit.right())
if (continuation.isCompleted) return@respondSessionRequest
val result = when (val response = it.jsonRpcResponse) {
is Wallet.Model.JsonRpcResponse.JsonRpcError -> {
Timber.tag(WC_TAG).e("Failed respond $response for request $request")
WcRequestError.WcRespondError(
code = response.code,
message = response.message,
).left()
}
is Wallet.Model.JsonRpcResponse.JsonRpcResult -> {
Timber.tag(WC_TAG).i("Successful respond $response for request $request")
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,10 @@ 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.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.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.utils.*
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
@ -21,21 +20,23 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import org.joda.time.DateTime
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 wcNetworksConverter: WcNetworksConverter,
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 +51,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,35 +58,33 @@ internal class DefaultWcSessionsManager(
override fun onWcSdkInit() {
oneTimeMigration.value = true
oneTimeSessionExtend.value = true
listenOnSessionDelete()
extendSessions()
}
override suspend fun saveSession(session: WcSession) {
store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId, session.securityStatus))
store.saveSession(
WcSessionDTO(
topic = session.sdkModel.topic,
walletId = session.wallet.walletId,
securityStatus = session.securityStatus,
connectingTime = session.connectingTime ?: DateTime.now().millis,
),
)
}
override suspend fun removeSession(session: WcSession): Either<Throwable, Unit> {
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
}
override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) {
val storedSession = sessions.firstOrNull()
sessions.firstOrNull()
?.values?.flatten()
?.firstOrNull { it.sdkModel.topic == topic }
?: return@withContext null
val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null
val wallet = storedSession.wallet
val networks = associateNetworks.associate(wallet, sdkSession.namespaces)
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = storedSession.securityStatus,
networks = networks,
)
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
@ -128,15 +123,17 @@ internal class DefaultWcSessionsManager(
inStore: Set<WcSessionDTO>,
wallets: List<UserWallet>,
): List<WcSession> {
val wcSessions = inStore.mapNotNull { session ->
val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null
val networks = associateNetworks.associate(wallet, sdkSession.namespaces)
val wcSessions = inStore.mapNotNull { storeSession ->
val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null
val networks = wcNetworksConverter.findWalletNetworks(wallet, sdkSession)
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = session.securityStatus,
securityStatus = storeSession.securityStatus,
networks = networks,
connectingTime = storeSession.connectingTime,
showWalletInfo = wallets.size > 1,
)
}
return wcSessions
@ -154,17 +151,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 +173,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

@ -3,11 +3,10 @@ package com.tangem.data.walletconnect.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.domain.models.network.Network
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
internal interface WcNamespaceConverter {
@ -27,7 +26,7 @@ internal interface WcNamespaceConverter {
return NetworkFactory(excludedBlockchains).create(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = wallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = wallet,
)
}
}

View file

@ -0,0 +1,102 @@
package com.tangem.data.walletconnect.utils
import com.reown.walletkit.client.Wallet
import com.tangem.data.common.currency.isCustomCoin
import com.tangem.data.walletconnect.model.CAIP10
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.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import javax.inject.Inject
internal class WcNetworksConverter @Inject constructor(
private val namespaceConverters: Set<WcNamespaceConverter>,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend fun findWalletNetworkForRequest(
request: WcSdkSessionRequest,
session: WcSession,
requestAddress: String,
): Network? {
val wallet = session.wallet
val allCoinNetwork = filterWalletNetworkForRequest(request.chainId.orEmpty(), session.wallet)
val requestNetwork = allCoinNetwork.find { network ->
val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network)
requestAddress.lowercase() == address?.lowercase()
}
return requestNetwork
}
/**
* return network with not custom derivationPath or first custom or any
*/
suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): Network? {
val networks = filterWalletNetworkForRequest(rawChainId, wallet)
return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull()
}
/**
* return all exist derivation networks
*/
suspend fun filterWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): List<Network> {
val walletNetworks = getWalletNetworks(wallet.walletId)
val blockchain = namespaceConverters
.firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf()
val allCoinNetwork = walletNetworks.filter { it.rawId == blockchain.id }
return allCoinNetwork
}
suspend fun findWalletNetworks(wallet: UserWallet, sdkSession: Wallet.Model.Session): Set<Network> {
val walletNetworks = getWalletNetworks(wallet.walletId)
val existNetworks = sdkSession.namespaces.values
.map { it.accounts }.flatten().toSet()
.mapNotNull { CAIP10.fromRaw(it) }
.mapNotNullTo(mutableSetOf()) { caip10 ->
val blockchain = namespaceConverters
.firstNotNullOfOrNull { it.toBlockchain(caip10.chainId) }
?: return@mapNotNullTo null
walletNetworks
// find all derivation
.filter { it.rawId == blockchain.id }
// find equal address
.firstOrNull {
val walletAddress = walletManagersFacade.getDefaultAddress(wallet.walletId, it)
walletAddress?.lowercase() == caip10.accountAddress.lowercase()
}
}
return existNetworks
}
suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List<Network> {
val walletNetworks = getWalletNetworks(sessionForApprove.wallet.walletId)
return sessionForApprove.network
.map { network -> walletNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } }
.flatten()
}
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(CryptoCurrency.Coin::network)
}
}

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(
@ -95,6 +95,8 @@ internal class DefaultWcPairUseCaseTest {
sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = CheckDAppResult.SAFE,
networks = setOf(),
connectingTime = null,
showWalletInfo = false,
)
private fun useCaseFactory() = DefaultWcPairUseCase(
@ -113,8 +115,7 @@ internal class DefaultWcPairUseCaseTest {
coEvery {
caipNamespaceDelegate.associate(
sessionProposal = sdkProposal,
userWallet = sessionForApprove.wallet,
networks = sessionForApprove.network,
sessionForApprove = sessionForApprove,
)
} returns mapOf()
}
@ -144,7 +145,7 @@ internal class DefaultWcPairUseCaseTest {
coEvery { sdkDelegate.pair(url) } returns sdkProposal.right()
coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right()
coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit
coEvery { sessionsManager.saveSession(any()) } returns Unit
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
val useCase = useCaseFactory()
@ -162,9 +163,12 @@ internal class DefaultWcPairUseCaseTest {
assertEquals(approveLoading, awaitItem())
coVerifyOrder {
sdkDelegate.approve(sdkApprove)
sessionsManager.saveSession(sessionForSave)
sessionsManager.saveSession(any())
}
assertEquals(result, awaitItem())
val actual: WcPairState = awaitItem()
assert(actual is WcPairState.Approving.Result)
actual as WcPairState.Approving.Result
assertEquals(result.session, actual.session)
awaitComplete()
}
}
@ -196,7 +200,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 = "",
@ -49,6 +51,7 @@ internal class WcSignUseCaseDelegateTest {
wallet = MockUserWalletFactory.create(),
networks = setOf(),
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
connectingTime = 0L,
sdkModel = WcSdkSession(
topic = "",
appMetaData = WcAppMetaData(
@ -59,19 +62,20 @@ internal class WcSignUseCaseDelegateTest {
redirect = "",
),
),
showWalletInfo = false,
),
)
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 +165,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)