Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-19 11:22:53 +05:00
commit c0b13031c7
6 changed files with 172 additions and 64 deletions

View file

@ -30,6 +30,7 @@ internal class DefaultYieldSupplyTransactionRepository(
override suspend fun createEnterTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
maxNetworkFee: BigDecimal,
): List<TransactionData.Uncompiled> {
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -62,6 +63,7 @@ internal class DefaultYieldSupplyTransactionRepository(
existingYieldContractAddress = existingYieldContractAddress,
calculatedYieldContractAddress = calculatedYieldContractAddress,
yieldTokenStatus = yieldTokenStatus,
maxNetworkFee = maxNetworkFee,
)
}
@ -93,12 +95,14 @@ internal class DefaultYieldSupplyTransactionRepository(
)
}
@Suppress("LongParameterList")
private fun buildEnterTransactions(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
existingYieldContractAddress: String?,
calculatedYieldContractAddress: String,
yieldTokenStatus: YieldSupplyStatus?,
maxNetworkFee: BigDecimal,
): MutableList<TransactionData.Uncompiled> {
val enterTransactions = mutableListOf<TransactionData.Uncompiled>()
@ -108,6 +112,7 @@ internal class DefaultYieldSupplyTransactionRepository(
createDeployTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
maxNetworkFee = maxNetworkFee,
),
)
}
@ -118,6 +123,7 @@ internal class DefaultYieldSupplyTransactionRepository(
cryptoCurrency = cryptoCurrency,
yieldSupplyStatus = yieldTokenStatus,
yieldContractAddress = calculatedYieldContractAddress,
maxNetworkFee = maxNetworkFee,
),
)
!yieldTokenStatus.isActive -> enterTransactions.add(
@ -126,6 +132,7 @@ internal class DefaultYieldSupplyTransactionRepository(
cryptoCurrency = cryptoCurrency,
yieldSupplyStatus = yieldTokenStatus,
yieldContractAddress = calculatedYieldContractAddress,
maxNetworkFee = maxNetworkFee,
),
)
else -> Unit
@ -202,6 +209,7 @@ internal class DefaultYieldSupplyTransactionRepository(
isActive = sdkSupplyStatus?.isActive == true,
isInitialized = sdkSupplyStatus?.isInitialized == true,
isAllowedToSpend = isAllowedToSpend,
// maxNetworkFee = sdkSupplyStatus?.maxNetworkFee,
)
}.onFailure(Timber::e).getOrNull()
}
@ -209,11 +217,12 @@ internal class DefaultYieldSupplyTransactionRepository(
private fun createDeployTransaction(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
maxNetworkFee: BigDecimal,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
walletAddress = walletManager.wallet.address,
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
maxNetworkFee = maxNetworkFee.convertToSdkAmount(cryptoCurrency),
)
val factoryContractAddress = walletManager.getYieldSupplyContractAddresses()?.factoryContractAddress
@ -234,10 +243,11 @@ internal class DefaultYieldSupplyTransactionRepository(
cryptoCurrency: CryptoCurrency.Token,
yieldContractAddress: String,
yieldSupplyStatus: YieldSupplyStatus,
maxNetworkFee: BigDecimal,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
maxNetworkFee = maxNetworkFee.convertToSdkAmount(cryptoCurrency),
)
return createTransaction(
@ -255,10 +265,11 @@ internal class DefaultYieldSupplyTransactionRepository(
cryptoCurrency: CryptoCurrency.Token,
yieldContractAddress: String,
yieldSupplyStatus: YieldSupplyStatus,
maxNetworkFee: BigDecimal,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
maxNetworkFee = maxNetworkFee.convertToSdkAmount(cryptoCurrency),
)
return createTransaction(
@ -364,8 +375,4 @@ internal class DefaultYieldSupplyTransactionRepository(
isAllowedToSpend = yieldSupplyStatus?.isAllowedToSpend ?: false,
),
)
private companion object {
val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO for TESTNET only
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYieldSupplyStatus
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@ -26,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYieldSupplyStatus
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultYieldSupplyTransactionRepositoryTest {
@ -74,7 +74,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
coEvery { walletManager.isAllowedToSpend(any()) } returns false
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
val result = repository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = BigDecimal.TEN,
)
// Assert that 3 transactions are returned: deploy, approve, enter
Truth.assertThat(result).isNotNull()
@ -117,13 +121,18 @@ class DefaultYieldSupplyTransactionRepositoryTest {
fun `createEnterTransactions returns init-approve-enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
coEvery { walletManager.isAllowedToSpend(any()) } returns false
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
isActive = false,
isInitialized = false,
maxNetworkFee = BigDecimal.TEN,
)
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
val result = repository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = BigDecimal.TEN,
)
// Assert that 3 transactions are returned: init token, approve, enter
Truth.assertThat(result).isNotNull()
@ -165,13 +174,18 @@ class DefaultYieldSupplyTransactionRepositoryTest {
fun `createEnterTransactions returns reactivate-approve-enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
coEvery { walletManager.isAllowedToSpend(any()) } returns false
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
isActive = false,
isInitialized = true,
maxNetworkFee = BigDecimal.TEN,
)
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
val result = repository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = BigDecimal.TEN,
)
// Assert that 3 transactions are returned: reactivate token, approve, enter
Truth.assertThat(result).isNotNull()
@ -220,7 +234,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
)
coEvery { walletManager.isAllowedToSpend(any()) } returns true
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
val result = repository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = BigDecimal.TEN,
)
// Assert that 2 transactions are returned: approve, enter
Truth.assertThat(result).isNotNull()
@ -257,7 +275,11 @@ class DefaultYieldSupplyTransactionRepositoryTest {
)
coEvery { walletManager.isAllowedToSpend(any()) } returns true
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
val result = repository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = BigDecimal.TEN,
)
// Assert that transaction is returned: enter
Truth.assertThat(result).isNotNull()

View file

@ -6,12 +6,14 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import java.math.BigDecimal
interface YieldSupplyTransactionRepository {
suspend fun createEnterTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
maxNetworkFee: BigDecimal,
): List<TransactionData.Uncompiled>
suspend fun createExitTransaction(

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import java.math.BigDecimal
class YieldSupplyStartEarningUseCase(
private val yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
@ -13,10 +14,12 @@ class YieldSupplyStartEarningUseCase(
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
maxNetworkFee: BigDecimal,
): Either<Throwable, List<TransactionData.Uncompiled>> = Either.catch {
yieldSupplyTransactionRepository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = maxNetworkFee,
)
}
}

View file

@ -2,9 +2,14 @@ package com.tangem.domain.yield.supply
import arrow.core.Either
import com.google.common.truth.Truth
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.FeeRepository
@ -50,123 +55,187 @@ class YieldSupplyEstimateEnterFeeUseCaseTest {
amount = BigDecimal.ONE.convertToSdkAmount(cryptoCurrency),
)
private fun uncompiled(fee: Fee) = TransactionData.Uncompiled(
private fun uncompiled(fee: Fee, extras: TransactionExtras) = TransactionData.Uncompiled(
fee = fee,
amount = BigDecimal.ONE.convertToSdkAmount(cryptoCurrency),
contractAddress = null,
sourceAddress = "0x1234567890123456789012345678901234567890",
destinationAddress = "0x1234567890123456789012345678901234567890",
extras = extras,
)
@Test
fun `test 1 transaction uses constant gas limit Legacy`() = runTest {
fun `test enter transaction uses constant gas limit Legacy`() = runTest {
val fee = TransactionFee.Single(ethLegacyFee())
val tx = uncompiled(ethLegacyFee())
coEvery { feeRepository.calculateFee(any(), any(), any()) } returns fee
val result = useCase(userWallet, cryptoCurrency, listOf(tx))
val result = useCase(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = listOf(
getDeployTx(),
getEnterTx(),
),
)
Truth.assertThat(result.isRight()).isTrue()
val txs = (result as Either.Right).value
Truth.assertThat(txs.size).isEqualTo(1)
val lastFee = txs.last().fee as Fee.Ethereum.Legacy
Truth.assertThat(lastFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
val deployFee = txs.first().fee as Fee.Ethereum.Legacy
val enterFee = txs.last().fee as Fee.Ethereum.Legacy
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
}
@Test
fun `test 2 transactions, only last uses constant gas limit Legacy`() = runTest {
fun `test not enter transaction uses gas limit Legacy`() = runTest {
val fee = TransactionFee.Single(ethLegacyFee())
val tx = uncompiled(ethLegacyFee())
coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee)
val result = useCase(userWallet, cryptoCurrency, listOf(tx, tx))
val result = useCase(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = listOf(
getDeployTx(),
getApproveTx(),
),
)
Truth.assertThat(result.isRight()).isTrue()
val txs = (result as Either.Right).value
Truth.assertThat(txs.size).isEqualTo(2)
val firstFee = txs.first().fee as Fee.Ethereum.Legacy
val lastFee = txs.last().fee as Fee.Ethereum.Legacy
Truth.assertThat(firstFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(lastFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
val deployFee = txs.first().fee as Fee.Ethereum.Legacy
val approveFee = txs.last().fee as Fee.Ethereum.Legacy
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
}
@Test
fun `test 3 transactions, only last uses constant gas limit Legacy`() = runTest {
fun `test enter transaction with wrong order uses gas limit Legacy`() = runTest {
val fee = TransactionFee.Single(ethLegacyFee())
val tx = uncompiled(ethLegacyFee())
coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee, fee)
val result = useCase(userWallet, cryptoCurrency, listOf(tx, tx, tx))
coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee)
val result = useCase(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = listOf(
getEnterTx(),
getDeployTx(),
),
)
Truth.assertThat(result.isRight()).isTrue()
val txs = (result as Either.Right).value
Truth.assertThat(txs.size).isEqualTo(3)
val firstFee = txs[0].fee as Fee.Ethereum.Legacy
val secondFee = txs[1].fee as Fee.Ethereum.Legacy
val lastFee = txs[2].fee as Fee.Ethereum.Legacy
Truth.assertThat(firstFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(secondFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(lastFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
val deployFee = txs.first().fee as Fee.Ethereum.Legacy
val enterFee = txs.last().fee as Fee.Ethereum.Legacy
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
}
@Test
fun `test 1 transaction uses constant gas limit Eip1559`() = runTest {
fun `test enter transaction uses constant gas limit Eip1559`() = runTest {
val fee = TransactionFee.Single(ethEip1559Fee())
val tx = uncompiled(ethEip1559Fee())
coEvery { feeRepository.calculateFee(any(), any(), any()) } returns fee
val result = useCase(userWallet, cryptoCurrency, listOf(tx))
val result = useCase(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = listOf(
getDeployTx(),
getEnterTx(),
),
)
Truth.assertThat(result.isRight()).isTrue()
val txs = (result as Either.Right).value
Truth.assertThat(txs.size).isEqualTo(1)
val lastFee = txs.last().fee as Fee.Ethereum.EIP1559
Truth.assertThat(lastFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
val deployFee = txs.first().fee as Fee.Ethereum.EIP1559
val enterFee = txs.last().fee as Fee.Ethereum.EIP1559
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
}
@Test
fun `test 2 transactions, only last uses constant gas limit Eip1559`() = runTest {
fun `test not enter transaction uses gas limit Eip1559`() = runTest {
val fee = TransactionFee.Single(ethEip1559Fee())
val tx = uncompiled(ethEip1559Fee())
coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee)
val result = useCase(userWallet, cryptoCurrency, listOf(tx, tx))
val result = useCase(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = listOf(
getDeployTx(),
getApproveTx(),
),
)
Truth.assertThat(result.isRight()).isTrue()
val txs = (result as Either.Right).value
Truth.assertThat(txs.size).isEqualTo(2)
val firstFee = txs.first().fee as Fee.Ethereum.EIP1559
val lastFee = txs.last().fee as Fee.Ethereum.EIP1559
Truth.assertThat(firstFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(lastFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
val deployFee = txs.first().fee as Fee.Ethereum.EIP1559
val approveFee = txs.last().fee as Fee.Ethereum.EIP1559
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
}
@Test
fun `test 3 transactions, only last uses constant gas limit Eip1559`() = runTest {
fun `test enter transaction with wrong order uses gas limit Eip1559`() = runTest {
val fee = TransactionFee.Single(ethEip1559Fee())
val tx = uncompiled(ethEip1559Fee())
coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee, fee)
val result = useCase(userWallet, cryptoCurrency, listOf(tx, tx, tx))
coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee)
val result = useCase(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionDataList = listOf(
getEnterTx(),
getDeployTx(),
),
)
Truth.assertThat(result.isRight()).isTrue()
val txs = (result as Either.Right).value
Truth.assertThat(txs.size).isEqualTo(3)
val firstFee = txs[0].fee as Fee.Ethereum.EIP1559
val secondFee = txs[1].fee as Fee.Ethereum.EIP1559
val lastFee = txs[2].fee as Fee.Ethereum.EIP1559
Truth.assertThat(firstFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(secondFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(lastFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
val deployFee = txs.first().fee as Fee.Ethereum.EIP1559
val enterFee = txs.last().fee as Fee.Ethereum.EIP1559
Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000))
Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000))
}
private fun getDeployTx() = uncompiled(
fee = ethLegacyFee(),
extras = EthereumTransactionExtras(
YieldSupplyContractCallDataProviderFactory.getDeployCallData(
walletAddress = "0x1234567890123456789012345678901234567890",
tokenContractAddress = "0x1234567890123456789012345678901234567890",
maxNetworkFee = BigDecimal.ONE.convertToSdkAmount(cryptoCurrency),
),
),
)
private fun getApproveTx() = uncompiled(
fee = ethLegacyFee(),
extras = EthereumTransactionExtras(
SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = "0x1234567890123456789012345678901234567890",
amount = null,
blockchain = Blockchain.EthereumTestnet,
),
),
)
private fun getEnterTx() = uncompiled(
fee = ethLegacyFee(),
extras = EthereumTransactionExtras(
YieldSupplyContractCallDataProviderFactory.getEnterCallData(
tokenContractAddress = "0x1234567890123456789012345678901234567890",
),
),
)
}

View file

@ -106,6 +106,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
val transactionListData = yieldSupplyStartEarningUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = MAX_NETWORK_FEE,
).getOrNull() ?: return
val updatedTransactionList = yieldSupplyEstimateEnterFeeUseCase.invoke(
@ -221,4 +222,8 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
onLoadFee()
}
}
private companion object {
val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO replace with value from api
}
}