Updated on 2026-08-14
This commit is contained in:
parent
246ab4b47e
commit
c4aba9a0b2
16 changed files with 365 additions and 0 deletions
|
|
@ -167,6 +167,7 @@ dependencies {
|
|||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.walletManager.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.polymarket)
|
||||
implementation(projects.domain.promo)
|
||||
implementation(projects.domain.blockaid)
|
||||
implementation(projects.domain.hotWallet)
|
||||
|
|
@ -232,6 +233,7 @@ dependencies {
|
|||
implementation(projects.data.swap)
|
||||
implementation(projects.data.walletManager)
|
||||
implementation(projects.data.yieldSupply)
|
||||
implementation(projects.data.polymarket)
|
||||
implementation(projects.data.promo)
|
||||
implementation(projects.data.hotWallet)
|
||||
implementation(projects.data.news)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.polymarket.PolymarketRepository
|
||||
import com.tangem.domain.polymarket.usecase.GetPolymarketEventsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object PolymarketDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetPolymarketEventsUseCase(polymarketRepository: PolymarketRepository): GetPolymarketEventsUseCase {
|
||||
return GetPolymarketEventsUseCase(polymarketRepository = polymarketRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.datasource.api.polymarket
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.polymarket.models.PolymarketEventsResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* Polymarket predictions BFF Discovery API (`api/predictions/v1`).
|
||||
*
|
||||
* Provided in [com.tangem.datasource.di.NetworkModule] via the [ApiConfig.ID.Predictions] config.
|
||||
*/
|
||||
interface PolymarketApi {
|
||||
|
||||
/**
|
||||
* Discovery feed: paginated prediction events, each with its top active markets.
|
||||
*
|
||||
* @param limit page size (BFF default 20)
|
||||
* @param cursor keyset pagination cursor; `null` for the first page
|
||||
*/
|
||||
@GET("api/predictions/v1/events")
|
||||
suspend fun getEvents(
|
||||
@Query("limit") limit: Int,
|
||||
@Query("cursor") cursor: String?,
|
||||
): ApiResponse<PolymarketEventsResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.datasource.api.polymarket.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response of `GET /api/predictions/v1/events` (BFF `EventsPageResponse`).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PolymarketEventsResponse(
|
||||
@Json(name = "events") val events: List<PolymarketEventDto>,
|
||||
@Json(name = "cursor") val cursor: String?,
|
||||
@Json(name = "hasNext") val hasNext: Boolean,
|
||||
)
|
||||
|
||||
/** BFF `Event`. Only the fields consumed by the app are declared; unknown JSON is ignored. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PolymarketEventDto(
|
||||
@Json(name = "eventId") val eventId: String,
|
||||
@Json(name = "slug") val slug: String,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "icon") val icon: String?,
|
||||
@Json(name = "image") val image: String?,
|
||||
@Json(name = "volume") val volume: Double?,
|
||||
@Json(name = "totalMarketsCount") val totalMarketsCount: Int,
|
||||
@Json(name = "markets") val markets: List<PolymarketMarketDto>,
|
||||
)
|
||||
|
||||
/** BFF `Market` (reduced preview inside an [PolymarketEventDto]). */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PolymarketMarketDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "question") val question: String,
|
||||
@Json(name = "icon") val icon: String?,
|
||||
@Json(name = "volume") val volume: Double?,
|
||||
@Json(name = "outcomes") val outcomes: List<PolymarketOutcomeDto>,
|
||||
)
|
||||
|
||||
/** BFF `OutcomeSummary`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PolymarketOutcomeDto(
|
||||
@Json(name = "assetId") val assetId: String,
|
||||
@Json(name = "label") val label: String,
|
||||
@Json(name = "probability") val probability: Double?,
|
||||
)
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
|||
import com.tangem.datasource.api.gasless.GaslessTxServiceApiV2
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.polymarket.PolymarketApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
|
||||
|
|
@ -36,6 +37,7 @@ import dagger.hilt.InstallIn
|
|||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object NetworkModule {
|
||||
|
|
@ -235,6 +237,17 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePolymarketApi(retrofitApiBuilder: RetrofitApiBuilder): PolymarketApi {
|
||||
// Polymarket BFF Discovery lives on the main Tangem gateway — reuse the TangemTech config.
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.TangemTech,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthApi(retrofitApiBuilder: RetrofitApiBuilder): AuthApi {
|
||||
|
|
|
|||
40
data/polymarket/build.gradle.kts
Normal file
40
data/polymarket/build.gradle.kts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.polymarket"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Core
|
||||
api(projects.core.datasource)
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region Domain
|
||||
api(projects.domain.polymarket)
|
||||
// endregion
|
||||
|
||||
// region tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.polymarket
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.polymarket.converter.PolymarketEventConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.polymarket.PolymarketApi
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.polymarket.PolymarketRepository
|
||||
import com.tangem.domain.polymarket.model.PolymarketEvent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultPolymarketRepository @Inject constructor(
|
||||
private val polymarketApi: PolymarketApi,
|
||||
private val eventConverter: PolymarketEventConverter,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PolymarketRepository {
|
||||
|
||||
override suspend fun getEvents(): Either<DataError, List<PolymarketEvent>> = withContext(dispatchers.io) {
|
||||
Either.catch {
|
||||
polymarketApi.getEvents(limit = DEFAULT_LIMIT, cursor = null)
|
||||
.getOrThrow()
|
||||
.events
|
||||
.map(eventConverter::convert)
|
||||
}.mapLeft {
|
||||
// DataError exposes only NoInternetConnection as a network failure.
|
||||
DataError.NetworkError.NoInternetConnection
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val DEFAULT_LIMIT = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.data.polymarket.converter
|
||||
|
||||
import com.tangem.datasource.api.polymarket.models.PolymarketEventDto
|
||||
import com.tangem.datasource.api.polymarket.models.PolymarketMarketDto
|
||||
import com.tangem.datasource.api.polymarket.models.PolymarketOutcomeDto
|
||||
import com.tangem.domain.polymarket.model.PolymarketEvent
|
||||
import com.tangem.domain.polymarket.model.PolymarketMarket
|
||||
import com.tangem.domain.polymarket.model.PolymarketOutcome
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class PolymarketEventConverter @Inject constructor() : Converter<PolymarketEventDto, PolymarketEvent> {
|
||||
|
||||
override fun convert(value: PolymarketEventDto): PolymarketEvent {
|
||||
return PolymarketEvent(
|
||||
id = value.eventId,
|
||||
slug = value.slug,
|
||||
title = value.title,
|
||||
iconUrl = value.icon,
|
||||
imageUrl = value.image,
|
||||
volume = value.volume?.let(BigDecimal::valueOf),
|
||||
totalMarketsCount = value.totalMarketsCount,
|
||||
markets = value.markets.map(::convertMarket),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertMarket(dto: PolymarketMarketDto): PolymarketMarket {
|
||||
return PolymarketMarket(
|
||||
id = dto.id,
|
||||
title = dto.question,
|
||||
iconUrl = dto.icon,
|
||||
volume = dto.volume?.let(BigDecimal::valueOf),
|
||||
outcomes = dto.outcomes.map(::convertOutcome),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertOutcome(dto: PolymarketOutcomeDto): PolymarketOutcome {
|
||||
return PolymarketOutcome(
|
||||
assetId = dto.assetId,
|
||||
title = dto.label,
|
||||
probability = dto.probability?.let(BigDecimal::valueOf),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.polymarket.di
|
||||
|
||||
import com.tangem.data.polymarket.DefaultPolymarketRepository
|
||||
import com.tangem.domain.polymarket.PolymarketRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface PolymarketDataModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindPolymarketRepository(impl: DefaultPolymarketRepository): PolymarketRepository
|
||||
}
|
||||
27
domain/polymarket/build.gradle.kts
Normal file
27
domain/polymarket/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
// region Other libraries
|
||||
api(deps.arrow.core)
|
||||
// endregion
|
||||
|
||||
// region Domain
|
||||
api(projects.domain.core)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.polymarket
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.polymarket.model.PolymarketEvent
|
||||
|
||||
interface PolymarketRepository {
|
||||
|
||||
/**
|
||||
* Fetch the Discovery feed of prediction events (each with its top active markets).
|
||||
*/
|
||||
suspend fun getEvents(): Either<DataError, List<PolymarketEvent>>
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.domain.polymarket.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* A prediction event — the primary Discovery feed entity (BFF `Event`).
|
||||
*
|
||||
* @property id unique event id
|
||||
* @property slug url-friendly event identifier
|
||||
* @property title event title
|
||||
* @property iconUrl optional event icon
|
||||
* @property imageUrl optional event image
|
||||
* @property volume total traded volume in USD, if known
|
||||
* @property totalMarketsCount total number of markets in the event (may exceed [markets] size,
|
||||
* since the Discovery feed carries only the top active markets)
|
||||
* @property markets nested markets (top active ones for the feed, all for event details)
|
||||
*/
|
||||
data class PolymarketEvent(
|
||||
val id: String,
|
||||
val slug: String,
|
||||
val title: String,
|
||||
val iconUrl: String?,
|
||||
val imageUrl: String?,
|
||||
val volume: BigDecimal?,
|
||||
val totalMarketsCount: Int,
|
||||
val markets: List<PolymarketMarket>,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.polymarket.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* A single Polymarket prediction market nested inside a [PolymarketEvent] (BFF `Market`).
|
||||
*
|
||||
* @property id unique market id
|
||||
* @property title the market question (e.g. "Will X happen by 2026?")
|
||||
* @property iconUrl optional market icon
|
||||
* @property volume total traded volume in USD, if known
|
||||
* @property outcomes normalized outcomes with their implied probabilities
|
||||
*/
|
||||
data class PolymarketMarket(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val iconUrl: String?,
|
||||
val volume: BigDecimal?,
|
||||
val outcomes: List<PolymarketOutcome>,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.polymarket.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* A single normalized outcome of a [PolymarketMarket] (BFF `OutcomeSummary`).
|
||||
*
|
||||
* @property assetId upstream asset (clob token) id of the outcome
|
||||
* @property title outcome label as provided by upstream (e.g. "Yes" / "No"), never hardcoded
|
||||
* @property probability implied probability in the 0..1 range, if known
|
||||
*/
|
||||
data class PolymarketOutcome(
|
||||
val assetId: String,
|
||||
val title: String,
|
||||
val probability: BigDecimal?,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.polymarket.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.polymarket.PolymarketRepository
|
||||
import com.tangem.domain.polymarket.model.PolymarketEvent
|
||||
|
||||
class GetPolymarketEventsUseCase(
|
||||
private val polymarketRepository: PolymarketRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Either<DataError, List<PolymarketEvent>> {
|
||||
return polymarketRepository.getEvents()
|
||||
}
|
||||
}
|
||||
|
|
@ -481,6 +481,7 @@ include(":domain:wallet-manager")
|
|||
include(":domain:wallet-manager:models")
|
||||
include(":domain:yield-supply")
|
||||
include(":domain:yield-supply:models")
|
||||
include(":domain:polymarket")
|
||||
include(":domain:promo")
|
||||
include(":domain:promo:models")
|
||||
include(":domain:news")
|
||||
|
|
@ -530,6 +531,7 @@ include(":data:swap")
|
|||
include(":data:express")
|
||||
include(":data:wallet-manager")
|
||||
include(":data:yield-supply")
|
||||
include(":data:polymarket")
|
||||
include(":data:promo")
|
||||
include(":data:news")
|
||||
include(":data:earn")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue