diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index b4f55832a9..a714d71200 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -168,4 +168,16 @@ internal object StakingDomainModule { stakingErrorResolver = stakingErrorResolver, ) } + + @Provides + @Singleton + fun provideGetConstructedStakingTransactionUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetConstructedStakingTransactionUseCase { + return GetConstructedStakingTransactionUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8af69450c8..73c0236567 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -100,4 +100,18 @@ internal object TransactionDomainModule { ): IsUtxoConsolidationAvailableUseCase { return IsUtxoConsolidationAvailableUseCase(walletManagersFacade) } + + @Provides + @Singleton + fun provideCreateApproveTransactionUseCase( + transactionRepository: TransactionRepository, + ): CreateApprovalTransactionUseCase { + return CreateApprovalTransactionUseCase(transactionRepository) + } + + @Provides + @Singleton + fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase { + return GetAllowanceUseCase(transactionRepository) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 8d89311bdd..9ce502abe3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -3,7 +3,11 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.converters.* @@ -25,6 +29,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata @@ -231,14 +236,26 @@ internal class DefaultStakingRepository( } } - override suspend fun constructTransaction(transactionId: String): StakingTransaction { + override suspend fun constructTransaction( + networkId: String, + fee: Fee, + transactionId: String, + ): Pair { return withContext(dispatchers.io) { val transactionResponse = stakeKitApi.constructTransaction( transactionId = transactionId, body = ConstructTransactionRequestBody(), ) - transactionConverter.convert(transactionResponse.getOrThrow()) + val transaction = transactionConverter.convert(transactionResponse.getOrThrow()) + val unsignedTransaction = transaction.unsignedTransaction ?: error("No unsigned transaction available") + val transactionData = TransactionData.Compiled( + value = getTransactionDataType(networkId, unsignedTransaction), + fee = fee, + status = TransactionStatus.Unconfirmed, + ) + + transaction to transactionData } } @@ -530,10 +547,24 @@ internal class DefaultStakingRepository( } } - override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean { + override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { return when (cryptoCurrency.id.getIntegrationKey()) { - Blockchain.Ethereum.id.plus(Blockchain.Polygon.toCoinId()) -> true - else -> false + Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> { + StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) + } + else -> StakingApproval.Empty + } + } + + private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data { + val blockchain = Blockchain.fromId(networkId) + return when (blockchain) { + Blockchain.Solana, + Blockchain.Cosmos, + -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) + Blockchain.Ethereum, + -> TransactionData.Compiled.Data.RawString(unsignedTransaction) + else -> error("Unsupported blockchain") } } @@ -579,6 +610,8 @@ internal class DefaultStakingRepository( const val NEAR_INTEGRATION_ID = "near-near-native-staking" const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" + // uncomment items as implementation is ready val integrationIdMap = mapOf( Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 56922c5bf0..87f16d07be 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -16,6 +16,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.extensions.hexToBytes import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.models.TransactionType @@ -24,6 +25,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber +import java.math.BigDecimal import java.math.BigInteger import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType @@ -42,15 +44,15 @@ internal class DefaultTransactionRepository( network: Network, txExtras: TransactionExtras?, hash: String?, - ): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, - ) + ) ?: error("Wallet manager not found") - return@withContext walletManager?.createTransactionDataInternal( + return@withContext walletManager.createTransactionDataInternal( amount = amount, fee = fee, memo = memo, @@ -61,6 +63,48 @@ internal class DefaultTransactionRepository( ) } + override suspend fun createApprovalTransaction( + amount: Amount, + fee: Fee, + contractAddress: String, + spenderAddress: String, + userWalletId: UserWalletId, + network: Network, + hash: String?, + ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) ?: error("Wallet manager not found") + val approver = walletManager as? Approver ?: error("Cannot cast to Approver") + + val approvalData = approver.getApproveData( + spenderAddress = spenderAddress, + value = amount, + ) + + val extras = createTransactionDataExtras( + data = approvalData, + network = network, + transactionType = TransactionType.APPROVE, + nonce = null, + gasLimit = null, + ) + + return@withContext createTransaction( + amount = amount, + fee = fee, + memo = null, + destination = contractAddress, + userWalletId = userWalletId, + network = network, + txExtras = extras, + hash = hash, + ) + } + override suspend fun validateTransaction( amount: Amount, fee: Fee?, @@ -141,6 +185,28 @@ internal class DefaultTransactionRepository( } } + override suspend fun getAllowance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency.Token, + spenderAddress: String, + ): BigDecimal { + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network) + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val allowanceResult = (walletManager as? Approver)?.getAllowance( + spenderAddress, + Token( + symbol = blockchain.currency, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) ?: error("Cannot cast to Approver") + + return allowanceResult.fold( + onSuccess = { it }, + onFailure = { error(it) }, + ) + } + private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType { return when (transactionType) { TransactionType.APPROVE -> SdkTransactionType.APPROVE diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index e92bc23b4c..dd1083b53b 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -9,7 +9,6 @@ android { namespace = "com.tangem.domain.staking" } - dependencies { api(projects.domain.staking.models) @@ -20,4 +19,8 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.features.staking.api) + + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt new file mode 100644 index 0000000000..52056c1f11 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.staking.model + +sealed class StakingApproval { + + data class Needed(val spenderAddress: String) : StakingApproval() + + data object Empty : StakingApproval() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt new file mode 100644 index 0000000000..9e31da097f --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository + +class GetConstructedStakingTransactionUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + suspend operator fun invoke( + networkId: String, + fee: Fee, + transactionId: String, + ): Either> = Either.catch { + stakingRepository.constructTransaction(networkId, fee, transactionId) + }.mapLeft { + stakingErrorResolver.resolve(it) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index cfe2c9a254..16ac583d13 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -22,19 +22,14 @@ class GetStakingTransactionUseCase( userWalletId: UserWalletId, network: Network, params: ActionParams, - ): Either { + ): Either> { return Either.catch { val createAction = stakingRepository.createAction(userWalletId, network, params) // workaround, sometimes transaction is not created immediately after actions/enter delay(PATCH_TRANSACTION_REQUEST_DELAY) - val createdTransaction = createAction.transactions - ?.get(createAction.currentStepIndex) - ?: error("No available transaction to patch") - val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) - - patchedTransaction + createAction.transactions ?: error("No available transaction to patch") }.mapLeft { stakingErrorResolver.resolve(it) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt index d183b36a2f..10bc37b3fa 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository @@ -10,9 +11,9 @@ class IsApproveNeededUseCase( private val stakingRepository: StakingRepository, private val stakingErrorResolver: StakingErrorResolver, ) { - operator fun invoke(cryptoCurrency: CryptoCurrency): Either { + operator fun invoke(cryptoCurrency: CryptoCurrency): Either { return Either - .catch { stakingRepository.isApproveNeeded(cryptoCurrency) } + .catch { stakingRepository.getStakingApproval(cryptoCurrency) } .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index b80b7b95d2..5b6604423e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -1,7 +1,12 @@ package com.tangem.domain.staking.repositories +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList @@ -66,7 +71,11 @@ interface StakingRepository { suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate - suspend fun constructTransaction(transactionId: String): StakingTransaction + suspend fun constructTransaction( + networkId: String, + fee: Fee, + transactionId: String, + ): Pair suspend fun submitHash(transactionId: String, transactionHash: String) @@ -77,6 +86,6 @@ interface StakingRepository { /** Returns whether additional staking is possible if there is already active staking */ fun isStakeMoreAvailable(networkId: Network.ID): Boolean - /** Returns whether approve is needed before staking */ - fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean + /** Returns staking approval */ + fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 989a7e9c5d..bbe01868f9 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -43,4 +43,7 @@ dependencies { /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) + testImplementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index b42fe2c3e4..bfc852c858 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -1,7 +1,11 @@ package com.tangem.domain.tokens.repository +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata @@ -209,7 +213,11 @@ class MockStakingRepository : StakingRepository { ) } - override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction( + override suspend fun constructTransaction( + networkId: String, + fee: Fee, + transactionId: String, + ): Pair = StakingTransaction( id = "id", network = NetworkType.SOLANA, status = StakingTransactionStatus.SIGNED, @@ -224,6 +232,9 @@ class MockStakingRepository : StakingRepository { explorerUrl = null, ledgerHwAppId = null, isMessage = false, + ) to TransactionData.Compiled( + value = TransactionData.Compiled.Data.RawString(""), + status = TransactionStatus.Unconfirmed, ) override suspend fun submitHash(transactionId: String, transactionHash: String) { @@ -240,5 +251,5 @@ class MockStakingRepository : StakingRepository { override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true - override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean = true + override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 3fb7cbf0b5..6784c1726f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -1,11 +1,16 @@ package com.tangem.domain.transaction -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.CommonSigner +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal import java.math.BigInteger interface TransactionRepository { @@ -20,7 +25,18 @@ interface TransactionRepository { network: Network, txExtras: TransactionExtras?, hash: String?, - ): TransactionData.Uncompiled? + ): TransactionData.Uncompiled + + @Suppress("LongParameterList") + suspend fun createApprovalTransaction( + amount: Amount, + fee: Fee, + contractAddress: String, + spenderAddress: String, + userWalletId: UserWalletId, + network: Network, + hash: String?, + ): TransactionData.Uncompiled @Suppress("LongParameterList") suspend fun validateTransaction( @@ -49,4 +65,10 @@ interface TransactionRepository { nonce: BigInteger?, gasLimit: BigInteger?, ): TransactionExtras + + suspend fun getAllowance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency.Token, + spenderAddress: String, + ): BigDecimal } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt new file mode 100644 index 0000000000..01cf52d680 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class CreateApprovalTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + @Suppress("LongParameterList") + suspend operator fun invoke( + cryptoCurrency: CryptoCurrency.Token, + userWalletId: UserWalletId, + amount: BigDecimal, + fee: Fee, + contractAddress: String, + spenderAddress: String, + hash: String? = null, + ) = Either.catch { + transactionRepository.createApprovalTransaction( + amount = amount.convertToAmount(cryptoCurrency), + contractAddress = contractAddress, + spenderAddress = spenderAddress, + userWalletId = userWalletId, + network = cryptoCurrency.network, + fee = fee, + hash = hash, + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index ab5172eaf8..87f10ea630 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -26,17 +26,15 @@ class CreateTransactionUseCase( txExtras: TransactionExtras? = null, hash: String? = null, ) = Either.catch { - requireNotNull( - transactionRepository.createTransaction( - amount = amount, - fee = fee, - memo = memo, - destination = destination, - userWalletId = userWalletId, - network = network, - txExtras = txExtras, - hash = hash, - ), - ) { "Failed to create transaction" } + transactionRepository.createTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + txExtras = txExtras, + hash = hash, + ) } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt new file mode 100644 index 0000000000..316a1061ef --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetAllowanceUseCase( + private val transactionRepository: TransactionRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + ): Either { + return Either.catch { + transactionRepository.getAllowance( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency as CryptoCurrency.Token, + spenderAddress = spenderAddress, + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt new file mode 100644 index 0000000000..c3962befb2 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -0,0 +1,45 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.approval + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer + +internal class SetConfirmationStateAssentApprovalTransformer( + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val fee: TransactionFee, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmationState = prevState.confirmationState.copyWrapped(), + bottomSheetConfig = null, + ) + } + + private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { + return if (this is StakingStates.ConfirmationState.Data) { + copy( + innerState = InnerConfirmationStakingState.ASSENT, + feeState = FeeState.Content( + fee = fee.normal, + rate = cryptoCurrencyStatusProvider().value.fiatRate, + isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + appCurrency = appCurrencyProvider(), + isFeeApproximate = false, + ), + validatorState = validatorState.copySealed(isClickable = true), + isPrimaryButtonEnabled = true, + isApprovalNeeded = true, + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index dfac50c551..f3bf4fa6d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData -import com.tangem.common.extensions.hexToBytes +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.models.AmountState @@ -18,15 +18,20 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.* +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet @@ -40,6 +45,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer import com.tangem.utils.Provider @@ -51,6 +57,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -64,6 +71,7 @@ internal class StakingViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getStakingTransactionUseCase: GetStakingTransactionUseCase, + private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -71,6 +79,9 @@ internal class StakingViewModel @Inject constructor( private val submitHashUseCase: SubmitHashUseCase, private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, + private val getAllowanceUseCase: GetAllowanceUseCase, + private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, @@ -101,7 +112,7 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() - private var isApprovalNeeded: Boolean = false + private var stakingApproval: StakingApproval = StakingApproval.Empty private var approvalJobHolder: JobHolder = JobHolder() @@ -121,7 +132,7 @@ internal class StakingViewModel @Inject constructor( } stakingStateRouter.onNextClick() if (isAssentState()) { - estimateGas(pendingActions) + getFee(pendingActions) } } @@ -139,6 +150,9 @@ internal class StakingViewModel @Inject constructor( value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") + val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") + val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") val stakingTransaction = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -146,8 +160,7 @@ internal class StakingViewModel @Inject constructor( params = ActionParams( actionCommonType = value.actionType, integrationId = yield.id, - amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided"), + amount = amountValue, address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address"), validatorAddress = validatorState.chosenValidator.address, @@ -159,19 +172,27 @@ internal class StakingViewModel @Inject constructor( error(it) } - stakingTransaction.unsignedTransaction?.let { - sendStakingTransaction( - transactionId = stakingTransaction.id, - gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"), - txData = TransactionData.Compiled(value = it.hexToBytes()), - pendingActionList = confirmationState.pendingActions, - ) - } ?: error("No unsigned transaction available") + stakingTransaction + .filterNot { it.type == StakingTransactionType.APPROVAL } + .forEach { transaction -> + val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase( + networkId = cryptoCurrencyStatus.currency.network.id.value, + fee = fee, + transactionId = transaction.id, + ).getOrNull() ?: error("No constructed transaction") + + sendStakingTransaction( + transactionId = constructedTransaction.id, + gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), + txData = transactionData, + pendingActionList = confirmationState.pendingActions, + ) + } } } } - private fun estimateGas(pendingActions: ImmutableList) { + private fun getFee(pendingActions: ImmutableList) { viewModelScope.launch { stateController.update( SetConfirmationStateLoadingTransformer( @@ -184,38 +205,99 @@ internal class StakingViewModel @Inject constructor( val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") - val pendingAction = pendingActions.firstOrNull() - val stakingGasEstimate = estimateGasUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - params = ActionParams( - actionCommonType = value.actionType, - integrationId = yield.id, - amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided"), - address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address"), - validatorAddress = validatorState.chosenValidator.address, - token = yield.token, - passthrough = pendingAction?.passthrough, - type = pendingAction?.type, - ), - ).getOrElse { - stateController.update(AddStakingErrorTransformer(it)) - return@launch - } + val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value + ?: error("No available address") + val validatorAddress = validatorState.chosenValidator.address - stateController.update( - SetConfirmationStateAssentTransformer( - appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - stakingGasEstimate = stakingGasEstimate, - pendingActionList = pendingActions, - ), - ) + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + ) + } else { + estimateGas( + pendingActions = pendingActions, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } else { + estimateGas( + pendingActions = pendingActions, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } } } + private suspend fun estimateGas( + pendingActions: ImmutableList, + amount: BigDecimal, + sourceAddress: String, + validatorAddress: String, + ) { + val pendingAction = pendingActions.firstOrNull() + val stakingGasEstimate = estimateGasUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + params = ActionParams( + actionCommonType = value.actionType, + integrationId = yield.id, + amount = amount, + address = sourceAddress, + validatorAddress = validatorAddress, + token = yield.token, + passthrough = pendingAction?.passthrough, + type = pendingAction?.type, + ), + ).getOrElse { + stateController.update(AddStakingErrorTransformer(it)) + return + } + + stateController.update( + SetConfirmationStateAssentTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = stakingGasEstimate, + pendingActionList = pendingActions, + ), + ) + } + + private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { + val approvalFee = getFeeUseCase( + amount = amount, + destination = validatorAddress, + userWallet = userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).getOrElse { + // TODO staking error + return + } + + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + fee = approvalFee, + ), + ) + } + override fun onPrevClick() { stakingStateRouter.onPrevClick() } @@ -286,7 +368,60 @@ internal class StakingViewModel @Inject constructor( }, ) - // todo staking polygon + val tokenCryptoCurrency = + cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: error("No token currency") + val amountValue = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") + val approval = stakingApproval as? StakingApproval.Needed ?: error("No staking approve spender address") + + val approvalTransaction = createApprovalTransactionUseCase( + amount = amountValue, + contractAddress = tokenCryptoCurrency.contractAddress, + spenderAddress = approval.spenderAddress, + fee = fee, + cryptoCurrency = tokenCryptoCurrency, + userWalletId = userWalletId, + ).fold( + ifLeft = { error -> + Timber.e(error.toString()) + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + fee = TransactionFee.Single(fee), + ), + ) + // TODO staking error + return@launch + }, + ifRight = { it }, + ) + + sendTransactionUseCase( + txData = approvalTransaction, + userWallet = userWallet, + network = tokenCryptoCurrency.network, + ).fold( + ifLeft = { error -> + Timber.e(error.toString()) + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + fee = TransactionFee.Single(fee), + ), + ) + // TODO staking error + }, + ifRight = { + getFee(confirmationState.pendingActions) + stateController.update(DismissBottomSheetStateTransformer()) + }, + ) }.saveIn(approvalJobHolder) } @@ -319,7 +454,7 @@ internal class StakingViewModel @Inject constructor( } private fun setupApprovalNeeded() { - isApprovalNeeded = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { false } + stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { StakingApproval.Empty } } private fun subscribeOnCurrencyStatusUpdates() { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index edd4767043..48724f9b77 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -7,7 +7,6 @@ import arrow.core.raise.either import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.Result import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse @@ -362,10 +361,10 @@ internal class DefaultSwapRepository @Inject constructor( ), ) ?: error("Cannot cast to Approver") - return when (result) { - is Result.Success -> result.data - is Result.Failure -> error(result.error) - } + return result.fold( + onSuccess = { it }, + onFailure = { error(it) }, + ) } override suspend fun getApproveData( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e7873d004f..6201894fe4 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-733" +tangemBlockchainSdk = "develop-734" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^