Updated on 2026-08-14
This commit is contained in:
parent
4b4555100f
commit
5ef3837bd5
9 changed files with 219 additions and 8 deletions
|
|
@ -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],
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
|
|||
params = MultiNetworkStatusFetcher.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
networks = setOf(params.network),
|
||||
extraTokens = params.extraTokens,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ object VisaUtilities {
|
|||
val visaDefaultDerivationPath
|
||||
get() = visaBlockchain.derivationPath(DerivationStyle.V3)
|
||||
val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0")
|
||||
val virtualAccountDerivationPath = DerivationPath("m/44'/60'/999998'/0/0")
|
||||
val curve = EllipticCurve.Secp256k1
|
||||
|
||||
fun signWithNonceMessage(nonce: String): String {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.networks.multi
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
|
|
@ -11,5 +12,16 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
*/
|
||||
interface MultiNetworkStatusFetcher : FlowFetcher<MultiNetworkStatusFetcher.Params> {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId, val networks: Set<Network>)
|
||||
/**
|
||||
* Params
|
||||
*
|
||||
* @property userWalletId user wallet id
|
||||
* @property networks networks whose statuses are fetched
|
||||
* @property extraTokens additional tokens to fetch balances for, beyond the wallet's added currencies
|
||||
*/
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val networks: Set<Network>,
|
||||
val extraTokens: Set<CryptoCurrency.Token> = emptySet(),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.networks.single
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
|
|
@ -15,7 +16,12 @@ interface SingleNetworkStatusFetcher : FlowFetcher<SingleNetworkStatusFetcher.Pa
|
|||
* Params
|
||||
*
|
||||
* @property userWalletId user wallet id
|
||||
* @property network network
|
||||
* @property network network whose status is fetched
|
||||
* @property extraTokens additional tokens to fetch balances for, beyond the wallet's added currencies
|
||||
*/
|
||||
data class Params(val userWalletId: UserWalletId, val network: Network)
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val network: Network,
|
||||
val extraTokens: Set<CryptoCurrency.Token> = emptySet(),
|
||||
)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ interface TangemPayCurrencyFactory {
|
|||
* @throws IllegalStateException if no wallet with [userWalletId] is currently loaded.
|
||||
*/
|
||||
fun create(userWalletId: UserWalletId): CryptoCurrency.Token
|
||||
fun createVirtualAccountToken(userWalletId: UserWalletId): CryptoCurrency.Token
|
||||
|
||||
/** Hardcoded token metadata for the Tangem Pay currency (USDC on Polygon). */
|
||||
companion object {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue