Updated on 2026-08-14
This commit is contained in:
commit
82117cec21
20 changed files with 1261 additions and 17 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
data/yield-supply/build.gradle.kts
Normal file
48
data/yield-supply/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.transaction.error
|
||||
|
||||
interface FeeErrorResolver {
|
||||
|
||||
fun resolve(throwable: Throwable): GetFeeError
|
||||
}
|
||||
35
domain/yield-supply/build.gradle.kts
Normal file
35
domain/yield-supply/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue