From c4aba9a0b2387cc2274027d995e2c3848913dc6b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Jul 2026 11:17:48 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/PolymarketDomainModule.kt | 20 +++++++++ .../api/polymarket/PolymarketApi.kt | 26 +++++++++++ .../models/PolymarketEventsResponse.kt | 45 +++++++++++++++++++ .../com/tangem/datasource/di/NetworkModule.kt | 13 ++++++ data/polymarket/build.gradle.kts | 40 +++++++++++++++++ .../polymarket/DefaultPolymarketRepository.kt | 36 +++++++++++++++ .../converter/PolymarketEventConverter.kt | 45 +++++++++++++++++++ .../polymarket/di/PolymarketDataModule.kt | 18 ++++++++ domain/polymarket/build.gradle.kts | 27 +++++++++++ .../domain/polymarket/PolymarketRepository.kt | 13 ++++++ .../polymarket/model/PolymarketEvent.kt | 27 +++++++++++ .../polymarket/model/PolymarketMarket.kt | 20 +++++++++ .../polymarket/model/PolymarketOutcome.kt | 16 +++++++ .../usecase/GetPolymarketEventsUseCase.kt | 15 +++++++ settings.gradle.kts | 2 + 16 files changed, 365 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/PolymarketDomainModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/polymarket/PolymarketApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/polymarket/models/PolymarketEventsResponse.kt create mode 100644 data/polymarket/build.gradle.kts create mode 100644 data/polymarket/src/main/java/com/tangem/data/polymarket/DefaultPolymarketRepository.kt create mode 100644 data/polymarket/src/main/java/com/tangem/data/polymarket/converter/PolymarketEventConverter.kt create mode 100644 data/polymarket/src/main/java/com/tangem/data/polymarket/di/PolymarketDataModule.kt create mode 100644 domain/polymarket/build.gradle.kts create mode 100644 domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/PolymarketRepository.kt create mode 100644 domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketEvent.kt create mode 100644 domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketMarket.kt create mode 100644 domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketOutcome.kt create mode 100644 domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/usecase/GetPolymarketEventsUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8da953b5fd..cb8d83b77e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/tangem/tap/di/domain/PolymarketDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PolymarketDomainModule.kt new file mode 100644 index 0000000000..7fb4fb172f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/PolymarketDomainModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/polymarket/PolymarketApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/polymarket/PolymarketApi.kt new file mode 100644 index 0000000000..8b10fd3a6e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/polymarket/PolymarketApi.kt @@ -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 +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/polymarket/models/PolymarketEventsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/polymarket/models/PolymarketEventsResponse.kt new file mode 100644 index 0000000000..f3b3536009 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/polymarket/models/PolymarketEventsResponse.kt @@ -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, + @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, +) + +/** 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, +) + +/** 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?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index cf0af7d6f0..773d5a26c7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -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 { diff --git a/data/polymarket/build.gradle.kts b/data/polymarket/build.gradle.kts new file mode 100644 index 0000000000..a95859fbad --- /dev/null +++ b/data/polymarket/build.gradle.kts @@ -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 +} \ No newline at end of file diff --git a/data/polymarket/src/main/java/com/tangem/data/polymarket/DefaultPolymarketRepository.kt b/data/polymarket/src/main/java/com/tangem/data/polymarket/DefaultPolymarketRepository.kt new file mode 100644 index 0000000000..8cc85a2e06 --- /dev/null +++ b/data/polymarket/src/main/java/com/tangem/data/polymarket/DefaultPolymarketRepository.kt @@ -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> = 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 + } +} \ No newline at end of file diff --git a/data/polymarket/src/main/java/com/tangem/data/polymarket/converter/PolymarketEventConverter.kt b/data/polymarket/src/main/java/com/tangem/data/polymarket/converter/PolymarketEventConverter.kt new file mode 100644 index 0000000000..5205dc6c17 --- /dev/null +++ b/data/polymarket/src/main/java/com/tangem/data/polymarket/converter/PolymarketEventConverter.kt @@ -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 { + + 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), + ) + } +} \ No newline at end of file diff --git a/data/polymarket/src/main/java/com/tangem/data/polymarket/di/PolymarketDataModule.kt b/data/polymarket/src/main/java/com/tangem/data/polymarket/di/PolymarketDataModule.kt new file mode 100644 index 0000000000..7f3a9f4de5 --- /dev/null +++ b/data/polymarket/src/main/java/com/tangem/data/polymarket/di/PolymarketDataModule.kt @@ -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 +} \ No newline at end of file diff --git a/domain/polymarket/build.gradle.kts b/domain/polymarket/build.gradle.kts new file mode 100644 index 0000000000..e4e98ca405 --- /dev/null +++ b/domain/polymarket/build.gradle.kts @@ -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 +} \ No newline at end of file diff --git a/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/PolymarketRepository.kt b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/PolymarketRepository.kt new file mode 100644 index 0000000000..3dfa17b9e3 --- /dev/null +++ b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/PolymarketRepository.kt @@ -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> +} \ No newline at end of file diff --git a/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketEvent.kt b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketEvent.kt new file mode 100644 index 0000000000..d47f9efaab --- /dev/null +++ b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketEvent.kt @@ -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, +) \ No newline at end of file diff --git a/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketMarket.kt b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketMarket.kt new file mode 100644 index 0000000000..688d2976f0 --- /dev/null +++ b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketMarket.kt @@ -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, +) \ No newline at end of file diff --git a/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketOutcome.kt b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketOutcome.kt new file mode 100644 index 0000000000..d7fda12c4a --- /dev/null +++ b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/model/PolymarketOutcome.kt @@ -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?, +) \ No newline at end of file diff --git a/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/usecase/GetPolymarketEventsUseCase.kt b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/usecase/GetPolymarketEventsUseCase.kt new file mode 100644 index 0000000000..44deee8081 --- /dev/null +++ b/domain/polymarket/src/main/kotlin/com/tangem/domain/polymarket/usecase/GetPolymarketEventsUseCase.kt @@ -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> { + return polymarketRepository.getEvents() + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index deee71855b..ec75b96e04 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")