Updated on 2026-08-14
This commit is contained in:
parent
721ab042e0
commit
7d599256d1
27 changed files with 366 additions and 454 deletions
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
|
||||
@Body body: P2PEthPoolTransactionRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
|
||||
|
||||
/**
|
||||
* 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<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
|
||||
@Body body: P2PEthPoolTransactionRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
|
||||
|
||||
/**
|
||||
* 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<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
|
||||
@Body body: P2PEthPoolTransactionRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<P2PEthPoolExitRequestDTO>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<String>,
|
||||
val tickets: List<String>? = 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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <T, R> Raise<StakingError>.handleApiResponse(
|
||||
response: ApiResponse<P2PEthPoolResponse<T>>,
|
||||
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,17 +86,9 @@ internal class DefaultP2PEthPoolRepository(
|
|||
|
||||
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = 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" }
|
||||
handleApiResponse(p2pEthPoolApi.getVaults(network.value)) { result ->
|
||||
result.vaults.map { vaultConverter.convert(it) }
|
||||
}
|
||||
is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,82 +97,61 @@ internal class DefaultP2PEthPoolRepository(
|
|||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
amount: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolDepositRequest(
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = createStakingTransaction(
|
||||
network = network,
|
||||
delegatorAddress = delegatorAddress,
|
||||
vaultAddress = vaultAddress,
|
||||
amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")),
|
||||
amount = amount,
|
||||
apiCall = p2pEthPoolApi::createDepositTransaction,
|
||||
)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createUnstakeTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
stakerPublicKey: String,
|
||||
stakeTransactionHash: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolUnstakeRequest(
|
||||
stakerPublicKey = stakerPublicKey,
|
||||
stakeTransactionHash = stakeTransactionHash,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
amount: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = createStakingTransaction(
|
||||
network = network,
|
||||
delegatorAddress = delegatorAddress,
|
||||
vaultAddress = vaultAddress,
|
||||
amount = amount,
|
||||
apiCall = p2pEthPoolApi::createUnstakeTransaction,
|
||||
)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createWithdrawTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
stakerAddress: String,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
amount: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = 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<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = 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" }
|
||||
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)
|
||||
}
|
||||
is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,18 +161,9 @@ internal class DefaultP2PEthPoolRepository(
|
|||
): Either<StakingError, P2PEthPoolBroadcastResult> = 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" }
|
||||
handleApiResponse(p2pEthPoolApi.broadcastTransaction(network.value, requestBody)) { result ->
|
||||
broadcastResultConverter.convert(result)
|
||||
}
|
||||
is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,19 +171,12 @@ internal class DefaultP2PEthPoolRepository(
|
|||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
): Either<StakingError, P2PEthPoolAccount> = either {
|
||||
): Either<StakingError, P2PEthPoolStakingAccount> = 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,23 +188,16 @@ internal class DefaultP2PEthPoolRepository(
|
|||
period: Int?,
|
||||
): Either<StakingError, List<P2PEthPoolReward>> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pEthPoolApi.getRewards(
|
||||
handleApiResponse(
|
||||
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 ->
|
||||
result.rewards.map { rewardConverter.convert(it) }
|
||||
}
|
||||
is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<P2PEthPoolAccountResponse, P2PEthPoolAccount> {
|
||||
internal object P2PEthPoolStakingAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolStakingAccount> {
|
||||
|
||||
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<P2PEthPoolAccountResponse
|
|||
)
|
||||
}
|
||||
|
||||
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
|
||||
fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
|
||||
return P2PEthPoolStake(
|
||||
assets = dto.assets,
|
||||
totalEarnedAssets = dto.totalEarnedAssets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
|
||||
fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
|
||||
return P2PEthPoolExitQueue(
|
||||
total = dto.total.toBigDecimal(),
|
||||
total = dto.total,
|
||||
requests = dto.requests.map(::convertExitRequest),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
|
||||
fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
|
||||
return P2PEthPoolExitRequest(
|
||||
ticket = dto.ticket,
|
||||
totalAssets = dto.totalAssets.toBigDecimal(),
|
||||
timestamp = Instant.ofEpochSecond(dto.timestamp),
|
||||
withdrawalTimestamp = Instant.ofEpochSecond(dto.withdrawalTimestamp),
|
||||
totalAssets = dto.totalAssets,
|
||||
timestamp = Instant.fromEpochMilliseconds(dto.timestamp),
|
||||
withdrawalTimestamp = dto.withdrawalTimestamp?.let { Instant.fromEpochMilliseconds(it) },
|
||||
isClaimable = dto.isClaimable,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
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.models.StatusSource
|
||||
import com.tangem.domain.models.staking.*
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import kotlinx.datetime.Instant
|
||||
import java.math.BigDecimal
|
||||
|
||||
/** Converts P2PEthPool API response to [StakingBalance] */
|
||||
/**
|
||||
* Converts P2PEthPool API response to [StakingBalance].
|
||||
*
|
||||
* Uses [P2PEthPoolStakingAccountConverter] for account conversion to avoid duplication.
|
||||
*/
|
||||
internal object P2PEthPoolStakingBalanceConverter {
|
||||
|
||||
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance {
|
||||
|
|
@ -19,14 +20,7 @@ internal object P2PEthPoolStakingBalanceConverter {
|
|||
address = response.delegatorAddress,
|
||||
)
|
||||
|
||||
val account = P2PEthPoolStakingAccount(
|
||||
delegatorAddress = response.delegatorAddress,
|
||||
vaultAddress = response.vaultAddress,
|
||||
stake = convertStake(response.stake),
|
||||
availableToUnstake = response.availableToUnstake,
|
||||
availableToWithdraw = response.availableToWithdraw,
|
||||
exitQueue = convertExitQueue(response.exitQueue),
|
||||
)
|
||||
val account = P2PEthPoolStakingAccountConverter.convert(response)
|
||||
|
||||
val hasActivePosition = account.stake.assets > 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Set<StakingBalance>>
|
||||
|
||||
/** 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<StakingBalance>?
|
||||
|
||||
/** 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<StakingID>)
|
||||
|
||||
/** Store error state for staking balances */
|
||||
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
/** Clear staking balances */
|
||||
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
}
|
||||
|
|
@ -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<Set<StakingBalance>>
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance?
|
||||
|
||||
suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<StakingBalance>?
|
||||
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID)
|
||||
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
interface P2PEthPoolBalancesStore : BaseStakingBalancesStore {
|
||||
|
||||
/** Store actual P2PEthPool account balances */
|
||||
suspend fun storeActual(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>)
|
||||
|
||||
/** Store empty state for accounts with no active positions */
|
||||
suspend fun storeEmpty(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
}
|
||||
|
|
@ -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<Set<StakingBalance>>
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance?
|
||||
|
||||
suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set<StakingBalance>?
|
||||
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID)
|
||||
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
/**
|
||||
* 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<YieldBalanceWrapperDTO>)
|
||||
|
||||
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
||||
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -5,27 +5,37 @@ import java.math.BigDecimal
|
|||
fun P2PEthPoolStakingAccount.toStakingBalanceEntries(vaultName: String? = null): List<StakingBalanceEntry> {
|
||||
return buildList {
|
||||
if (stake.assets > BigDecimal.ZERO) {
|
||||
add(
|
||||
StakingBalanceEntry(
|
||||
add(createStakedEntry(vaultAddress, stake.assets, vaultName))
|
||||
}
|
||||
exitQueue.requests.filter { !it.isClaimable }.forEach { add(createUnstakingEntry(vaultAddress, it, vaultName)) }
|
||||
if (availableToWithdraw > BigDecimal.ZERO) {
|
||||
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 = stake.assets,
|
||||
amount = amount,
|
||||
validator = ValidatorInfo(address = vaultAddress, name = vaultName),
|
||||
date = null,
|
||||
actions = StakingEntryActions.P2PEthPool(
|
||||
ticket = null,
|
||||
estimatedWithdrawalDate = null,
|
||||
isClaimable = false,
|
||||
),
|
||||
actions = StakingEntryActions.P2PEthPool(ticket = null, estimatedWithdrawalDate = null, isClaimable = false),
|
||||
isPending = false,
|
||||
rawCurrencyId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
exitQueue.requests.forEach { request ->
|
||||
add(
|
||||
StakingBalanceEntry(
|
||||
private fun createUnstakingEntry(
|
||||
vaultAddress: String,
|
||||
request: P2PEthPoolExitRequest,
|
||||
vaultName: String?,
|
||||
): StakingBalanceEntry {
|
||||
return StakingBalanceEntry(
|
||||
id = "${vaultAddress}_${request.ticket}",
|
||||
type = StakingEntryType.UNSTAKING,
|
||||
amount = request.totalAssets,
|
||||
|
|
@ -34,20 +44,22 @@ fun P2PEthPoolStakingAccount.toStakingBalanceEntries(vaultName: String? = null):
|
|||
actions = StakingEntryActions.P2PEthPool(
|
||||
ticket = request.ticket,
|
||||
estimatedWithdrawalDate = request.withdrawalTimestamp,
|
||||
isClaimable = request.isClaimable,
|
||||
isClaimable = false,
|
||||
),
|
||||
isPending = false,
|
||||
rawCurrencyId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (availableToWithdraw > BigDecimal.ZERO) {
|
||||
add(
|
||||
StakingBalanceEntry(
|
||||
private fun createWithdrawableEntry(
|
||||
vaultAddress: String,
|
||||
amount: BigDecimal,
|
||||
vaultName: String?,
|
||||
): StakingBalanceEntry {
|
||||
return StakingBalanceEntry(
|
||||
id = "${vaultAddress}_withdrawable",
|
||||
type = StakingEntryType.WITHDRAWABLE,
|
||||
amount = availableToWithdraw,
|
||||
amount = amount,
|
||||
validator = ValidatorInfo(address = vaultAddress, name = vaultName),
|
||||
date = null,
|
||||
actions = StakingEntryActions.P2PEthPool(
|
||||
|
|
@ -57,8 +69,18 @@ fun P2PEthPoolStakingAccount.toStakingBalanceEntries(vaultName: String? = null):
|
|||
),
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<P2PEthPoolExitRequest>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Individual exit request in the queue
|
||||
*/
|
||||
data class P2PEthPoolExitRequest(
|
||||
val ticket: String,
|
||||
val totalAssets: SerializedBigDecimal,
|
||||
val timestamp: Instant,
|
||||
val withdrawalTimestamp: Instant,
|
||||
val isClaimable: Boolean,
|
||||
)
|
||||
|
|
@ -15,7 +15,7 @@ import java.math.BigDecimal
|
|||
*/
|
||||
class P2PEthPoolIntegration(
|
||||
override val integrationId: StakingIntegrationID,
|
||||
vaults: List<P2PEthPoolVault>,
|
||||
private val vaults: List<P2PEthPoolVault>,
|
||||
) : 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
|
||||
|
|
|
|||
|
|
@ -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<StakingError, P2PEthPoolUnsignedTx>
|
||||
|
||||
/**
|
||||
|
|
@ -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<StakingError, P2PEthPoolUnsignedTx>
|
||||
|
||||
/**
|
||||
|
|
@ -104,7 +110,7 @@ interface P2PEthPoolRepository {
|
|||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
): Either<StakingError, P2PEthPoolAccount>
|
||||
): Either<StakingError, P2PEthPoolStakingAccount>
|
||||
|
||||
/**
|
||||
* Get rewards history for account and vault
|
||||
|
|
|
|||
|
|
@ -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<PendingAction> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue