From 7d599256d1220cb3d81db9ef3cb3f006901d37f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Dec 2025 15:05:35 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../MockP2PEthPoolAccountResponseFactory.kt | 4 +- .../datasource/api/ethpool/P2PEthPoolApi.kt | 23 +- .../request/P2PEthPoolDepositRequest.kt | 19 -- .../request/P2PEthPoolTransactionRequest.kt | 23 ++ .../request/P2PEthPoolUnstakeRequest.kt | 20 -- .../request/P2PEthPoolWithdrawRequest.kt | 15 -- .../response/P2PEthPoolAccountResponse.kt | 6 +- .../response/P2PEthPoolDepositResponse.kt | 22 -- ...se.kt => P2PEthPoolTransactionResponse.kt} | 14 +- .../response/P2PEthPoolUnstakeResponse.kt | 22 -- .../com/tangem/datasource/di/NetworkModule.kt | 8 + .../staking/DefaultP2PEthPoolRepository.kt | 224 ++++++++---------- ...t => P2PEthPoolStakingAccountConverter.kt} | 29 ++- .../P2PEthPoolStakingBalanceConverter.kt | 46 +--- .../staking/store/BaseStakingBalancesStore.kt | 35 +++ .../staking/store/P2PEthPoolBalancesStore.kt | 24 +- .../staking/store/StakingBalancesStore.kt | 26 +- .../staking/P2PEthPoolStakingAccount.kt | 2 +- .../staking/P2PEthPoolStakingAccountExt.kt | 126 ++++++---- .../model/ethpool/P2PEthPoolAccount.kt | 44 ---- .../staking/model/P2PEthPoolIntegration.kt | 13 +- .../repositories/P2PEthPoolRepository.kt | 22 +- .../StakingBalanceEntryConverter.kt | 12 +- .../converters/YieldBalancesConverter.kt | 11 +- .../helpers/P2PEthPoolTransactionCreator.kt | 13 +- .../TokenDetailsStakingInfoConverter.kt | 6 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 11 +- 27 files changed, 366 insertions(+), 454 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt rename core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/{P2PEthPoolWithdrawResponse.kt => P2PEthPoolTransactionResponse.kt} (57%) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt rename data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/{P2PEthPoolAccountConverter.kt => P2PEthPoolStakingAccountConverter.kt} (54%) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/store/BaseStakingBalancesStore.kt delete mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolAccount.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt index 2eafba7703..4dc0b16d03 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt @@ -35,7 +35,7 @@ object MockP2PEthPoolAccountResponseFactory { availableToUnstake = stakedAmount, availableToWithdraw = BigDecimal.ZERO, exitQueue = P2PEthPoolExitQueueDTO( - total = 0.0, + total = BigDecimal.ZERO, requests = emptyList(), ), ) @@ -55,7 +55,7 @@ object MockP2PEthPoolAccountResponseFactory { availableToUnstake = BigDecimal.ZERO, availableToWithdraw = BigDecimal.ZERO, exitQueue = P2PEthPoolExitQueueDTO( - total = 0.0, + total = BigDecimal.ZERO, requests = emptyList(), ), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt index cf3ab2f3e5..99de156115 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt @@ -2,9 +2,7 @@ package com.tangem.datasource.api.ethpool import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest -import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest -import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest -import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest +import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest import com.tangem.datasource.api.ethpool.models.response.* import retrofit2.http.* @@ -31,13 +29,13 @@ interface P2PEthPoolApi { * Create unsigned transaction for depositing ETH into a vault. * * @param network Ethereum pool network: "mainnet" or "hoodi" - * @param body Deposit parameters (delegator address, vault address, amount) + * @param body Transaction parameters (delegator address, vault address, amount) */ @POST("api/v1/staking/pool/{network}/staking/deposit") suspend fun createDepositTransaction( @Path("network") network: String, - @Body body: P2PEthPoolDepositRequest, - ): ApiResponse> + @Body body: P2PEthPoolTransactionRequest, + ): ApiResponse> /** * Prepare unstake transaction @@ -45,13 +43,13 @@ interface P2PEthPoolApi { * Create unsigned transaction to initiate unstaking process. * * @param network Ethereum pool network: "mainnet" or "hoodi" - * @param body Unstake parameters (staker public key, stake transaction hash) + * @param body Transaction parameters (delegator address, vault address, amount) */ @POST("api/v1/staking/pool/{network}/staking/unstake") suspend fun createUnstakeTransaction( @Path("network") network: String, - @Body body: P2PEthPoolUnstakeRequest, - ): ApiResponse> + @Body body: P2PEthPoolTransactionRequest, + ): ApiResponse> /** * Prepare withdrawal transaction @@ -59,13 +57,13 @@ interface P2PEthPoolApi { * Create unsigned transaction to withdraw available funds from exit queue. * * @param network Ethereum pool network: "mainnet" or "hoodi" - * @param body Withdrawal parameters (staker address) + * @param body Transaction parameters (delegator address, vault address, amount) */ @POST("api/v1/staking/pool/{network}/staking/withdraw") suspend fun createWithdrawTransaction( @Path("network") network: String, - @Body body: P2PEthPoolWithdrawRequest, - ): ApiResponse> + @Body body: P2PEthPoolTransactionRequest, + ): ApiResponse> /** * Broadcast signed transaction @@ -107,6 +105,7 @@ interface P2PEthPoolApi { * @param vaultAddress Ethereum address of the vault * @param period Optional period filter (30, 60, or 90 days) */ + // TODO p2p not used, consider removing this method @GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards") suspend fun getRewards( @Path("network") network: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt deleted file mode 100644 index e4b3200add..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.datasource.api.ethpool.models.request - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -/** - * Request body for creating deposit transaction - * - * Used in: POST /api/v1/staking/pool/{network}/staking/deposit - */ -@JsonClass(generateAdapter = true) -data class P2PEthPoolDepositRequest( - @Json(name = "delegatorAddress") - val delegatorAddress: String, - @Json(name = "vaultAddress") - val vaultAddress: String, - @Json(name = "amount") - val amount: Double, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt new file mode 100644 index 0000000000..500fc969be --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.ethpool.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +/** + * Unified request body for creating staking transactions (deposit, unstake, withdraw) + * + * Used in: + * - POST /api/v1/staking/pool/{network}/staking/deposit + * - POST /api/v1/staking/pool/{network}/staking/unstake + * - POST /api/v1/staking/pool/{network}/staking/withdraw + */ +@JsonClass(generateAdapter = true) +data class P2PEthPoolTransactionRequest( + @Json(name = "delegatorAddress") + val delegatorAddress: String, + @Json(name = "vaultAddress") + val vaultAddress: String, + @Json(name = "amount") + val amount: BigDecimal, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt deleted file mode 100644 index 0e56cd5112..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.api.ethpool.models.request - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -/** - * Request body for creating unstake transaction - * - * Used in: POST /api/v1/staking/pool/{network}/staking/unstake - * - * Note: Documentation seems to contain Bitcoin-related fields (possibly copy-paste error). - * Using as-is per specification. - */ -@JsonClass(generateAdapter = true) -data class P2PEthPoolUnstakeRequest( - @Json(name = "stakerPublicKey") - val stakerPublicKey: String, - @Json(name = "stakeTransactionHash") - val stakeTransactionHash: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt deleted file mode 100644 index d0a7aac5cf..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.datasource.api.ethpool.models.request - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -/** - * Request body for creating withdrawal transaction - * - * Used in: POST /api/v1/staking/pool/{network}/staking/withdraw - */ -@JsonClass(generateAdapter = true) -data class P2PEthPoolWithdrawRequest( - @Json(name = "stakerAddress") - val stakerAddress: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt index 526c3c7d16..647750c927 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt @@ -34,7 +34,7 @@ data class P2PEthPoolStakeDTO( @JsonClass(generateAdapter = true) data class P2PEthPoolExitQueueDTO( @Json(name = "total") - val total: Double, + val total: BigDecimal, @Json(name = "requests") val requests: List, ) @@ -44,11 +44,11 @@ data class P2PEthPoolExitRequestDTO( @Json(name = "ticket") val ticket: String, @Json(name = "totalAssets") - val totalAssets: Double, + val totalAssets: BigDecimal, @Json(name = "timestamp") val timestamp: Long, @Json(name = "withdrawalTimestamp") - val withdrawalTimestamp: Long, + val withdrawalTimestamp: Long?, @Json(name = "isClaimable") val isClaimable: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt deleted file mode 100644 index ed922bf827..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.datasource.api.ethpool.models.response - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.joda.time.DateTime - -/** - * Response for POST /api/v1/staking/pool/{network}/staking/deposit - */ -@JsonClass(generateAdapter = true) -data class P2PEthPoolDepositResponse( - @Json(name = "amount") - val amount: Double, - @Json(name = "vaultAddress") - val vaultAddress: String, - @Json(name = "delegatorAddress") - val delegatorAddress: String, - @Json(name = "unsignedTransaction") - val unsignedTransaction: P2PEthPoolUnsignedTxDTO, - @Json(name = "createdAt") - val createdAt: DateTime, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolWithdrawResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolTransactionResponse.kt similarity index 57% rename from core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolWithdrawResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolTransactionResponse.kt index 1d5ff5bb4a..9c44476b5c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolWithdrawResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolTransactionResponse.kt @@ -3,14 +3,20 @@ package com.tangem.datasource.api.ethpool.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.joda.time.DateTime +import java.math.BigDecimal /** - * Response for POST /api/v1/staking/pool/{network}/staking/withdraw + * Unified response for staking transactions (deposit, unstake, withdraw) + * + * Response for: + * - POST /api/v1/staking/pool/{network}/staking/deposit + * - POST /api/v1/staking/pool/{network}/staking/unstake + * - POST /api/v1/staking/pool/{network}/staking/withdraw */ @JsonClass(generateAdapter = true) -data class P2PEthPoolWithdrawResponse( +data class P2PEthPoolTransactionResponse( @Json(name = "amount") - val amount: Double, + val amount: BigDecimal, @Json(name = "vaultAddress") val vaultAddress: String, @Json(name = "delegatorAddress") @@ -20,5 +26,5 @@ data class P2PEthPoolWithdrawResponse( @Json(name = "createdAt") val createdAt: DateTime, @Json(name = "tickets") - val tickets: List, + val tickets: List? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt deleted file mode 100644 index a3e2391339..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.datasource.api.ethpool.models.response - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -/** - * Response for POST /api/v1/staking/pool/{network}/staking/unstake - * - * Note: Contains Bitcoin-related fields (likely documentation error). - * Using as-is per specification. - */ -@JsonClass(generateAdapter = true) -data class P2PEthPoolUnstakeResponse( - @Json(name = "stakerPublicKey") - val stakerPublicKey: String, - @Json(name = "stakeTransactionHash") - val stakeTransactionHash: String, - @Json(name = "unstakeTransactionHex") - val unstakeTransactionHex: String, // unsigned - @Json(name = "unstakeFee") - val unstakeFee: Double, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 511a2dab3d..d3f942e798 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -38,6 +38,8 @@ internal object NetworkModule { private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L + private const val P2P_ETH_POOL_API_TIMEOUT_SECONDS = 60L + @Provides @Singleton fun provideApiConfigManager( @@ -82,6 +84,12 @@ internal object NetworkModule { return retrofitApiBuilder.build( apiConfigId = ApiConfig.ID.P2PEthPool, applyTimeoutAnnotations = false, + timeouts = Timeouts( + callTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS, + connectTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS, + readTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS, + writeTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS, + ), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index 46eae8ecb3..4ffe515037 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -2,21 +2,32 @@ package com.tangem.data.staking import arrow.core.Either import arrow.core.getOrElse +import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensure -import com.tangem.data.staking.converters.ethpool.* +import com.tangem.data.staking.converters.ethpool.P2PEthPoolBroadcastResultConverter +import com.tangem.data.staking.converters.ethpool.P2PEthPoolErrorConverter +import com.tangem.data.staking.converters.ethpool.P2PEthPoolRewardConverter +import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingAccountConverter +import com.tangem.data.staking.converters.ethpool.P2PEthPoolUnsignedTxConverter +import com.tangem.data.staking.converters.ethpool.P2PEthPoolVaultConverter import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest -import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest -import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest -import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest +import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolResponse +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolTransactionResponse import com.tangem.datasource.local.token.P2PEthPoolVaultsStore +import com.tangem.domain.models.staking.P2PEthPoolStakingAccount import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingOption -import com.tangem.domain.staking.model.ethpool.* -import com.tangem.domain.staking.repositories.P2PEthPoolRepository +import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult +import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork +import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward +import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -36,11 +47,30 @@ internal class DefaultP2PEthPoolRepository( ) : P2PEthPoolRepository { private val vaultConverter = P2PEthPoolVaultConverter - private val accountInfoConverter = P2PEthPoolAccountConverter + private val accountConverter = P2PEthPoolStakingAccountConverter private val rewardConverter = P2PEthPoolRewardConverter private val broadcastResultConverter = P2PEthPoolBroadcastResultConverter private val errorConverter = P2PEthPoolErrorConverter + /** + * Handles P2PEthPool API response with error checking and result extraction. + * Reduces duplication across all API call methods. + */ + private inline fun Raise.handleApiResponse( + response: ApiResponse>, + transform: (T) -> R, + ): R = when (response) { + is ApiResponse.Success -> { + val data = response.data + ensure(data.error == null) { + errorConverter.convertFromErrorDetails(requireNotNull(data.error)) + } + val result = requireNotNull(data.result) { "Result is null in successful response" } + transform(result) + } + is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) + } + override suspend fun fetchVaults(network: P2PEthPoolNetwork) { val vaults = if (stakingFeatureToggles.isEthStakingEnabled) { getVaults(network).getOrElse { error -> @@ -56,16 +86,8 @@ internal class DefaultP2PEthPoolRepository( override suspend fun getVaults(network: P2PEthPoolNetwork): Either> = either { withContext(dispatchers.io) { - when (val response = p2pEthPoolApi.getVaults(network.value)) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - result.vaults.map { vaultConverter.convert(it) } - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) + handleApiResponse(p2pEthPoolApi.getVaults(network.value)) { result -> + result.vaults.map { vaultConverter.convert(it) } } } } @@ -75,81 +97,60 @@ internal class DefaultP2PEthPoolRepository( delegatorAddress: String, vaultAddress: String, amount: String, - ): Either = either { - withContext(dispatchers.io) { - val requestBody = P2PEthPoolDepositRequest( - delegatorAddress = delegatorAddress, - vaultAddress = vaultAddress, - amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")), - ) - val response = p2pEthPoolApi.createDepositTransaction(network.value, requestBody) - when (response) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - P2PEthPoolUnsignedTxConverter.convert(result.unsignedTransaction) - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) - } - } - } + ): Either = createStakingTransaction( + network = network, + delegatorAddress = delegatorAddress, + vaultAddress = vaultAddress, + amount = amount, + apiCall = p2pEthPoolApi::createDepositTransaction, + ) override suspend fun createUnstakeTransaction( network: P2PEthPoolNetwork, - stakerPublicKey: String, - stakeTransactionHash: String, - ): Either = either { - withContext(dispatchers.io) { - val requestBody = P2PEthPoolUnstakeRequest( - stakerPublicKey = stakerPublicKey, - stakeTransactionHash = stakeTransactionHash, - ) - val response = p2pEthPoolApi.createUnstakeTransaction(network.value, requestBody) - when (response) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - // Note: API returns only hex string for unstake, not full transaction structure - P2PEthPoolUnsignedTx( - serializeTx = result.unstakeTransactionHex, - to = "", // Will be parsed from hex by wallet - data = result.unstakeTransactionHex, - value = java.math.BigDecimal.ZERO, - nonce = 0, - chainId = network.chainId, - gasLimit = java.math.BigDecimal.ZERO, - maxFeePerGas = java.math.BigDecimal.ZERO, - maxPriorityFeePerGas = java.math.BigDecimal.ZERO, - ) - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) - } - } - } + delegatorAddress: String, + vaultAddress: String, + amount: String, + ): Either = createStakingTransaction( + network = network, + delegatorAddress = delegatorAddress, + vaultAddress = vaultAddress, + amount = amount, + apiCall = p2pEthPoolApi::createUnstakeTransaction, + ) override suspend fun createWithdrawTransaction( network: P2PEthPoolNetwork, - stakerAddress: String, + delegatorAddress: String, + vaultAddress: String, + amount: String, + ): Either = createStakingTransaction( + network = network, + delegatorAddress = delegatorAddress, + vaultAddress = vaultAddress, + amount = amount, + apiCall = p2pEthPoolApi::createWithdrawTransaction, + ) + + private suspend fun createStakingTransaction( + network: P2PEthPoolNetwork, + delegatorAddress: String, + vaultAddress: String, + amount: String, + apiCall: + suspend ( + String, + P2PEthPoolTransactionRequest, + ) -> ApiResponse>, ): Either = either { withContext(dispatchers.io) { - val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress) - val response = p2pEthPoolApi.createWithdrawTransaction(network.value, requestBody) - when (response) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - P2PEthPoolUnsignedTxConverter.convert(result.unsignedTransaction) - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) + val requestBody = P2PEthPoolTransactionRequest( + delegatorAddress = delegatorAddress, + vaultAddress = vaultAddress, + amount = amount.toBigDecimalOrNull() + ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")), + ) + handleApiResponse(apiCall(network.value, requestBody)) { result -> + P2PEthPoolUnsignedTxConverter.convert(result.unsignedTransaction) } } } @@ -160,17 +161,8 @@ internal class DefaultP2PEthPoolRepository( ): Either = either { withContext(dispatchers.io) { val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction) - val response = p2pEthPoolApi.broadcastTransaction(network.value, requestBody) - when (response) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - broadcastResultConverter.convert(result) - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) + handleApiResponse(p2pEthPoolApi.broadcastTransaction(network.value, requestBody)) { result -> + broadcastResultConverter.convert(result) } } } @@ -179,19 +171,12 @@ internal class DefaultP2PEthPoolRepository( network: P2PEthPoolNetwork, delegatorAddress: String, vaultAddress: String, - ): Either = either { + ): Either = either { withContext(dispatchers.io) { - val response = p2pEthPoolApi.getAccountInfo(network.value, delegatorAddress, vaultAddress) - when (response) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - accountInfoConverter.convert(result) - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) + handleApiResponse( + p2pEthPoolApi.getAccountInfo(network.value, delegatorAddress, vaultAddress), + ) { result -> + accountConverter.convert(result) } } } @@ -203,22 +188,15 @@ internal class DefaultP2PEthPoolRepository( period: Int?, ): Either> = either { withContext(dispatchers.io) { - val response = p2pEthPoolApi.getRewards( - network = network.value, - delegatorAddress = delegatorAddress, - vaultAddress = vaultAddress, - period = period, - ) - when (response) { - is ApiResponse.Success -> { - val data = response.data - ensure(data.error == null) { - errorConverter.convertFromErrorDetails(requireNotNull(data.error)) - } - val result = requireNotNull(data.result) { "Result is null in successful response" } - result.rewards.map { rewardConverter.convert(it) } - } - is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause)) + handleApiResponse( + p2pEthPoolApi.getRewards( + network = network.value, + delegatorAddress = delegatorAddress, + vaultAddress = vaultAddress, + period = period, + ), + ) { result -> + result.rewards.map { rewardConverter.convert(it) } } } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolAccountConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolStakingAccountConverter.kt similarity index 54% rename from data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolAccountConverter.kt rename to data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolStakingAccountConverter.kt index 3fd0209523..5661bf65ad 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolAccountConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolStakingAccountConverter.kt @@ -4,17 +4,20 @@ import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountRespon import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitQueueDTO import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitRequestDTO import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO -import com.tangem.domain.staking.model.ethpool.* +import com.tangem.domain.models.staking.P2PEthPoolExitQueue +import com.tangem.domain.models.staking.P2PEthPoolExitRequest +import com.tangem.domain.models.staking.P2PEthPoolStake +import com.tangem.domain.models.staking.P2PEthPoolStakingAccount import com.tangem.utils.converter.Converter -import org.joda.time.Instant +import kotlinx.datetime.Instant /** - * Converter from P2PEthPool Account Info Response to Domain model + * Converts P2PEthPool Account API response to domain [P2PEthPoolStakingAccount]. */ -internal object P2PEthPoolAccountConverter : Converter { +internal object P2PEthPoolStakingAccountConverter : Converter { - override fun convert(value: P2PEthPoolAccountResponse): P2PEthPoolAccount { - return P2PEthPoolAccount( + override fun convert(value: P2PEthPoolAccountResponse): P2PEthPoolStakingAccount { + return P2PEthPoolStakingAccount( delegatorAddress = value.delegatorAddress, vaultAddress = value.vaultAddress, stake = convertStake(value.stake), @@ -24,26 +27,26 @@ internal object P2PEthPoolAccountConverter : Converter BigDecimal.ZERO || account.exitQueue.total > BigDecimal.ZERO || @@ -45,28 +39,4 @@ internal object P2PEthPoolStakingBalanceConverter { ) } } - - private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake { - return P2PEthPoolStake( - assets = dto.assets, - totalEarnedAssets = dto.totalEarnedAssets, - ) - } - - private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue { - return P2PEthPoolExitQueue( - total = dto.total.toBigDecimal(), - requests = dto.requests.map(::convertExitRequest), - ) - } - - private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest { - return P2PEthPoolExitRequest( - ticket = dto.ticket, - totalAssets = dto.totalAssets.toBigDecimal(), - timestamp = Instant.fromEpochSeconds(dto.timestamp), - withdrawalTimestamp = Instant.fromEpochSeconds(dto.withdrawalTimestamp), - isClaimable = dto.isClaimable, - ) - } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/BaseStakingBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/BaseStakingBalancesStore.kt new file mode 100644 index 0000000000..a9763e801e --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/BaseStakingBalancesStore.kt @@ -0,0 +1,35 @@ +package com.tangem.data.staking.store + +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Base interface for staking balances stores. + * + * Defines common read/query operations shared by all staking provider stores. + */ +interface BaseStakingBalancesStore { + + /** Get flow of staking balances for a wallet */ + fun get(userWalletId: UserWalletId): Flow> + + /** Get a single staking balance synchronously */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? + + /** Get all staking balances for a wallet synchronously */ + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + + /** Refresh a single staking balance from cache */ + suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) + + /** Refresh multiple staking balances from cache */ + suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) + + /** Store error state for staking balances */ + suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) + + /** Clear staking balances */ + suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/P2PEthPoolBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/P2PEthPoolBalancesStore.kt index 06d300af6e..ecd7442ce6 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/P2PEthPoolBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/P2PEthPoolBalancesStore.kt @@ -1,31 +1,19 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse -import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow /** - * Store for P2PEthPool staking balances + * Store for P2PEthPool staking balances. + * + * Extends [BaseStakingBalancesStore] with P2PEthPool-specific storage operations. */ -interface P2PEthPoolBalancesStore { - - fun get(userWalletId: UserWalletId): Flow> - - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? - - suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? - - suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) - - suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) +interface P2PEthPoolBalancesStore : BaseStakingBalancesStore { + /** Store actual P2PEthPool account balances */ suspend fun storeActual(userWalletId: UserWalletId, values: Set) + /** Store empty state for accounts with no active positions */ suspend fun storeEmpty(userWalletId: UserWalletId, stakingIds: Set) - - suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) - - suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt index 5d730dd5b7..aff991ff47 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt @@ -1,27 +1,15 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -/** Store of StakeKit [StakingBalance] */ -interface StakingBalancesStore { - - fun get(userWalletId: UserWalletId): Flow> - - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? - - suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? - - suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) - - suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) +/** + * Store for StakeKit staking balances. + * + * Extends [BaseStakingBalancesStore] with StakeKit-specific storage operations. + */ +interface StakingBalancesStore : BaseStakingBalancesStore { + /** Store actual StakeKit yield balances */ suspend fun storeActual(userWalletId: UserWalletId, values: Set) - - suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) - - suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccount.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccount.kt index d196a17cbc..a6d8489e14 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccount.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccount.kt @@ -32,6 +32,6 @@ data class P2PEthPoolExitRequest( val ticket: String, val totalAssets: SerializedBigDecimal, val timestamp: Instant, - val withdrawalTimestamp: Instant, + val withdrawalTimestamp: Instant?, val isClaimable: Boolean, ) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccountExt.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccountExt.kt index c189a5465f..69db40bd72 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccountExt.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PEthPoolStakingAccountExt.kt @@ -5,60 +5,82 @@ import java.math.BigDecimal fun P2PEthPoolStakingAccount.toStakingBalanceEntries(vaultName: String? = null): List { return buildList { if (stake.assets > BigDecimal.ZERO) { - add( - StakingBalanceEntry( - id = vaultAddress, - type = StakingEntryType.STAKED, - amount = stake.assets, - validator = ValidatorInfo(address = vaultAddress, name = vaultName), - date = null, - actions = StakingEntryActions.P2PEthPool( - ticket = null, - estimatedWithdrawalDate = null, - isClaimable = false, - ), - isPending = false, - rawCurrencyId = null, - ), - ) + add(createStakedEntry(vaultAddress, stake.assets, vaultName)) } - - exitQueue.requests.forEach { request -> - add( - StakingBalanceEntry( - id = "${vaultAddress}_${request.ticket}", - type = StakingEntryType.UNSTAKING, - amount = request.totalAssets, - validator = ValidatorInfo(address = vaultAddress, name = vaultName), - date = request.withdrawalTimestamp, - actions = StakingEntryActions.P2PEthPool( - ticket = request.ticket, - estimatedWithdrawalDate = request.withdrawalTimestamp, - isClaimable = request.isClaimable, - ), - isPending = false, - rawCurrencyId = null, - ), - ) - } - + exitQueue.requests.filter { !it.isClaimable }.forEach { add(createUnstakingEntry(vaultAddress, it, vaultName)) } if (availableToWithdraw > BigDecimal.ZERO) { - add( - StakingBalanceEntry( - id = "${vaultAddress}_withdrawable", - type = StakingEntryType.WITHDRAWABLE, - amount = availableToWithdraw, - validator = ValidatorInfo(address = vaultAddress, name = vaultName), - date = null, - actions = StakingEntryActions.P2PEthPool( - ticket = null, - estimatedWithdrawalDate = null, - isClaimable = true, - ), - isPending = false, - rawCurrencyId = null, - ), - ) + add(createWithdrawableEntry(vaultAddress, availableToWithdraw, vaultName)) + } + if (stake.totalEarnedAssets > BigDecimal.ZERO) { + add(createRewardsEntry(vaultAddress, stake.totalEarnedAssets, vaultName)) } } +} + +private fun createStakedEntry(vaultAddress: String, amount: BigDecimal, vaultName: String?): StakingBalanceEntry { + return StakingBalanceEntry( + id = vaultAddress, + type = StakingEntryType.STAKED, + amount = amount, + validator = ValidatorInfo(address = vaultAddress, name = vaultName), + date = null, + actions = StakingEntryActions.P2PEthPool(ticket = null, estimatedWithdrawalDate = null, isClaimable = false), + isPending = false, + rawCurrencyId = null, + ) +} + +private fun createUnstakingEntry( + vaultAddress: String, + request: P2PEthPoolExitRequest, + vaultName: String?, +): StakingBalanceEntry { + return StakingBalanceEntry( + id = "${vaultAddress}_${request.ticket}", + type = StakingEntryType.UNSTAKING, + amount = request.totalAssets, + validator = ValidatorInfo(address = vaultAddress, name = vaultName), + date = request.withdrawalTimestamp, + actions = StakingEntryActions.P2PEthPool( + ticket = request.ticket, + estimatedWithdrawalDate = request.withdrawalTimestamp, + isClaimable = false, + ), + isPending = false, + rawCurrencyId = null, + ) +} + +private fun createWithdrawableEntry( + vaultAddress: String, + amount: BigDecimal, + vaultName: String?, +): StakingBalanceEntry { + return StakingBalanceEntry( + id = "${vaultAddress}_withdrawable", + type = StakingEntryType.WITHDRAWABLE, + amount = amount, + validator = ValidatorInfo(address = vaultAddress, name = vaultName), + date = null, + actions = StakingEntryActions.P2PEthPool( + ticket = null, + estimatedWithdrawalDate = null, + isClaimable = true, + ), + isPending = false, + rawCurrencyId = null, + ) +} + +private fun createRewardsEntry(vaultAddress: String, amount: BigDecimal, vaultName: String?): StakingBalanceEntry { + return StakingBalanceEntry( + id = "${vaultAddress}_rewards", + type = StakingEntryType.REWARDS, + amount = amount, + validator = ValidatorInfo(address = vaultAddress, name = vaultName), + date = null, + actions = StakingEntryActions.P2PEthPool(ticket = null, estimatedWithdrawalDate = null, isClaimable = false), + isPending = false, + rawCurrencyId = null, + ) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolAccount.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolAccount.kt deleted file mode 100644 index 56476007cb..0000000000 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolAccount.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.domain.staking.model.ethpool - -import com.tangem.domain.models.serialization.SerializedBigDecimal -import org.joda.time.Instant - -/** - * Account staking information - * Contains detailed balance and exit queue information - */ -data class P2PEthPoolAccount( - val delegatorAddress: String, - val vaultAddress: String, - val stake: P2PEthPoolStake, - val availableToUnstake: SerializedBigDecimal, - val availableToWithdraw: SerializedBigDecimal, - val exitQueue: P2PEthPoolExitQueue, -) - -/** - * Current stake information - */ -data class P2PEthPoolStake( - val assets: SerializedBigDecimal, - val totalEarnedAssets: SerializedBigDecimal, -) - -/** - * Exit queue information - */ -data class P2PEthPoolExitQueue( - val total: SerializedBigDecimal, - val requests: List, -) - -/** - * Individual exit request in the queue - */ -data class P2PEthPoolExitRequest( - val ticket: String, - val totalAssets: SerializedBigDecimal, - val timestamp: Instant, - val withdrawalTimestamp: Instant, - val isClaimable: Boolean, -) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt index 8842e9281c..a47b643544 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt @@ -15,7 +15,7 @@ import java.math.BigDecimal */ class P2PEthPoolIntegration( override val integrationId: StakingIntegrationID, - vaults: List, + private val vaults: List, ) : StakingIntegration { // Basic @@ -46,7 +46,7 @@ class P2PEthPoolIntegration( amountRequirement = StakingAmountRequirement( isRequired = true, minimum = DEFAULT_MINIMUM_STAKE, - maximum = null, + maximum = calculateMaximumStakeAmount(), ), isPartialAmountDisabled = false, ) @@ -75,6 +75,15 @@ class P2PEthPoolIntegration( override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken = token + private fun calculateMaximumStakeAmount(): BigDecimal? { + return vaults + .mapNotNull { vault -> + val availableCapacity = vault.capacity - vault.totalAssets + if (availableCapacity > BigDecimal.ZERO) availableCapacity else null + } + .maxOrNull() + } + companion object { private const val MIN_COOLDOWN_DAYS = 1 private const val MAX_COOLDOWN_DAYS = 4 diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt index 04892f5414..7b4e4568e2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt @@ -1,8 +1,8 @@ package com.tangem.domain.staking.repositories import arrow.core.Either +import com.tangem.domain.models.staking.P2PEthPoolStakingAccount import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward @@ -54,14 +54,16 @@ interface P2PEthPoolRepository { * to withdraw the funds. * * @param network P2PEthPool network (MAINNET or TESTNET) - * @param stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology) - * @param stakeTransactionHash Original stake transaction hash + * @param delegatorAddress User's wallet address + * @param vaultAddress Vault contract address + * @param amount Amount of ETH to unstake * @return Either error or unsigned transaction */ suspend fun createUnstakeTransaction( network: P2PEthPoolNetwork, - stakerPublicKey: String, - stakeTransactionHash: String, + delegatorAddress: String, + vaultAddress: String, + amount: String, ): Either /** @@ -70,12 +72,16 @@ interface P2PEthPoolRepository { * Only works when funds are available (after exit queue wait period). * * @param network P2PEthPool network (MAINNET or TESTNET) - * @param stakerAddress User's wallet address + * @param delegatorAddress User's wallet address + * @param vaultAddress Vault contract address + * @param amount Amount of ETH to withdraw * @return Either error or unsigned transaction with withdrawal tickets */ suspend fun createWithdrawTransaction( network: P2PEthPoolNetwork, - stakerAddress: String, + delegatorAddress: String, + vaultAddress: String, + amount: String, ): Either /** @@ -104,7 +110,7 @@ interface P2PEthPoolRepository { network: P2PEthPoolNetwork, delegatorAddress: String, vaultAddress: String, - ): Either + ): Either /** * Get rewards history for account and vault diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt index 3cfec3cb1c..a46b3a7c79 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt @@ -27,7 +27,6 @@ import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTon import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.datetime.Instant import java.math.BigDecimal @@ -197,7 +196,11 @@ internal class StakingBalanceEntryConverter( private fun StakingBalanceEntry.getPendingActions(): List { return when (val actions = this.actions) { is StakingEntryActions.StakeKit -> actions.pendingActions - is StakingEntryActions.P2PEthPool -> persistentListOf() + is StakingEntryActions.P2PEthPool -> if (type == StakingEntryType.WITHDRAWABLE) { + listOf(STUB_WITHDRAW_ACTION) + } else { + emptyList() + } } } @@ -223,6 +226,11 @@ internal class StakingBalanceEntryConverter( } private companion object { + val STUB_WITHDRAW_ACTION = PendingAction( + StakingActionType.WITHDRAW, + passthrough = "", + args = null, + ) const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 3fcfbe6f06..5e73aad950 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -81,14 +81,7 @@ internal class YieldBalancesConverter( private fun getRewardBlockType(stakingBalance: StakingBalance?): RewardBlockType { val blockchainId = cryptoCurrencyStatus.currency.network.rawId - - if (stakingBalance is StakingBalance.Data.P2PEthPool) { - return if (isStakingRewardUnavailable(blockchainId)) { - RewardBlockType.RewardUnavailable.DefaultRewardUnavailable - } else { - RewardBlockType.NoRewards - } - } + val isCoin = cryptoCurrencyStatus.currency.id.isCoin val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit val rewards = stakeKitBalance?.balance?.items @@ -98,7 +91,7 @@ internal class YieldBalancesConverter( val isRewardsClaimable = rewards?.isNotEmpty() == true return when { - isStakingRewardUnavailable(blockchainId) -> { + isStakingRewardUnavailable(blockchainId, isCoin) -> { if (BlockchainUtils.isSolana(blockchainId)) { RewardBlockType.RewardUnavailable.SolanaRewardUnavailable } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/P2PEthPoolTransactionCreator.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/P2PEthPoolTransactionCreator.kt index ed2e4f77df..09ee8a1b5c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/P2PEthPoolTransactionCreator.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/P2PEthPoolTransactionCreator.kt @@ -75,13 +75,20 @@ internal class P2PEthPoolTransactionCreator @Inject constructor( ) } is StakingActionCommonType.Exit -> { - p2pEthPoolRepository.createWithdrawTransaction( + p2pEthPoolRepository.createUnstakeTransaction( network = network, - stakerAddress = sourceAddress, + delegatorAddress = sourceAddress, + vaultAddress = vaultAddress, + amount = amount.toPlainString(), ) } is StakingActionCommonType.Pending -> { - Either.Left(StakingError.DomainError("Pending actions not supported for P2PEthPool")) + p2pEthPoolRepository.createWithdrawTransaction( + network = network, + delegatorAddress = sourceAddress, + vaultAddress = vaultAddress, + amount = amount.toPlainString(), + ) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index d93f8c4469..81f0f9314e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -174,8 +174,12 @@ internal class TokenDetailsStakingInfoConverter( private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { val blockchainId = status.currency.network.rawId + val isCoin = status.currency.id.isCoin + val rewardBlockType = when { - isStakingRewardUnavailable(blockchainId) -> RewardBlockType.RewardUnavailable.DefaultRewardUnavailable + isStakingRewardUnavailable(blockchainId, isCoin) -> { + RewardBlockType.RewardUnavailable.DefaultRewardUnavailable + } stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards else -> RewardBlockType.Rewards } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index b51011e498..e43e2690a6 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -115,6 +115,11 @@ object BlockchainUtils { return blockchain == Blockchain.BSC || blockchain == Blockchain.BSCTestnet } + fun isEthereum(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) + return blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet + } + data class BlockchainInfo( val blockchainId: String, val name: String, @@ -155,8 +160,10 @@ object BlockchainUtils { return blockchain != Blockchain.Cardano } - fun isStakingRewardUnavailable(blockchainId: String): Boolean { - return isSolana(blockchainId) || isBSC(blockchainId) || isTon(blockchainId) + fun isStakingRewardUnavailable(blockchainId: String, isCoin: Boolean): Boolean { + val isP2PEthPool = isEthereum(blockchainId) && isCoin + + return isSolana(blockchainId) || isBSC(blockchainId) || isTon(blockchainId) || isP2PEthPool } /** Checks if the blockchain uses case-insensitive contract addresses */