Updated on 2026-08-14
This commit is contained in:
parent
f748b8539d
commit
f1238f5ef8
20 changed files with 528 additions and 88 deletions
|
|
@ -168,4 +168,16 @@ internal object StakingDomainModule {
|
|||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetConstructedStakingTransactionUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
): GetConstructedStakingTransactionUseCase {
|
||||
return GetConstructedStakingTransactionUseCase(
|
||||
stakingRepository = stakingRepository,
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StakingTransaction, TransactionData.Compiled> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
sealed class StakingApproval {
|
||||
|
||||
data class Needed(val spenderAddress: String) : StakingApproval()
|
||||
|
||||
data object Empty : StakingApproval()
|
||||
}
|
||||
|
|
@ -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<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
|
||||
stakingRepository.constructTransaction(networkId, fee, transactionId)
|
||||
}.mapLeft {
|
||||
stakingErrorResolver.resolve(it)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,19 +22,14 @@ class GetStakingTransactionUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
params: ActionParams,
|
||||
): Either<StakingError, StakingTransaction> {
|
||||
): Either<StakingError, List<StakingTransaction>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<StakingError, Boolean> {
|
||||
operator fun invoke(cryptoCurrency: CryptoCurrency): Either<StakingError, StakingApproval> {
|
||||
return Either
|
||||
.catch { stakingRepository.isApproveNeeded(cryptoCurrency) }
|
||||
.catch { stakingRepository.getStakingApproval(cryptoCurrency) }
|
||||
.mapLeft { stakingErrorResolver.resolve(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StakingTransaction, TransactionData.Compiled>
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -43,4 +43,7 @@ dependencies {
|
|||
/** Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.tangem.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
}
|
||||
|
|
@ -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, TransactionData.Compiled> = 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, BigDecimal> {
|
||||
return Either.catch {
|
||||
transactionRepository.getAllowance(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency as CryptoCurrency.Token,
|
||||
spenderAddress = spenderAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val fee: TransactionFee,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PendingAction>) {
|
||||
private fun getFee(pendingActions: ImmutableList<PendingAction>) {
|
||||
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<PendingAction>,
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue