Updated on 2026-08-14
This commit is contained in:
parent
2722983c42
commit
b8fc2d4f76
47 changed files with 1677 additions and 0 deletions
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
|
||||
internal class DefaultP2PEthPoolAuthProvider(
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
) : P2PEthPoolAuthProvider {
|
||||
|
||||
override fun getApiKey(): String {
|
||||
// val keys = environmentConfigStorage.getConfigSync().p2pApiKey
|
||||
// ?: error("No P2P api keys provided")
|
||||
//
|
||||
// return keys.mainnet
|
||||
|
||||
environmentConfigStorage
|
||||
|
||||
return "TODO restore p2p config call after release 5.30"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,12 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultAppVersionProvider
|
||||
import com.tangem.tap.network.auth.DefaultAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultP2PEthPoolAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import dagger.Module
|
||||
|
|
@ -50,6 +52,12 @@ internal class AuthModule {
|
|||
return DefaultStakeKitAuthProvider(environmentConfigStorage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider {
|
||||
return DefaultP2PEthPoolAuthProvider(environmentConfigStorage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppVersionProvider(): AppVersionProvider {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ sealed class ApiConfig {
|
|||
Express,
|
||||
TangemTech,
|
||||
StakeKit,
|
||||
P2PEthPool,
|
||||
TangemPay,
|
||||
BlockAid,
|
||||
YieldSupply,
|
||||
|
|
@ -35,6 +36,7 @@ sealed class ApiConfig {
|
|||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is P2PEthPool -> ID.P2PEthPool
|
||||
is TangemPay -> ID.TangemPay
|
||||
is BlockAid -> ID.BlockAid
|
||||
is YieldSupply -> ID.YieldSupply
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
/**
|
||||
* P2P.org Ethereum Pooled Staking API configuration
|
||||
*/
|
||||
internal class P2PEthPool(
|
||||
private val p2pAuthProvider: P2PEthPoolAuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createTestEnvironment(),
|
||||
createMockEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://api-test.p2p.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMockEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createHeaders() = buildMap {
|
||||
put(key = "Authorization", value = ProviderSuspend { "Bearer ${p2pAuthProvider.getApiKey()}" })
|
||||
put(key = "accept", value = ProviderSuspend { "application/json" })
|
||||
put(key = "Content-Type", value = ProviderSuspend { "application/json" })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
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.response.*
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* P2P.org Ethereum Pooled Staking API client
|
||||
*
|
||||
* Documentation: https://docs.p2p.org/
|
||||
*
|
||||
* Base URL: https://api.p2p.org (prod) / https://api-test.p2p.org (testnet)
|
||||
*/
|
||||
interface P2PEthPoolApi {
|
||||
|
||||
/**
|
||||
* Get list of available vaults
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/vaults")
|
||||
suspend fun getVaults(
|
||||
@Path("network") network: String = "mainnet",
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
|
||||
/**
|
||||
* Prepare deposit transaction
|
||||
*
|
||||
* 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)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/deposit")
|
||||
suspend fun createDepositTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolDepositRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
|
||||
|
||||
/**
|
||||
* Prepare unstake transaction
|
||||
*
|
||||
* 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)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/unstake")
|
||||
suspend fun createUnstakeTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolUnstakeRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
|
||||
|
||||
/**
|
||||
* Prepare withdrawal transaction
|
||||
*
|
||||
* Create unsigned transaction to withdraw available funds from exit queue.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Withdrawal parameters (staker address)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/withdraw")
|
||||
suspend fun createWithdrawTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolWithdrawRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
|
||||
|
||||
/**
|
||||
* Broadcast signed transaction
|
||||
*
|
||||
* Submit a signed transaction to the blockchain network.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Signed transaction in hexadecimal format
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/transaction/send")
|
||||
suspend fun broadcastTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolBroadcastRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolBroadcastResponse>>
|
||||
|
||||
/**
|
||||
* Get account summary
|
||||
*
|
||||
* Retrieve staking balance, rewards, and exit queue information for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}")
|
||||
suspend fun getAccountInfo(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
|
||||
|
||||
/**
|
||||
* Get rewards history
|
||||
*
|
||||
* Retrieve historical rewards data for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
* @param period Optional period filter (30, 60, or 90 days)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards")
|
||||
suspend fun getRewards(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
@Query("period") period: Int? = null,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolRewardsResponse>>
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for broadcasting signed transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/transaction/send
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolBroadcastRequest(
|
||||
@Json(name = "signedTransaction")
|
||||
val signedTransaction: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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,20 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolAccountResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "stake")
|
||||
val stake: P2PEthPoolStakeDTO,
|
||||
@Json(name = "availableToUnstake")
|
||||
val availableToUnstake: BigDecimal,
|
||||
@Json(name = "availableToWithdraw")
|
||||
val availableToWithdraw: BigDecimal,
|
||||
@Json(name = "exitQueue")
|
||||
val exitQueue: P2PEthPoolExitQueueDTO,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolStakeDTO(
|
||||
@Json(name = "assets")
|
||||
val assets: BigDecimal,
|
||||
@Json(name = "totalEarnedAssets")
|
||||
val totalEarnedAssets: BigDecimal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitQueueDTO(
|
||||
@Json(name = "total")
|
||||
val total: Double,
|
||||
@Json(name = "requests")
|
||||
val requests: List<P2PEthPoolExitRequestDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitRequestDTO(
|
||||
@Json(name = "ticket")
|
||||
val ticket: String,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
@Json(name = "timestamp")
|
||||
val timestamp: Long,
|
||||
@Json(name = "withdrawalTimestamp")
|
||||
val withdrawalTimestamp: Long,
|
||||
@Json(name = "isClaimable")
|
||||
val isClaimable: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
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}/transaction/send
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolBroadcastResponse(
|
||||
@Json(name = "hash")
|
||||
val hash: String,
|
||||
@Json(name = "status")
|
||||
val status: P2PEthPoolTxStatusDTO,
|
||||
@Json(name = "blockNumber")
|
||||
val blockNumber: Int,
|
||||
@Json(name = "transactionIndex")
|
||||
val transactionIndex: Int,
|
||||
@Json(name = "gasUsed")
|
||||
val gasUsed: String,
|
||||
@Json(name = "cumulativeGasUsed")
|
||||
val cumulativeGasUsed: String,
|
||||
@Json(name = "effectiveGasPrice")
|
||||
val effectiveGasPrice: String?,
|
||||
@Json(name = "from")
|
||||
val from: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction status from P2P API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
@Json(name = "success")
|
||||
SUCCESS,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Error response structure for P2P.org API
|
||||
*
|
||||
* All P2P API endpoints return errors in this format
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorResponse(
|
||||
@Json(name = "error")
|
||||
val error: P2PEthPoolErrorDetailsDTO,
|
||||
@Json(name = "result")
|
||||
val result: Any? = null, // null on error
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorDetailsDTO(
|
||||
@Json(name = "code")
|
||||
val code: Int, // Error code (e.g., 127106, 101111)
|
||||
@Json(name = "message")
|
||||
val message: String, // Human-readable error message
|
||||
@Json(name = "name")
|
||||
val name: String, // Error name/type
|
||||
@Json(name = "errors")
|
||||
val errors: List<String>? = null, // Optional validation errors array
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Unified response wrapper for all P2P.org API responses
|
||||
*
|
||||
* All P2P API endpoints return responses in this format:
|
||||
* ```json
|
||||
* {
|
||||
* "error": null | { code, message, name, errors },
|
||||
* "result": { ... } | null
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param T The type of the result data
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolResponse<T>(
|
||||
@Json(name = "error")
|
||||
val error: P2PEthPoolErrorDetailsDTO?,
|
||||
@Json(name = "result")
|
||||
val result: T?,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
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 GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardsResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "rewards")
|
||||
val rewards: List<P2PEthPoolRewardDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardDTO(
|
||||
@Json(name = "date")
|
||||
val date: DateTime,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "balance")
|
||||
val balance: BigDecimal,
|
||||
@Json(name = "rewards")
|
||||
val rewards: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Unsigned transaction structure
|
||||
*
|
||||
* Used in deposit, withdraw responses
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnsignedTxDTO(
|
||||
@Json(name = "serializeTx")
|
||||
val serializeTx: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
@Json(name = "value")
|
||||
val value: String,
|
||||
@Json(name = "nonce")
|
||||
val nonce: Int,
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
@Json(name = "gasLimit")
|
||||
val gasLimit: BigDecimal,
|
||||
@Json(name = "maxFeePerGas")
|
||||
val maxFeePerGas: BigDecimal,
|
||||
@Json(name = "maxPriorityFeePerGas")
|
||||
val maxPriorityFeePerGas: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
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 GET /api/v1/staking/pool/{network}/vaults
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolVaultsResponse(
|
||||
@Json(name = "network")
|
||||
val network: P2PEthPoolNetworkDTO,
|
||||
@Json(name = "vaults")
|
||||
val vaults: List<P2PEthPoolVaultDTO>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Network identifier in P2P API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolNetworkDTO {
|
||||
@Json(name = "mainnet")
|
||||
MAINNET,
|
||||
|
||||
@Json(name = "hoodi")
|
||||
HOODI,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolVaultDTO(
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "displayName")
|
||||
val displayName: String,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "baseApy")
|
||||
val baseApy: Double,
|
||||
@Json(name = "capacity")
|
||||
val capacity: Double,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
@Json(name = "feePercent")
|
||||
val feePercent: Double,
|
||||
@Json(name = "isPrivate")
|
||||
val isPrivate: Boolean,
|
||||
@Json(name = "isGenesis")
|
||||
val isGenesis: Boolean,
|
||||
@Json(name = "isSmoothingPool")
|
||||
val isSmoothingPool: Boolean,
|
||||
@Json(name = "isErc20")
|
||||
val isErc20: Boolean,
|
||||
@Json(name = "tokenName")
|
||||
val tokenName: String?,
|
||||
@Json(name = "tokenSymbol")
|
||||
val tokenSymbol: String?,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawResponse(
|
||||
@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,
|
||||
@Json(name = "tickets")
|
||||
val tickets: List<String>,
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.AuthProvider
|
|||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
|
@ -39,6 +40,12 @@ internal object ApiConfigsModule {
|
|||
return StakeKit(stakeKitAuthProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideP2PEthPoolConfig(p2pAuthProvider: P2PEthPoolAuthProvider): ApiConfig {
|
||||
return P2PEthPool(p2pAuthProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemTechConfig(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.datasource.api.express.TangemExpressApi
|
|||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.moonpay.MoonPayApi
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
|
|
@ -73,6 +74,15 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolApi(retrofitApiBuilder: RetrofitApiBuilder): P2PEthPoolApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.P2PEthPool,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ data class EnvironmentConfig(
|
|||
val express: ExpressModel? = null,
|
||||
val devExpress: ExpressModel? = null,
|
||||
val stakeKitApiKey: String? = null,
|
||||
// val p2pApiKey: P2PKeys? = null, TODO p2p after release 5.30
|
||||
val blockAidApiKey: String? = null,
|
||||
val tangemApiKey: String? = null,
|
||||
val tangemApiKeyDev: String? = null,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
// p2pApiKey = value.p2pApiKey, TODO p2p after release 5.30
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
tangemApiKey = value.tangemApiKey,
|
||||
tangemApiKeyDev = value.tangemApiKeyDev,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?,
|
||||
@Json(name = "polygonScanApiKey") val polygonScanApiKey: String?,
|
||||
@Json(name = "stakeKitApiKey") val stakeKitApiKey: String?,
|
||||
// @Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?, TODO p2p after release 5.30
|
||||
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
|
||||
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
|
||||
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
|
||||
|
|
@ -97,6 +98,12 @@ data class TonCenterKeys(
|
|||
@Json(name = "testnet") val testnet: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "hoodi") val hoodi: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockToken(
|
||||
@Json(name = "jsonRpc") val jsonRPC: String?,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ class ApiConfigTest {
|
|||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD
|
|||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
|
|
@ -39,6 +40,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private val appVersionProvider = mockk<AppVersionProvider>()
|
||||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||
private val p2pEthPoolAuthProvider = mockk<P2PEthPoolAuthProvider>()
|
||||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
private val appInfoProvider = mockk<AppInfoProvider>()
|
||||
private val tangemApiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||
|
|
@ -58,6 +60,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { appVersionProvider.versionName } returns VERSION_NAME
|
||||
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
|
||||
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
|
||||
every { p2pEthPoolAuthProvider.getApiKey() } returns P2P_API_KEY
|
||||
every { appAuthProvider.getApiKey(any()) } returns tangemApiKeyProvider
|
||||
coEvery { tangemApiKeyProvider.invoke() } returns TANGEM_API_KEY
|
||||
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
|
||||
|
|
@ -110,6 +113,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,6 +127,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.TangemPay -> createTangemPayModel()
|
||||
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
|
||||
ApiConfig.ID.MoonPay -> createMoonPayModel()
|
||||
ApiConfig.ID.P2PEthPool -> createP2PModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,6 +280,21 @@ internal class ProdApiConfigsManagerTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.P2PEthPool,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
headers = mapOf(
|
||||
"Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" },
|
||||
"accept" to ProviderSuspend { "application/json" },
|
||||
"Content-Type" to ProviderSuspend { "application/json" },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
|
|
@ -293,6 +313,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
const val VERSION_NAME = "debug"
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val P2P_API_KEY = "p2p_api_key"
|
||||
const val APP_CARD_ID = "app_card_id"
|
||||
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.data.staking.converters.ethpool.*
|
||||
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.domain.staking.model.ethpool.*
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* P2P staking repository implementation
|
||||
*/
|
||||
internal class DefaultP2PEthPoolRepository(
|
||||
private val p2pApi: P2PEthPoolApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : P2PEthPoolRepository {
|
||||
|
||||
private val vaultConverter = P2PEthPoolVaultConverter
|
||||
private val accountInfoConverter = P2PEthPoolAccountConverter
|
||||
private val rewardConverter = P2PEthPoolRewardConverter
|
||||
private val broadcastResultConverter = P2PEthPoolBroadcastResultConverter
|
||||
private val errorConverter = P2PEthPoolErrorConverter
|
||||
|
||||
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pApi.getVaults(network.value)
|
||||
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.vaults.map { vaultConverter.convert(it) }
|
||||
}
|
||||
is ApiResponse.Error -> raise(StakingError.UnknownError(response.cause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createDepositTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
amount: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolDepositRequest(
|
||||
delegatorAddress = delegatorAddress,
|
||||
vaultAddress = vaultAddress,
|
||||
amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")),
|
||||
)
|
||||
val response = p2pApi.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,
|
||||
)
|
||||
val response = p2pApi.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,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress)
|
||||
val response = p2pApi.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun broadcastTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
signedTransaction: String,
|
||||
): Either<StakingError, P2PEthPoolBroadcastResult> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction)
|
||||
val response = p2pApi.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAccountInfo(
|
||||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
): Either<StakingError, P2PEthPoolAccount> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pApi.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRewards(
|
||||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
period: Int?,
|
||||
): Either<StakingError, List<P2PEthPoolReward>> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pApi.getRewards(network.value, delegatorAddress, vaultAddress, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,13 @@ internal class DefaultStakingErrorResolver(
|
|||
is StakingError.DomainError -> {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
|
||||
}
|
||||
// P2P errors
|
||||
is StakingError.InvalidAmount,
|
||||
is StakingError.DataError,
|
||||
is StakingError.UnknownError,
|
||||
-> {
|
||||
// P2P errors - no specific analytics event yet
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
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.staking.model.ethpool.*
|
||||
import com.tangem.utils.converter.Converter
|
||||
import org.joda.time.Instant
|
||||
|
||||
/**
|
||||
* Converter from P2P Account Info Response to Domain model
|
||||
*/
|
||||
internal object P2PEthPoolAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolAccount> {
|
||||
|
||||
override fun convert(value: P2PEthPoolAccountResponse): P2PEthPoolAccount {
|
||||
return P2PEthPoolAccount(
|
||||
delegatorAddress = value.delegatorAddress,
|
||||
vaultAddress = value.vaultAddress,
|
||||
stake = convertStake(value.stake),
|
||||
availableToUnstake = value.availableToUnstake,
|
||||
availableToWithdraw = value.availableToWithdraw,
|
||||
exitQueue = convertExitQueue(value.exitQueue),
|
||||
)
|
||||
}
|
||||
|
||||
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.ofEpochSecond(dto.timestamp),
|
||||
withdrawalTimestamp = Instant.ofEpochSecond(dto.withdrawalTimestamp),
|
||||
isClaimable = dto.isClaimable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolBroadcastResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolTxStatusDTO
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Converter from P2P Broadcast Transaction Response to Domain model
|
||||
*/
|
||||
internal object P2PEthPoolBroadcastResultConverter : Converter<P2PEthPoolBroadcastResponse, P2PEthPoolBroadcastResult> {
|
||||
|
||||
override fun convert(value: P2PEthPoolBroadcastResponse): P2PEthPoolBroadcastResult {
|
||||
return P2PEthPoolBroadcastResult(
|
||||
hash = value.hash,
|
||||
status = convertStatus(value.status),
|
||||
blockNumber = value.blockNumber,
|
||||
transactionIndex = value.transactionIndex,
|
||||
gasUsed = value.gasUsed.toBigDecimalOrNull() ?: BigDecimal.ZERO,
|
||||
cumulativeGasUsed = value.cumulativeGasUsed.toBigDecimalOrNull() ?: BigDecimal.ZERO,
|
||||
effectiveGasPrice = value.effectiveGasPrice?.toBigDecimalOrNull(),
|
||||
from = value.from,
|
||||
to = value.to,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertStatus(status: P2PEthPoolTxStatusDTO): P2PEthPoolBroadcastStatus {
|
||||
return when (status) {
|
||||
P2PEthPoolTxStatusDTO.SUCCESS -> P2PEthPoolBroadcastStatus.SUCCESS
|
||||
P2PEthPoolTxStatusDTO.FAILED -> P2PEthPoolBroadcastStatus.FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolErrorDetailsDTO
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolErrorResponse
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from P2P Error Response to Domain StakingError
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> {
|
||||
|
||||
override fun convert(value: P2PEthPoolErrorResponse): StakingError {
|
||||
return convertFromErrorDetails(value.error)
|
||||
}
|
||||
|
||||
fun convertFromErrorDetails(details: P2PEthPoolErrorDetailsDTO): StakingError {
|
||||
return when (details.code) {
|
||||
// Authentication errors
|
||||
101111 -> StakingError.UnknownError(
|
||||
Exception("Missing Bearer token: ${details.message}"),
|
||||
)
|
||||
101109 -> StakingError.UnknownError(
|
||||
Exception("Invalid Bearer token: ${details.message}"),
|
||||
)
|
||||
101110 -> StakingError.UnknownError(
|
||||
Exception("Server authorization error: ${details.message}"),
|
||||
)
|
||||
|
||||
// Validation errors
|
||||
100101 -> StakingError.InvalidAmount(details.message)
|
||||
|
||||
// Withdrawal errors
|
||||
127104 -> StakingError.DataError(
|
||||
IllegalStateException("No withdrawable balance: ${details.message}"),
|
||||
)
|
||||
127105 -> StakingError.UnknownError(
|
||||
Exception("Gas amount too low: ${details.message}"),
|
||||
)
|
||||
127106 -> StakingError.InvalidAmount("Invalid delegator address: ${details.message}")
|
||||
127107 -> StakingError.UnknownError(
|
||||
Exception("Gas price too low: ${details.message}"),
|
||||
)
|
||||
127108 -> StakingError.UnknownError(
|
||||
Exception("Transaction simulation failed: ${details.message}"),
|
||||
)
|
||||
|
||||
// Vault errors
|
||||
127101 -> StakingError.DataError(
|
||||
IllegalStateException("Invalid vault: ${details.message}"),
|
||||
)
|
||||
|
||||
// Account errors
|
||||
124108 -> StakingError.DataError(
|
||||
IllegalStateException("Invalid delegator: ${details.message}"),
|
||||
)
|
||||
|
||||
// Network errors
|
||||
127100 -> StakingError.DataError(
|
||||
IllegalStateException("Unsupported network: ${details.message}"),
|
||||
)
|
||||
|
||||
else -> StakingError.UnknownError(
|
||||
Exception("${details.code}: ${details.message}"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolRewardDTO
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from P2P Reward Entry DTO to Domain model
|
||||
*/
|
||||
internal object P2PEthPoolRewardConverter : Converter<P2PEthPoolRewardDTO, P2PEthPoolReward> {
|
||||
|
||||
override fun convert(value: P2PEthPoolRewardDTO): P2PEthPoolReward {
|
||||
return P2PEthPoolReward(
|
||||
date = value.date,
|
||||
apy = value.apy.toBigDecimal(),
|
||||
balance = value.balance,
|
||||
rewards = value.rewards,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolUnsignedTxDTO
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Converter from P2P Unsigned Transaction DTO to Domain model
|
||||
*/
|
||||
internal object P2PEthPoolUnsignedTxConverter : Converter<P2PEthPoolUnsignedTxDTO, P2PEthPoolUnsignedTx> {
|
||||
|
||||
override fun convert(value: P2PEthPoolUnsignedTxDTO): P2PEthPoolUnsignedTx {
|
||||
return P2PEthPoolUnsignedTx(
|
||||
serializeTx = value.serializeTx,
|
||||
to = value.to,
|
||||
data = value.data,
|
||||
value = value.value.toBigDecimalOrNull() ?: BigDecimal.ZERO,
|
||||
nonce = value.nonce,
|
||||
chainId = value.chainId,
|
||||
gasLimit = value.gasLimit,
|
||||
maxFeePerGas = value.maxFeePerGas,
|
||||
maxPriorityFeePerGas = value.maxPriorityFeePerGas,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolVaultDTO
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from P2P Vault DTO to Domain model
|
||||
*/
|
||||
internal object P2PEthPoolVaultConverter : Converter<P2PEthPoolVaultDTO, P2PEthPoolVault> {
|
||||
|
||||
override fun convert(value: P2PEthPoolVaultDTO): P2PEthPoolVault {
|
||||
return P2PEthPoolVault(
|
||||
vaultAddress = value.vaultAddress,
|
||||
displayName = value.displayName,
|
||||
apy = value.apy.toBigDecimal(),
|
||||
baseApy = value.baseApy.toBigDecimal(),
|
||||
capacity = value.capacity.toBigDecimal(),
|
||||
totalAssets = value.totalAssets.toBigDecimal(),
|
||||
feePercent = value.feePercent.toBigDecimal(),
|
||||
isPrivate = value.isPrivate,
|
||||
isGenesis = value.isGenesis,
|
||||
isSmoothingPool = value.isSmoothingPool,
|
||||
isErc20 = value.isErc20,
|
||||
tokenName = value.tokenName,
|
||||
tokenSymbol = value.tokenSymbol,
|
||||
createdAt = value.createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.staking.di
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.staking.DefaultP2PEthPoolRepository
|
||||
import com.tangem.data.staking.DefaultStakingActionRepository
|
||||
import com.tangem.data.staking.DefaultStakingErrorResolver
|
||||
import com.tangem.data.staking.DefaultStakingRepository
|
||||
|
|
@ -11,12 +12,14 @@ import com.tangem.data.staking.converters.error.StakeKitErrorConverter
|
|||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles
|
||||
import com.tangem.data.staking.utils.DefaultStakingCleaner
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.StakingActionsStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.repositories.StakingActionRepository
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
|
|
@ -60,6 +63,18 @@ internal object StakingDataModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolRepository(
|
||||
p2pApi: P2PEthPoolApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): P2PEthPoolRepository {
|
||||
return DefaultP2PEthPoolRepository(
|
||||
p2pApi = p2pApi,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingTransactionHashRepository(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import org.joda.time.Instant
|
||||
|
||||
/**
|
||||
* P2P.org 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,
|
||||
)
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* P2P.org staking action
|
||||
* Represents a staking operation (deposit, unstake, withdraw)
|
||||
*/
|
||||
data class P2PEthPoolAction(
|
||||
val id: String,
|
||||
val type: P2PEthPoolActionType,
|
||||
val status: P2PEthPoolActionStatus,
|
||||
val amount: SerializedBigDecimal,
|
||||
val vaultAddress: String,
|
||||
val delegatorAddress: String,
|
||||
val transaction: P2PEthPoolStakingTransaction?,
|
||||
val createdAt: DateTime?,
|
||||
val completedAt: DateTime?,
|
||||
val metadata: P2PEthPoolActionMetadata?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Types of P2P staking actions
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolActionType {
|
||||
DEPOSIT,
|
||||
UNSTAKE,
|
||||
WITHDRAW,
|
||||
CLAIM_REWARDS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of P2P staking action
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolActionStatus {
|
||||
CREATED,
|
||||
WAITING_FOR_SIGNATURE,
|
||||
PROCESSING,
|
||||
CONFIRMED,
|
||||
FAILED,
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction details for P2P staking action
|
||||
*/
|
||||
data class P2PEthPoolStakingTransaction(
|
||||
val id: String?,
|
||||
val unsignedTransaction: P2PEthPoolUnsignedTxDetails?,
|
||||
val signedTransaction: String?,
|
||||
val status: P2PEthPoolTxStatus,
|
||||
val gasEstimate: P2PEthPoolGasEstimate?,
|
||||
val error: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Unsigned transaction details
|
||||
*/
|
||||
data class P2PEthPoolUnsignedTxDetails(
|
||||
val to: String,
|
||||
val data: String,
|
||||
val value: SerializedBigDecimal,
|
||||
val nonce: Int,
|
||||
val chainId: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Gas estimation for transaction
|
||||
*/
|
||||
data class P2PEthPoolGasEstimate(
|
||||
val gasLimit: SerializedBigDecimal,
|
||||
val maxFeePerGas: SerializedBigDecimal,
|
||||
val maxPriorityFeePerGas: SerializedBigDecimal,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction status
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolTxStatus {
|
||||
UNSIGNED,
|
||||
SIGNED,
|
||||
PENDING,
|
||||
CONFIRMED,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional metadata for action
|
||||
*/
|
||||
data class P2PEthPoolActionMetadata(
|
||||
val ticket: String?,
|
||||
val exitQueuePosition: Int?,
|
||||
val estimatedCompletionDate: DateTime?,
|
||||
val unstakeFee: SerializedBigDecimal?,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import org.joda.time.Instant
|
||||
|
||||
/**
|
||||
* P2P.org staking balance information (similar to StakeKit's YieldBalanceItem)
|
||||
* Contains staked amounts, rewards, and pending actions
|
||||
*/
|
||||
data class P2PEthPoolStakingBalance(
|
||||
val vaultAddress: String,
|
||||
val delegatorAddress: String,
|
||||
val items: List<P2PEthPoolBalanceItem>,
|
||||
val network: P2PEthPoolNetwork,
|
||||
)
|
||||
|
||||
/**
|
||||
* Individual balance item for P2P staking
|
||||
*/
|
||||
data class P2PEthPoolBalanceItem(
|
||||
val type: P2PEthPoolBalanceType,
|
||||
val amount: SerializedBigDecimal,
|
||||
val rawAmount: String?,
|
||||
val date: Instant?,
|
||||
val pendingAction: P2PEthPoolPendingAction?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Types of balances in P2P staking
|
||||
*/
|
||||
enum class P2PEthPoolBalanceType {
|
||||
STAKED,
|
||||
REWARDS,
|
||||
UNSTAKING,
|
||||
WITHDRAWABLE,
|
||||
LOCKED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending action information
|
||||
*/
|
||||
data class P2PEthPoolPendingAction(
|
||||
val type: P2PEthPoolPendingActionType,
|
||||
val ticket: String?,
|
||||
val estimatedDate: Instant?,
|
||||
val isClaimable: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* Types of pending actions
|
||||
*/
|
||||
enum class P2PEthPoolPendingActionType {
|
||||
UNSTAKE_PENDING,
|
||||
WITHDRAWAL_AVAILABLE,
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* P2P.org transaction broadcast result
|
||||
* Contains transaction confirmation details
|
||||
*/
|
||||
data class P2PEthPoolBroadcastResult(
|
||||
val hash: String,
|
||||
val status: P2PEthPoolBroadcastStatus,
|
||||
val blockNumber: Int,
|
||||
val transactionIndex: Int,
|
||||
val gasUsed: SerializedBigDecimal,
|
||||
val cumulativeGasUsed: SerializedBigDecimal,
|
||||
val effectiveGasPrice: SerializedBigDecimal?,
|
||||
val from: String,
|
||||
val to: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction broadcast status
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolBroadcastStatus {
|
||||
SUCCESS, // Transaction confirmed successfully
|
||||
FAILED, // Transaction failed
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
/**
|
||||
* P2P.org supported networks for pooled staking
|
||||
*
|
||||
* @property value Network identifier used in API requests
|
||||
* @property displayName Human-readable network name
|
||||
* @property chainId Ethereum chain ID
|
||||
*/
|
||||
enum class P2PEthPoolNetwork(
|
||||
val value: String,
|
||||
val displayName: String,
|
||||
val chainId: Int,
|
||||
) {
|
||||
/**
|
||||
* Ethereum mainnet
|
||||
* Chain ID: 1
|
||||
*/
|
||||
MAINNET(
|
||||
value = "mainnet",
|
||||
displayName = "Ethereum",
|
||||
chainId = 1,
|
||||
),
|
||||
|
||||
/**
|
||||
* Ethereum testnet (Holesky)
|
||||
* Chain ID: 17000
|
||||
*/
|
||||
TESTNET(
|
||||
value = "hoodi",
|
||||
displayName = "Holesky Testnet",
|
||||
chainId = 17000,
|
||||
),
|
||||
;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get P2PNetwork by chain ID
|
||||
*
|
||||
* @param chainId Ethereum chain ID
|
||||
* @return P2PNetwork or null if not supported
|
||||
*/
|
||||
fun fromChainId(chainId: Int): P2PEthPoolNetwork? {
|
||||
return entries.find { it.chainId == chainId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get P2PNetwork by API value
|
||||
*
|
||||
* @param value API network identifier
|
||||
* @return P2PNetwork or null if not found
|
||||
*/
|
||||
fun fromValue(value: String): P2PEthPoolNetwork? {
|
||||
return entries.find { it.value == value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if chain ID is supported for P2P staking
|
||||
*
|
||||
* @param chainId Ethereum chain ID
|
||||
* @return true if supported, false otherwise
|
||||
*/
|
||||
fun isSupported(chainId: Int): Boolean {
|
||||
return fromChainId(chainId) != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* P2P.org rewards history entry
|
||||
* Historical reward information for account
|
||||
*/
|
||||
data class P2PEthPoolReward(
|
||||
val date: DateTime,
|
||||
val apy: SerializedBigDecimal,
|
||||
val balance: SerializedBigDecimal,
|
||||
val rewards: SerializedBigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
|
||||
/**
|
||||
* P2P.org pooled staking information (similar to StakeKit's Yield)
|
||||
* Contains vault details, APY, status, and metadata
|
||||
*/
|
||||
data class P2PEthPoolStaking(
|
||||
val id: String, // Vault address used as unique ID
|
||||
val vault: P2PEthPoolVaultDetails,
|
||||
val status: Status,
|
||||
val apy: SerializedBigDecimal,
|
||||
val metadata: Metadata,
|
||||
val isAvailable: Boolean,
|
||||
val network: P2PEthPoolNetwork,
|
||||
) {
|
||||
|
||||
data class Status(
|
||||
val enter: Boolean, // Can deposit
|
||||
val exit: Boolean, // Can unstake/withdraw
|
||||
)
|
||||
|
||||
data class Metadata(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val logoUri: String?,
|
||||
val documentation: String?,
|
||||
val cooldownPeriod: Period?, // Exit queue waiting time
|
||||
val warmupPeriod: Period?, // Time before rewards start
|
||||
val minimumStake: SerializedBigDecimal?,
|
||||
val maximumStake: SerializedBigDecimal?,
|
||||
val fee: Fee,
|
||||
val rewardSchedule: RewardSchedule,
|
||||
val rewardClaiming: RewardClaiming,
|
||||
) {
|
||||
|
||||
data class Period(
|
||||
val days: Int,
|
||||
)
|
||||
|
||||
data class Fee(
|
||||
val enabled: Boolean,
|
||||
val percent: SerializedBigDecimal?,
|
||||
)
|
||||
|
||||
enum class RewardSchedule {
|
||||
DAILY,
|
||||
WEEKLY,
|
||||
MONTHLY,
|
||||
CONTINUOUS,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
enum class RewardClaiming {
|
||||
AUTO, // Automatically compounded
|
||||
MANUAL, // Need to claim manually
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed vault information for P2P staking
|
||||
*/
|
||||
data class P2PEthPoolVaultDetails(
|
||||
val vaultAddress: String,
|
||||
val displayName: String,
|
||||
val tokenSymbol: String,
|
||||
val capacity: SerializedBigDecimal,
|
||||
val totalAssets: SerializedBigDecimal,
|
||||
val isPrivate: Boolean,
|
||||
val isGenesis: Boolean,
|
||||
val isSmoothingPool: Boolean,
|
||||
val isErc20: Boolean,
|
||||
val createdAt: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
|
||||
/**
|
||||
* P2P.org unsigned transaction ready for signing
|
||||
* Contains all necessary data for transaction signing
|
||||
*/
|
||||
data class P2PEthPoolUnsignedTx(
|
||||
val serializeTx: String,
|
||||
val to: String,
|
||||
val data: String,
|
||||
val value: SerializedBigDecimal,
|
||||
val nonce: Int,
|
||||
val chainId: Int,
|
||||
val gasLimit: SerializedBigDecimal,
|
||||
val maxFeePerGas: SerializedBigDecimal,
|
||||
val maxPriorityFeePerGas: SerializedBigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* P2P.org pooled staking vault information
|
||||
* Simplified version for vault list display
|
||||
*/
|
||||
data class P2PEthPoolVault(
|
||||
val vaultAddress: String,
|
||||
val displayName: String,
|
||||
val apy: SerializedBigDecimal,
|
||||
val baseApy: SerializedBigDecimal,
|
||||
val capacity: SerializedBigDecimal,
|
||||
val totalAssets: SerializedBigDecimal,
|
||||
val feePercent: SerializedBigDecimal,
|
||||
val isPrivate: Boolean,
|
||||
val isGenesis: Boolean,
|
||||
val isSmoothingPool: Boolean,
|
||||
val isErc20: Boolean,
|
||||
val tokenName: String?,
|
||||
val tokenSymbol: String?,
|
||||
val createdAt: DateTime,
|
||||
)
|
||||
|
|
@ -24,6 +24,16 @@ sealed class StakingError {
|
|||
|
||||
// endregion
|
||||
|
||||
// region p2p errors
|
||||
|
||||
data class InvalidAmount(val message: String) : StakingError()
|
||||
|
||||
data class DataError(val exception: Throwable) : StakingError()
|
||||
|
||||
data class UnknownError(val exception: Throwable) : StakingError()
|
||||
|
||||
// endregion
|
||||
|
||||
data class DomainError(val message: String?) : StakingError()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.domain.staking.repositories
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.staking.model.ethpool.*
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
|
||||
/**
|
||||
* P2P staking repository interface
|
||||
*/
|
||||
interface P2PEthPoolRepository {
|
||||
|
||||
/**
|
||||
* Get list of available staking vaults
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @return Either error or list of vaults with APY, capacity, fees
|
||||
*/
|
||||
suspend fun getVaults(
|
||||
network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET,
|
||||
): Either<StakingError, List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Create unsigned transaction for depositing ETH into a vault
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @param amount Amount of ETH to deposit
|
||||
* @return Either error or unsigned transaction ready to sign
|
||||
*/
|
||||
suspend fun createDepositTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
amount: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx>
|
||||
|
||||
/**
|
||||
* Create unsigned transaction to initiate unstaking
|
||||
*
|
||||
* Unstaking adds funds to exit queue. After ~1-4 days, use [createWithdrawTransaction]
|
||||
* to withdraw the funds.
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology)
|
||||
* @param stakeTransactionHash Original stake transaction hash
|
||||
* @return Either error or unsigned transaction
|
||||
*/
|
||||
suspend fun createUnstakeTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
stakerPublicKey: String,
|
||||
stakeTransactionHash: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx>
|
||||
|
||||
/**
|
||||
* Create unsigned transaction to withdraw funds from exit queue
|
||||
*
|
||||
* Only works when funds are available (after exit queue wait period).
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param stakerAddress User's wallet address
|
||||
* @return Either error or unsigned transaction with withdrawal tickets
|
||||
*/
|
||||
suspend fun createWithdrawTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
stakerAddress: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx>
|
||||
|
||||
/**
|
||||
* Broadcast signed transaction to blockchain
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param signedTransaction Signed transaction in hex format (with 0x prefix)
|
||||
* @return Either error or broadcast result with transaction hash
|
||||
*/
|
||||
suspend fun broadcastTransaction(
|
||||
network: P2PEthPoolNetwork,
|
||||
signedTransaction: String,
|
||||
): Either<StakingError, P2PEthPoolBroadcastResult>
|
||||
|
||||
/**
|
||||
* Get account staking information for specific vault
|
||||
*
|
||||
* Returns current stake, rewards, exit queue status, and available amounts
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @return Either error or account info
|
||||
*/
|
||||
suspend fun getAccountInfo(
|
||||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
): Either<StakingError, P2PEthPoolAccount>
|
||||
|
||||
/**
|
||||
* Get rewards history for account and vault
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @param period Optional period filter in days (30, 60, or 90)
|
||||
* @return Either error or list of reward entries
|
||||
*/
|
||||
suspend fun getRewards(
|
||||
network: P2PEthPoolNetwork,
|
||||
delegatorAddress: String,
|
||||
vaultAddress: String,
|
||||
period: Int? = null,
|
||||
): Either<StakingError, List<P2PEthPoolReward>>
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.lib.auth
|
||||
|
||||
interface P2PEthPoolAuthProvider {
|
||||
|
||||
fun getApiKey(): String
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue