Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-15 19:30:17 +05:00
parent 9ebc1ac551
commit 25664700f6
20 changed files with 1261 additions and 17 deletions

View file

@ -153,6 +153,7 @@ dependencies {
implementation(projects.domain.swap)
implementation(projects.domain.walletManager)
implementation(projects.domain.walletManager.models)
implementation(projects.domain.yieldSupply)
implementation(projects.common)
implementation(projects.common.routing)
@ -201,6 +202,7 @@ dependencies {
implementation(projects.data.notifications)
implementation(projects.data.swap)
implementation(projects.data.walletManager)
implementation(projects.data.yieldSupply)
/** Features */
implementation(projects.features.referral.impl)

View file

@ -0,0 +1,50 @@
package com.tangem.tap.di.domain
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object YieldSupplyDomainModule {
@Provides
@Singleton
fun provideYieldSupplyStartEarningUseCase(
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
): YieldSupplyStartEarningUseCase {
return YieldSupplyStartEarningUseCase(
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyStopEarningUseCase(
yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
): YieldSupplyStopEarningUseCase {
return YieldSupplyStopEarningUseCase(
yieldSupplyTransactionRepository = yieldSupplyTransactionRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyEstimateEnterFeeUseCase(
feeRepository: FeeRepository,
feeErrorResolver: FeeErrorResolver,
): YieldSupplyEstimateEnterFeeUseCase {
return YieldSupplyEstimateEnterFeeUseCase(
feeRepository = feeRepository,
feeErrorResolver = feeErrorResolver,
)
}
}

View file

@ -10,6 +10,10 @@ android {
namespace = "com.tangem.data.transaction"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Tangem SDKs */
@ -21,13 +25,14 @@ dependencies {
implementation(projects.core.utils)
/** Domain */
implementation(projects.domain.transaction)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.legacy)
implementation(projects.domain.walletManager)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.transaction)
implementation(projects.domain.demo)
/** DI */
implementation(deps.hilt.android)
@ -35,4 +40,12 @@ dependencies {
/** Other */
implementation(deps.timber)
/** tests */
testImplementation(projects.common.test)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -1,13 +1,57 @@
package com.tangem.data.transaction
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.demo.DemoTransactionSender
import com.tangem.domain.demo.models.DemoConfig
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.transaction.FeeRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
internal class DefaultFeeRepository : FeeRepository {
internal class DefaultFeeRepository(
private val walletManagersFacade: WalletManagersFacade,
private val demoConfig: DemoConfig,
) : FeeRepository {
override fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean {
return networkId.toBlockchain().isFeeApproximate(amountType)
}
override suspend fun calculateFee(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
transactionData: TransactionData,
): TransactionFee {
val transactionSender = if (userWallet is UserWallet.Cold &&
demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)
) {
demoTransactionSender(userWallet, cryptoCurrency)
} else {
walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
) ?: error("WalletManager is null")
}
return when (val result = transactionSender.getFee(transactionData)) {
is Result.Success -> result.data
is Result.Failure -> throw result.error
}
}
private suspend fun demoTransactionSender(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
): DemoTransactionSender {
return DemoTransactionSender(
walletManagersFacade
.getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network)
?: error("WalletManager is null"),
)
}
}

View file

@ -3,10 +3,13 @@ package com.tangem.data.transaction.di
import com.tangem.data.transaction.DefaultFeeRepository
import com.tangem.data.transaction.DefaultTransactionRepository
import com.tangem.data.transaction.DefaultWalletAddressServiceRepository
import com.tangem.data.transaction.error.DefaultFeeErrorResolver
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -35,8 +38,11 @@ internal object TransactionDataModule {
@Provides
@Singleton
fun providesFeeRepository(): FeeRepository {
return DefaultFeeRepository()
fun providesFeeRepository(walletManagersFacade: WalletManagersFacade): FeeRepository {
return DefaultFeeRepository(
walletManagersFacade,
demoConfig = DemoConfig(),
)
}
@Provides
@ -50,4 +56,10 @@ internal object TransactionDataModule {
dispatchers = coroutineDispatcherProvider,
)
}
@Provides
@Singleton
fun providerFeeErrorResolver(): FeeErrorResolver {
return DefaultFeeErrorResolver()
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.data.transaction.error
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.transaction.error.GetFeeError
internal class DefaultFeeErrorResolver : FeeErrorResolver {
override fun resolve(throwable: Throwable): GetFeeError {
return when (throwable) {
is BlockchainSdkError.Tron.AccountActivationError -> {
GetFeeError.BlockchainErrors.TronActivationError
}
is BlockchainSdkError.Kaspa.ZeroUtxoError -> {
GetFeeError.BlockchainErrors.KaspaZeroUtxo
}
is BlockchainSdkError.Sui.OneSuiRequired -> {
GetFeeError.BlockchainErrors.SuiOneCoinRequired
}
else -> GetFeeError.DataError(throwable)
}
}
}

View file

@ -0,0 +1,48 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.data.yield.supply"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Tangem SDKs */
implementation(tangemDeps.blockchain)
/** Core */
implementation(projects.core.datasource)
implementation(projects.core.utils)
/** Domain */
implementation(projects.domain.yieldSupply)
implementation(projects.domain.walletManager)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.timber)
/** tests */
testImplementation(projects.common.test)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -0,0 +1,371 @@
package com.tangem.data.yield.supply
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
import com.tangem.blockchain.blockchains.tron.TronTransactionExtras
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory
import com.tangem.blockchainsdk.utils.toBlockchain
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 com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LargeClass")
internal class DefaultYieldSupplyTransactionRepository(
private val walletManagersFacade: WalletManagersFacade,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldSupplyTransactionRepository {
override suspend fun createEnterTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): List<TransactionData.Uncompiled> {
val cryptoCurrency = cryptoCurrencyStatus.currency
require(cryptoCurrency is CryptoCurrency.Token)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
val existingYieldContractAddress = getYieldContractAddress(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
val calculatedYieldContractAddress = calculateYieldContractAddress(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
) ?: error("Calculated yield contract address is null")
val yieldTokenStatus = cryptoCurrencyStatus.value.yieldSupplyStatus ?: getYieldTokenStatus(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
)
return buildEnterTransactions(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
existingYieldContractAddress = existingYieldContractAddress,
calculatedYieldContractAddress = calculatedYieldContractAddress,
yieldTokenStatus = yieldTokenStatus,
)
}
override suspend fun createExitTransaction(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
yieldSupplyStatus: YieldSupplyStatus,
fee: Fee?,
): TransactionData.Uncompiled = withContext(dispatchers.io) {
require(cryptoCurrency is CryptoCurrency.Token)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
val callData = YieldSupplyContractCallDataProviderFactory.getExitCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
)
createTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
callData = callData,
destinationAddress = walletManager.getYieldContract(),
yieldSupplyStatus = yieldSupplyStatus,
fee = fee,
)
}
private fun buildEnterTransactions(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
existingYieldContractAddress: String?,
calculatedYieldContractAddress: String,
yieldTokenStatus: YieldSupplyStatus?,
): MutableList<TransactionData.Uncompiled> {
val enterTransactions = mutableListOf<TransactionData.Uncompiled>()
when {
existingYieldContractAddress == null || existingYieldContractAddress == EthereumUtils.ZERO_ADDRESS -> {
enterTransactions.add(
createDeployTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
),
)
}
yieldTokenStatus == null -> error("Yield token status is null")
!yieldTokenStatus.isInitialized -> enterTransactions.add(
createInitTokenTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
yieldSupplyStatus = yieldTokenStatus,
yieldContractAddress = calculatedYieldContractAddress,
),
)
!yieldTokenStatus.isActive -> enterTransactions.add(
createReactivateTokenTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
yieldSupplyStatus = yieldTokenStatus,
yieldContractAddress = calculatedYieldContractAddress,
),
)
else -> Unit
}
if (yieldTokenStatus?.isAllowedToSpend == false) {
enterTransactions.add(
createTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
callData = ApprovalERC20TokenCallData(
spenderAddress = calculatedYieldContractAddress,
amount = null,
),
destinationAddress = cryptoCurrency.contractAddress,
yieldSupplyStatus = yieldTokenStatus,
fee = null,
),
)
}
enterTransactions.add(
createEnterTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
yieldSupplyStatus = yieldTokenStatus,
yieldContractAddress = calculatedYieldContractAddress,
),
)
return enterTransactions
}
private suspend fun calculateYieldContractAddress(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): String? = withContext(dispatchers.io) {
require(cryptoCurrency is CryptoCurrency.Token)
runCatching {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
walletManager.calculateYieldContract()
}.onFailure(Timber::e)
.getOrNull()
}
private suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? =
withContext(dispatchers.io) {
require(cryptoCurrency is CryptoCurrency.Token)
runCatching {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
walletManager.getYieldContract()
}.onFailure(Timber::e)
.getOrNull()
}
private suspend fun getYieldTokenStatus(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency,
): YieldSupplyStatus? = withContext(dispatchers.io) {
require(cryptoCurrency is CryptoCurrency.Token)
runCatching {
val sdkSupplyStatus = walletManager.getYieldSupplyStatus(cryptoCurrency.contractAddress)
val isAllowedToSpend = walletManager.isAllowedToSpend(cryptoCurrency.contractAddress)
YieldSupplyStatus(
isActive = sdkSupplyStatus?.isActive == true,
isInitialized = sdkSupplyStatus?.isInitialized == true,
isAllowedToSpend = isAllowedToSpend,
)
}.onFailure(Timber::e).getOrNull()
}
private fun createDeployTransaction(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
walletAddress = walletManager.wallet.address,
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
)
val factoryContractAddress = walletManager.getYieldSupplyContractAddresses()?.factoryContractAddress
?: error("Factory contract address is null")
return createTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
callData = callData,
destinationAddress = factoryContractAddress,
yieldSupplyStatus = null,
fee = null,
)
}
private fun createInitTokenTransaction(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
yieldContractAddress: String,
yieldSupplyStatus: YieldSupplyStatus,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
)
return createTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
callData = callData,
destinationAddress = yieldContractAddress,
yieldSupplyStatus = yieldSupplyStatus,
fee = null,
)
}
private fun createReactivateTokenTransaction(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
yieldContractAddress: String,
yieldSupplyStatus: YieldSupplyStatus,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
maxNetworkFee = MAX_NETWORK_FEE.convertToSdkAmount(cryptoCurrency),
)
return createTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
callData = callData,
destinationAddress = yieldContractAddress,
yieldSupplyStatus = yieldSupplyStatus,
fee = null,
)
}
private fun createEnterTransaction(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency.Token,
yieldSupplyStatus: YieldSupplyStatus?,
yieldContractAddress: String,
): TransactionData.Uncompiled {
val callData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(
tokenContractAddress = cryptoCurrency.contractAddress,
)
return createTransaction(
walletManager = walletManager,
cryptoCurrency = cryptoCurrency,
callData = callData,
destinationAddress = yieldContractAddress,
yieldSupplyStatus = yieldSupplyStatus,
fee = null,
)
}
@Suppress("LongParameterList")
private fun createTransaction(
walletManager: WalletManager,
cryptoCurrency: CryptoCurrency,
callData: SmartContractCallData,
destinationAddress: String,
yieldSupplyStatus: YieldSupplyStatus?,
fee: Fee?,
): TransactionData.Uncompiled {
requireNotNull(cryptoCurrency as? CryptoCurrency.Token)
val blockchain = cryptoCurrency.network.id.toBlockchain()
val extras = createTransactionDataExtras(
callData = callData,
blockchain = blockchain,
)
val amount = getYieldSupplyAmount(cryptoCurrency, yieldSupplyStatus)
return if (fee != null) {
walletManager.createTransaction(
amount = amount,
fee = fee,
destination = destinationAddress,
).copy(
extras = extras,
)
} else {
TransactionData.Uncompiled(
amount = amount,
sourceAddress = walletManager.wallet.address,
destinationAddress = destinationAddress,
extras = extras,
fee = null,
)
}
}
private fun createTransactionDataExtras(
callData: SmartContractCallData,
blockchain: Blockchain,
): TransactionExtras {
return when {
blockchain.isEvm() -> {
EthereumTransactionExtras(
callData = callData,
gasLimit = null,
nonce = null,
)
}
blockchain == Blockchain.Tron -> {
TronTransactionExtras(
callData = callData,
)
}
else -> error("Data extras not supported for $blockchain")
}
}
private fun getYieldSupplyAmount(cryptoCurrency: CryptoCurrency.Token, yieldSupplyStatus: YieldSupplyStatus?) =
BigDecimal.ZERO.convertToSdkAmount(
cryptoCurrency = cryptoCurrency,
amountType = AmountType.TokenYieldSupply(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
isActive = yieldSupplyStatus?.isActive ?: false,
isInitialized = yieldSupplyStatus?.isInitialized ?: false,
isAllowedToSpend = yieldSupplyStatus?.isAllowedToSpend ?: false,
),
)
private companion object {
val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO for TESTNET only
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.yield.supply.di
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object YieldSupplyDataModule {
@Provides
@Singleton
fun providerYieldSupplyTransactionRepository(
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): YieldSupplyTransactionRepository {
return DefaultYieldSupplyTransactionRepository(
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
)
}
}

View file

@ -0,0 +1,288 @@
package com.tangem.data.yield.supply
import com.google.common.truth.Truth
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
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
import io.mockk.spyk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultYieldSupplyTransactionRepositoryTest {
private val networkId = Network.ID(value = "ETH/test", derivationPath = Network.DerivationPath.None)
private val mockedContractAddress = "0x000000000000000000000000000000000000"
private val yieldContractAddress = "0x1234"
private val userWalletId = mockk<UserWalletId>()
private val cryptoCurrency = mockk<CryptoCurrency.Token>(relaxed = true) {
every { network.id } returns networkId
every { contractAddress } returns mockedContractAddress
}
private val cryptoCurrencyStatus = mockk<CryptoCurrencyStatus>(relaxed = true) {
every { currency } returns cryptoCurrency
every { value.yieldSupplyStatus } returns null
}
private val walletManager = mockk<WalletManager>(relaxed = true) {
every { wallet } returns mockk(relaxed = true)
every { getYieldSupplyContractAddresses() } returns mockk(relaxed = true) {
every { factoryContractAddress } returns "factory"
}
}
private val walletManagersFacade: WalletManagersFacade = mockk {
coEvery { getOrCreateWalletManager(any(), any(), any()) } returns walletManager
}
private lateinit var repository: DefaultYieldSupplyTransactionRepository
@BeforeEach
fun setUp() {
repository = spyk(
objToCopy = DefaultYieldSupplyTransactionRepository(
walletManagersFacade = walletManagersFacade,
dispatchers = TestingCoroutineDispatcherProvider(),
),
recordPrivateCalls = true,
)
every { mockk<Token>().contractAddress } returns mockedContractAddress
}
@Test
fun `createEnterTransactions returns deploy-approve-enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns EthereumUtils.ZERO_ADDRESS
coEvery { walletManager.getYieldSupplyStatus(any()) } returns null
coEvery { walletManager.isAllowedToSpend(any()) } returns false
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
// Assert that 3 transactions are returned: deploy, approve, enter
Truth.assertThat(result).isNotNull()
Truth.assertThat(result).isNotEmpty()
// Check transaction - deploy
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getDeployCallData(
walletAddress = walletManager.wallet.address,
tokenContractAddress = mockedContractAddress,
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
)
val firstTransaction = result.first()
Truth.assertThat(firstTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((firstTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(firstExpectedCallData.data)
// Check transaction - approve
val secondExpectedCallData = SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = yieldContractAddress,
amount = null,
blockchain = Blockchain.EthereumTestnet,
)
val secondTransaction = result[1]
Truth.assertThat(secondTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((secondTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(secondExpectedCallData.data)
// Check transaction - enter
val thirdExpectedCallData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(mockedContractAddress)
val thirdTransaction = result[2]
Truth.assertThat(thirdTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((thirdTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(thirdExpectedCallData.data)
}
@Test
fun `createEnterTransactions returns init-approve-enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
isActive = false,
isInitialized = false,
maxNetworkFee = BigDecimal.TEN,
)
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
// Assert that 3 transactions are returned: init token, approve, enter
Truth.assertThat(result).isNotNull()
Truth.assertThat(result).isNotEmpty()
// Check transaction - init token
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getInitTokenCallData(
tokenContractAddress = mockedContractAddress,
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
)
val firstTransaction = result.first()
Truth.assertThat(firstTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((firstTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(firstExpectedCallData.data)
// Check transaction - approve
val secondExpectedCallData = SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = yieldContractAddress,
amount = null,
blockchain = Blockchain.EthereumTestnet,
)
val secondTransaction = result[1]
Truth.assertThat(secondTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((secondTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(secondExpectedCallData.data)
// Check transaction - enter
val thirdExpectedCallData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(mockedContractAddress)
val thirdTransaction = result[2]
Truth.assertThat(thirdTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((thirdTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(thirdExpectedCallData.data)
}
@Test
fun `createEnterTransactions returns reactivate-approve-enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
isActive = false,
isInitialized = true,
maxNetworkFee = BigDecimal.TEN,
)
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
// Assert that 3 transactions are returned: reactivate token, approve, enter
Truth.assertThat(result).isNotNull()
Truth.assertThat(result).isNotEmpty()
// Check transaction - reactivate token
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
tokenContractAddress = mockedContractAddress,
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
)
val firstTransaction = result.first()
Truth.assertThat(firstTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((firstTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(firstExpectedCallData.data)
// Check transaction - approve
val secondExpectedCallData = SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = yieldContractAddress,
amount = null,
blockchain = Blockchain.EthereumTestnet,
)
val secondTransaction = result[1]
Truth.assertThat(secondTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((secondTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(secondExpectedCallData.data)
// Check transaction - enter
val thirdExpectedCallData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(mockedContractAddress)
val thirdTransaction = result[2]
Truth.assertThat(thirdTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((thirdTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(thirdExpectedCallData.data)
}
@Test
fun `createEnterTransactions returns reactivate-enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
isActive = false,
isInitialized = true,
maxNetworkFee = BigDecimal.TEN,
)
coEvery { walletManager.isAllowedToSpend(any()) } returns true
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
// Assert that 2 transactions are returned: approve, enter
Truth.assertThat(result).isNotNull()
Truth.assertThat(result).isNotEmpty()
// Check transaction - reactivate token
val firstExpectedCallData = YieldSupplyContractCallDataProviderFactory.getReactivateTokenCallData(
tokenContractAddress = mockedContractAddress,
maxNetworkFee = BigDecimal.TEN.convertToSdkAmount(cryptoCurrency),
)
val firstTransaction = result.first()
Truth.assertThat(firstTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((firstTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(firstExpectedCallData.data)
// Check transaction - enter
val thirdExpectedCallData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(mockedContractAddress)
val thirdTransaction = result[1]
Truth.assertThat(thirdTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((thirdTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(thirdExpectedCallData.data)
}
@Test
fun `createEnterTransactions returns enter transactions`() = runTest {
coEvery { walletManager.getYieldContract() } returns yieldContractAddress
coEvery { walletManager.calculateYieldContract() } returns yieldContractAddress
coEvery { walletManager.getYieldSupplyStatus(any()) } returns SDKYieldSupplyStatus(
isActive = true,
isInitialized = true,
maxNetworkFee = BigDecimal.TEN,
)
coEvery { walletManager.isAllowedToSpend(any()) } returns true
val result = repository.createEnterTransactions(userWalletId, cryptoCurrencyStatus)
// Assert that transaction is returned: enter
Truth.assertThat(result).isNotNull()
Truth.assertThat(result).isNotEmpty()
// Check transaction - enter
val thirdExpectedCallData = YieldSupplyContractCallDataProviderFactory.getEnterCallData(mockedContractAddress)
val thirdTransaction = result[0]
Truth.assertThat(thirdTransaction.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((thirdTransaction.extras as EthereumTransactionExtras).callData?.data)
.isEqualTo(thirdExpectedCallData.data)
}
@Test
fun `createExitTransaction returns valid transaction`() = runTest {
val expectedCallData =
YieldSupplyContractCallDataProviderFactory.getExitCallData(mockedContractAddress)
val yieldSupplyStatus = mockk<YieldSupplyStatus>(relaxed = true)
val result = repository.createExitTransaction(userWalletId, cryptoCurrency, yieldSupplyStatus, null)
Truth.assertThat(result).isNotNull()
Truth.assertThat(result.extras).isInstanceOf(EthereumTransactionExtras::class.java)
Truth.assertThat((result.extras as EthereumTransactionExtras).callData?.data).isEqualTo(expectedCallData.data)
}
}

View file

@ -7,18 +7,26 @@ import java.math.BigDecimal
import com.tangem.blockchain.common.Amount as SdkAmount
/** Converts `BigDecimal` [cryptoCurrency] to [SdkAmount] */
fun BigDecimal.convertToSdkAmount(cryptoCurrency: CryptoCurrency): SdkAmount = SdkAmount(
fun BigDecimal.convertToSdkAmount(
cryptoCurrency: CryptoCurrency,
amountType: AmountType = getAmountTypeFromCryptoCurrency(cryptoCurrency),
): SdkAmount = SdkAmount(
currencySymbol = cryptoCurrency.symbol,
value = this,
decimals = cryptoCurrency.decimals,
type = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.Coin
is CryptoCurrency.Token -> AmountType.Token(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
)
},
)
type = amountType,
)
/**
* Converts [CryptoCurrency] to [AmountType] based on its type
*/
private fun getAmountTypeFromCryptoCurrency(cryptoCurrency: CryptoCurrency) = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.Coin
is CryptoCurrency.Token -> AmountType.Token(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
)
}

View file

@ -1,10 +1,21 @@
package com.tangem.domain.transaction
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
interface FeeRepository {
/** Returns if fee is approximate for current [networkId] */
fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean
/** Returns fee calculated for the transaction [transactionData] */
suspend fun calculateFee(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
transactionData: TransactionData,
): TransactionFee
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.transaction.error
interface FeeErrorResolver {
fun resolve(throwable: Throwable): GetFeeError
}

View file

@ -0,0 +1,35 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.domain.yield.supply"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.transaction)
implementation(projects.domain.legacy)
/** Tandem SDK */
implementation(tangemDeps.blockchain)
/** Other */
implementation(deps.arrow.core)
/** tests */
testImplementation(projects.common.test)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.yield.supply
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.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
interface YieldSupplyTransactionRepository {
suspend fun createEnterTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): List<TransactionData.Uncompiled>
suspend fun createExitTransaction(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
yieldSupplyStatus: YieldSupplyStatus,
fee: Fee?,
): TransactionData.Uncompiled
}

View file

@ -0,0 +1,58 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
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.wallet.UserWallet
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.transaction.error.GetFeeError
class YieldSupplyEstimateEnterFeeUseCase(
private val feeRepository: FeeRepository,
private val feeErrorResolver: FeeErrorResolver,
) {
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
transactionDataList: List<TransactionData.Uncompiled>,
): Either<GetFeeError, List<TransactionData.Uncompiled>> = Either.catch {
transactionDataList.mapIndexed { index, transaction ->
val fee = feeRepository.calculateFee(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
transactionData = transaction,
).normal
if (index == transactionDataList.lastIndex) { // todo yield supply replace with check for contract
transaction.copy(fee = fee.fixFee(cryptoCurrency))
} else {
transaction.copy(fee = fee)
}
}
}.mapLeft(feeErrorResolver::resolve)
private fun Fee.fixFee(cryptoCurrency: CryptoCurrency) = when (this) {
is Fee.Ethereum.Legacy -> copy(
gasLimit = ETHEREUM_CONSTANT_GAS_LIMIT,
amount = amount.copy(
value = gasPrice.multiply(ETHEREUM_CONSTANT_GAS_LIMIT)
.toBigDecimal().movePointLeft(cryptoCurrency.decimals),
),
)
is Fee.Ethereum.EIP1559 -> copy(
gasLimit = ETHEREUM_CONSTANT_GAS_LIMIT,
amount = amount.copy(
value = maxFeePerGas.multiply(ETHEREUM_CONSTANT_GAS_LIMIT)
.toBigDecimal().movePointLeft(cryptoCurrency.decimals),
),
)
else -> this
}
private companion object {
// Using constant gas limit to avoid fee calculation errors when contract address is not deployed yet
val ETHEREUM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger()
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
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
class YieldSupplyStartEarningUseCase(
private val yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<Throwable, List<TransactionData.Uncompiled>> = Either.catch {
yieldSupplyTransactionRepository.createEnterTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
class YieldSupplyStopEarningUseCase(
private val yieldSupplyTransactionRepository: YieldSupplyTransactionRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
fee: Fee?,
): Either<Throwable, TransactionData.Uncompiled> = Either.catch {
val yieldTokenStatus = cryptoCurrencyStatus.value.yieldSupplyStatus ?: error("")
val cryptoCurrency = cryptoCurrencyStatus.currency
yieldSupplyTransactionRepository.createExitTransaction(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyStatus = yieldTokenStatus,
fee = null,
)
}
}

View file

@ -0,0 +1,172 @@
package com.tangem.domain.yield.supply
import arrow.core.Either
import com.google.common.truth.Truth
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.math.BigInteger
@OptIn(ExperimentalCoroutinesApi::class)
class YieldSupplyEstimateEnterFeeUseCaseTest {
private val feeRepository: FeeRepository = mockk()
private val feeErrorResolver: FeeErrorResolver = mockk()
private val useCase = YieldSupplyEstimateEnterFeeUseCase(feeRepository, feeErrorResolver)
private val userWallet: UserWallet = mockk()
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) {
every { decimals } returns 18
}
private fun ethLegacyFee(
gasPrice: BigInteger = BigInteger.valueOf(100_000_000_000L),
gasLimit: BigInteger = BigInteger.valueOf(21_000),
) = Fee.Ethereum.Legacy(
gasPrice = gasPrice,
gasLimit = gasLimit,
amount = BigDecimal.ONE.convertToSdkAmount(cryptoCurrency),
)
private fun ethEip1559Fee(
maxFeePerGas: BigInteger = BigInteger.valueOf(100_000_000_000L),
gasLimit: BigInteger = BigInteger.valueOf(21_000),
) = Fee.Ethereum.EIP1559(
maxFeePerGas = maxFeePerGas,
priorityFee = BigInteger.ONE,
gasLimit = gasLimit,
amount = BigDecimal.ONE.convertToSdkAmount(cryptoCurrency),
)
private fun uncompiled(fee: Fee) = TransactionData.Uncompiled(
fee = fee,
amount = BigDecimal.ONE.convertToSdkAmount(cryptoCurrency),
contractAddress = null,
sourceAddress = "0x1234567890123456789012345678901234567890",
destinationAddress = "0x1234567890123456789012345678901234567890",
)
@Test
fun `test 1 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))
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))
}
@Test
fun `test 2 transactions, only last uses constant 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))
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))
}
@Test
fun `test 3 transactions, only last uses constant 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))
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))
}
@Test
fun `test 1 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))
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))
}
@Test
fun `test 2 transactions, only last uses constant 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))
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))
}
@Test
fun `test 3 transactions, only last uses constant 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))
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))
}
}

View file

@ -350,6 +350,7 @@ include(":domain:swap")
include(":domain:swap:models")
include(":domain:wallet-manager")
include(":domain:wallet-manager:models")
include(":domain:yield-supply")
// endregion Domain modules
// region Data modules
@ -383,5 +384,6 @@ include(":data:blockaid")
include(":data:swap")
include(":data:express")
include(":data:wallet-manager")
include(":data:yield-supply")
// endregion Data modules
include(":features:tangempay:onboarding")