Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-22 16:14:23 +05:00
parent b28711ef43
commit ed26d23a78
9 changed files with 219 additions and 8 deletions

View file

@ -67,7 +67,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = network,
networkCurrencies = networksCurrencies[network].orEmpty().toSet(),
networkCurrencies = networksCurrencies[network].orEmpty().toSet() + params.extraTokens,
xpub = xpubByNetwork[network],
)
}

View file

@ -21,6 +21,7 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
params = MultiNetworkStatusFetcher.Params(
userWalletId = params.userWalletId,
networks = setOf(params.network),
extraTokens = params.extraTokens,
),
)
}

View file

@ -5,8 +5,9 @@ import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.requireUserWalletsSync
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import javax.inject.Inject
@ -23,9 +24,7 @@ internal class DefaultTangemPayCurrencyFactory @Inject constructor(
}
override fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
val userWallet = userWalletsListRepository.requireUserWalletsSync()
.firstOrNull { it.walletId == userWalletId }
?: error("User wallet with id $userWalletId not found")
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
val network = networkFactory.create(
blockchain = VisaUtilities.visaBlockchain,
userWallet = userWallet,
@ -40,4 +39,21 @@ internal class DefaultTangemPayCurrencyFactory @Inject constructor(
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
)
}
override fun createVirtualAccountToken(userWalletId: UserWalletId): CryptoCurrency.Token {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
val network = networkFactory.create(
blockchain = VisaUtilities.visaBlockchain,
derivationPath = Network.DerivationPath.Custom(VisaUtilities.virtualAccountDerivationPath.rawPath),
userWallet = userWallet,
)
return cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = TangemPayCurrencyFactory.TOKEN_ID,
name = TangemPayCurrencyFactory.TOKEN_NAME,
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
)
}
}

View file

@ -1,19 +1,41 @@
package com.tangem.data.virtualaccount.flow
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.VirtualAccountStatusValue
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
import com.tangem.domain.wallets.extension.hasDerivation
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
private val virtualAccountStatusesStore: VirtualAccountStatusesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val networkFactory: NetworkFactory,
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
private val userWalletsListRepository: UserWalletsListRepository,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
) : VirtualAccountStatusFetcher {
override suspend fun invoke(params: VirtualAccountStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
@ -21,6 +43,7 @@ internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
// TODO([REDACTED_TASK_KEY]): Replace with the real VA status fetch (provisioning state, balance and banking
// details) from the backend once Virtual Account status endpoints are available. Until then the
// account is surfaced as NotCreated so the entity flows through the app end-to-end.
getBalance(params.userWalletId)
virtualAccountStatusesStore.store(
userWalletId = params.userWalletId,
status = AccountStatus.Virtual(account = account, value = VirtualAccountStatusValue.NotCreated),
@ -31,4 +54,55 @@ internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
source = StatusSource.ONLY_CACHE,
)
}
private suspend fun getBalance(userWalletId: UserWalletId): Either<VirtualAccountStatusValue, BigDecimal> {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
val hasVirtualAccountDerivation = userWallet.hasDerivation(
blockchain = VisaUtilities.visaBlockchain,
derivationPath = VisaUtilities.virtualAccountDerivationPath.rawPath,
)
if (!hasVirtualAccountDerivation) {
// TODO: Doston(VA) Derive will be implemented in [REDACTED_TASK_KEY]
TangemLogger.withTag(TAG).d("Virtual account is not derived")
return VirtualAccountStatusValue.Error.NotSynced.left()
}
val network = networkFactory.create(
blockchain = VisaUtilities.visaBlockchain,
derivationPath =
Network.DerivationPath.Custom(VisaUtilities.virtualAccountDerivationPath.rawPath),
userWallet = userWallet,
)
if (network == null) {
TangemLogger.withTag(TAG).d("Can not create network for Virtual account")
return VirtualAccountStatusValue.Error.Unavailable.left()
}
val token = tangemPayCurrencyFactory.createVirtualAccountToken(userWalletId)
singleNetworkStatusFetcher(
SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = network,
extraTokens = setOf(token),
),
)
val verifiedStatus = singleNetworkStatusSupplier
.getSyncOrNull(SingleNetworkStatusProducer.Params(userWalletId, network))
?.value as? NetworkStatus.Verified
val balance = (verifiedStatus?.amounts?.get(token.id) as? NetworkStatus.Amount.Loaded)?.value
return if (balance != null) {
TangemLogger.withTag(TAG).d("VA on-chain balance = $balance")
balance.right()
} else {
TangemLogger.withTag(TAG).d("Can not get VA balance")
VirtualAccountStatusValue.Error.Unavailable.left()
}
}
private companion object {
private const val TAG = "VirtualAccountStatusFetcher"
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.data.virtualaccount.flow
import arrow.core.right
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
import com.tangem.domain.wallets.extension.hasDerivation
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
private const val USER_WALLET_EXTENSIONS = "com.tangem.domain.wallets.extension.UserWalletExtensionsKt"
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultVirtualAccountStatusFetcherTest {
private val virtualAccountStatusesStore: VirtualAccountStatusesStore = mockk(relaxed = true)
private val dispatchers = TestingCoroutineDispatcherProvider()
private val networkFactory: NetworkFactory = mockk()
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory = mockk()
private val userWalletsListRepository: UserWalletsListRepository = mockk()
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk()
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk(relaxed = true)
private val fetcher = DefaultVirtualAccountStatusFetcher(
virtualAccountStatusesStore = virtualAccountStatusesStore,
dispatchers = dispatchers,
networkFactory = networkFactory,
tangemPayCurrencyFactory = tangemPayCurrencyFactory,
userWalletsListRepository = userWalletsListRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
)
private val userWalletId = UserWalletId("011")
private val userWallet: UserWallet = mockk { every { walletId } returns userWalletId }
private val network: Network = mockk()
private val token: CryptoCurrency.Token = mockk()
@BeforeEach
fun setUp() {
mockkStatic(USER_WALLET_EXTENSIONS)
clearMocks(networkFactory, tangemPayCurrencyFactory, singleNetworkStatusFetcher, userWalletsListRepository)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet))
every {
networkFactory.create(any<Blockchain>(), any<Network.DerivationPath>(), any<UserWallet>())
} returns network
every { tangemPayCurrencyFactory.createVirtualAccountToken(userWalletId) } returns token
coEvery { singleNetworkStatusFetcher(any()) } returns Unit.right()
}
@AfterEach
fun tearDown() {
unmockkStatic(USER_WALLET_EXTENSIONS)
}
@Test
fun `GIVEN VA derivation missing WHEN invoke THEN on-chain fetch skipped`() = runTest {
// Arrange
every { userWallet.hasDerivation(any(), any()) } returns false
// Act
fetcher.invoke(VirtualAccountStatusFetcher.Params(userWalletId))
// Assert
coVerify(exactly = 0) { singleNetworkStatusFetcher(any()) }
}
@Test
fun `GIVEN VA derivation present WHEN invoke THEN on-chain fetch performed`() = runTest {
// Arrange
every { userWallet.hasDerivation(any(), any()) } returns true
// Act
fetcher.invoke(VirtualAccountStatusFetcher.Params(userWalletId))
// Assert
coVerify(exactly = 1) { singleNetworkStatusFetcher(any()) }
}
}