Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-04 10:44:21 +03:00
commit 928fde1fc8
1397 changed files with 55981 additions and 16540 deletions

View file

@ -1,13 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -&gt; raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -&gt; Unit.right() } return@either }</ID>
<ID>MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending &amp;&amp; action.amount &lt; it.amount &amp;&amp; it.type == BalanceType.STAKED &amp;&amp; it.validatorAddress == action.validatorAddress }</ID>
<ID>NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId)</ID>
<ID>UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier</ID>
<ID>UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf()</ID>
<ID>UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: ""</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -23,9 +23,9 @@ class FetchStakingYieldBalanceUseCase(
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
)
.getOrElse {
when (it) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it"))
.getOrElse { error ->
when (error) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$error"))
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
}

View file

@ -4,10 +4,11 @@ import arrow.core.Either
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.network.Network
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
class GetConstructedStakingTransactionUseCase(
private val stakeKitRepository: StakeKitRepository,
@ -15,12 +16,17 @@ class GetConstructedStakingTransactionUseCase(
) {
suspend operator fun invoke(
networkId: String,
networkId: Network.RawID,
fee: Fee,
amount: Amount,
transactionId: String,
): Either<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
stakeKitRepository.constructTransaction(networkId, fee, amount, transactionId)
stakeKitRepository.constructTransaction(
networkId = networkId,
fee = fee,
amount = amount,
transactionId = transactionId,
)
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -100,7 +100,7 @@ class InvalidatePendingTransactionsUseCase(
type = BalanceType.STAKED,
amount = action.amount,
rawCurrencyId = null,
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "",
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0).orEmpty(),
date = null,
pendingActions = emptyList(),
pendingActionsConstraints = emptyList(),
@ -149,10 +149,10 @@ class InvalidatePendingTransactionsUseCase(
}
private fun findPartialUnstake(balances: MutableList<BalanceItem>, action: StakingAction): Pair<Int, BigDecimal> {
val index = balances.indexOfFirst {
!it.isPending && action.amount < it.amount &&
it.type == BalanceType.STAKED &&
it.validatorAddress == action.validatorAddress
val index = balances.indexOfFirst { balance ->
!balance.isPending && action.amount < balance.amount &&
balance.type == BalanceType.STAKED &&
balance.validatorAddress == action.validatorAddress
}
return index to action.amount
}

View file

@ -2,23 +2,27 @@ package com.tangem.domain.staking
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
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.models.staking.StakingID
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
/**
* Factory class for creating instances of [StakingID]
*
* @property walletManagersFacade wallet manager facade
* @property walletManagersFacade wallet manager facade
* @property stakingFeatureToggles staking feature toggles
*
[REDACTED_AUTHOR]
*/
class StakingIdFactory(
private val walletManagersFacade: WalletManagersFacade,
private val stakingFeatureToggles: StakingFeatureToggles,
) {
/**
@ -72,6 +76,8 @@ class StakingIdFactory(
ensureNotNull(integrationId) { Error.UnsupportedCurrency }
ensure(stakingFeatureToggles.isIntegrationEnabled(integrationId)) { Error.UnsupportedCurrency }
val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() }
ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) }

View file

@ -8,7 +8,7 @@ import com.tangem.domain.models.staking.action.StakingActionType
sealed class StakingAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(
category = "Staking",
event = event,

View file

@ -147,7 +147,7 @@ sealed interface StakingIntegrationID {
* @return a [StakingIntegrationID] if supported, or `null` if not supported.
*/
fun create(currencyId: CryptoCurrency.ID): StakingIntegrationID? {
val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId)
val blockchain = currencyId.toBlockchain()
return if (currencyId.contractAddress.isNullOrBlank()) {
// Order is not important — either P2PEthPool or Stakekit.Coin can be in any order

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
*
[REDACTED_AUTHOR]
*/
abstract class MultiStakingBalanceSupplier(
open class MultiStakingBalanceSupplier(
override val factory: FlowProducer.Factory<MultiStakingBalanceProducer.Params, MultiStakingBalanceProducer>,
override val keyCreator: (MultiStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<MultiStakingBalanceProducer, MultiStakingBalanceProducer.Params, Set<StakingBalance>>()

View file

@ -5,11 +5,11 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
@ -55,7 +55,7 @@ interface StakeKitRepository {
suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate
suspend fun constructTransaction(
networkId: String,
networkId: Network.RawID,
fee: Fee,
amount: Amount,
transactionId: String,

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
*
[REDACTED_AUTHOR]
*/
abstract class SingleStakingBalanceSupplier(
open class SingleStakingBalanceSupplier(
override val factory: FlowProducer.Factory<SingleStakingBalanceProducer.Params, SingleStakingBalanceProducer>,
override val keyCreator: (SingleStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<SingleStakingBalanceProducer, SingleStakingBalanceProducer.Params, StakingBalance>()

View file

@ -1,5 +1,8 @@
package com.tangem.domain.staking.toggles
import com.tangem.domain.staking.model.StakingIntegrationID
interface StakingFeatureToggles {
val isEthStakingEnabled: Boolean
fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean
}

View file

@ -5,17 +5,16 @@ import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.test.core.ProvideTestModels
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
@ -30,11 +29,16 @@ import org.junit.jupiter.params.ParameterizedTest
internal class StakingIdFactoryTest {
private val walletManagersFacade: WalletManagersFacade = mockk()
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
private val stakingFeatureToggles: StakingFeatureToggles = mockk()
private val factory = StakingIdFactory(
walletManagersFacade = walletManagersFacade,
stakingFeatureToggles = stakingFeatureToggles,
)
@BeforeEach
fun resetMocks() {
clearMocks(walletManagersFacade)
clearMocks(walletManagersFacade, stakingFeatureToggles)
every { stakingFeatureToggles.isIntegrationEnabled(any()) } returns true
}
@Nested
@ -66,6 +70,33 @@ internal class StakingIdFactoryTest {
}
}
@Test
fun `create returns UnsupportedCurrency if integration is disabled by toggle`() = runTest {
// Arrange
val userWalletId = UserWalletId(stringValue = "011")
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
every {
stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.StakeKit.Coin.Ton)
} returns false
// Act
val actual = factory.create(
userWalletId = userWalletId,
currencyId = currency.id,
network = currency.network,
)
// Assert
val expected = StakingIdFactory.Error.UnsupportedCurrency
Truth.assertThat(actual.leftOrNull()).isEqualTo(expected)
coVerify(inverse = true) {
walletManagersFacade.getDefaultAddress(userWalletId = any(), network = any())
}
}
@Test
fun `create returns UnableToGetAddress if address is null`() = runTest {
// Arrange
@ -154,7 +185,7 @@ internal class StakingIdFactoryTest {
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(
value = "token⟨ETH⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
value = "token⟨ethereum⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
),
expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon),
),
@ -168,6 +199,6 @@ internal class StakingIdFactoryTest {
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: Either<StakingIdFactory.Error, StakingID>)
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}")
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}")
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.staking
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingIntegrationID
@ -144,11 +145,11 @@ class StakingIntegrationIDTest {
expected = StakingIntegrationID.P2PEthPool,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"),
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩polygon-ecosystem-token⚓1234567890"),
expected = StakingIntegrationID.StakeKit.EthereumToken.Polygon,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨SOLANA⟩solana⚓1234567890"),
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨solana⟩solana⚓1234567890"),
expected = null,
),
)
@ -157,6 +158,6 @@ class StakingIntegrationIDTest {
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingIntegrationID?)
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}")
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}")
}
}