From 93e94cbf15cfd68cd4b489a91c918560b6f54d43 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Tue, 17 Feb 2026 00:10:23 +0300 Subject: [PATCH] feat: add Pezkuwi Dashboard card with live People Chain data - Dashboard card on Assets page showing roles, trust score, referral, staking, and perwerde points from People Chain pallets - Repository queries: Tiki, Trust, Referral, StakingScore, Perwerde - CachedStakingDetails double map query (RelayChain + AssetHub sources) - Full i18n support across all 15 locales including new Turkish locale - "Apply & Actions" button opens Telegram bot - Staking improvements for split ecosystem multi-endpoint stats --- common/build.gradle | 4 +- .../data/memory/RealComputationalCache.kt | 23 ++- common/src/main/res/values-es/strings.xml | 7 + common/src/main/res/values-fr-rFR/strings.xml | 7 + common/src/main/res/values-hu/strings.xml | 7 + common/src/main/res/values-in/strings.xml | 7 + common/src/main/res/values-it/strings.xml | 7 + common/src/main/res/values-ja/strings.xml | 7 + common/src/main/res/values-ko/strings.xml | 7 + common/src/main/res/values-ku/strings.xml | 7 + common/src/main/res/values-pl/strings.xml | 7 + common/src/main/res/values-pt/strings.xml | 7 + common/src/main/res/values-ru/strings.xml | 7 + common/src/main/res/values-tr/strings.xml | 9 ++ common/src/main/res/values-vi/strings.xml | 7 + common/src/main/res/values-zh-rCN/strings.xml | 7 + common/src/main/res/values/strings.xml | 7 + .../data/model/PezkuwiDashboardData.kt | 11 ++ .../repository/PezkuwiDashboardRepository.kt | 119 ++++++++++++++ .../dashboard/PezkuwiDashboardInteractor.kt | 21 +++ .../balance/list/BalanceListFragment.kt | 29 +++- .../balance/list/BalanceListViewModel.kt | 28 +++- .../balance/list/di/BalanceListModule.kt | 27 +++- .../list/model/PezkuwiDashboardModel.kt | 9 ++ .../list/view/PezkuwiDashboardAdapter.kt | 88 ++++++++++ .../res/drawable/bg_pezkuwi_dashboard.xml | 13 ++ .../res/layout/item_pezkuwi_dashboard.xml | 151 ++++++++++++++++++ .../data/repository/BagListRepository.kt | 3 +- .../data/repository/StakingRepositoryImpl.kt | 2 +- .../di/StakingFeatureModule.kt | 3 +- .../domain/common/StakingSharedComputation.kt | 63 +++++--- .../RealNominationPoolRewardCalculator.kt | 4 - .../common/StartMultiStakingInteractor.kt | 6 +- .../direct/DirectStakingProperties.kt | 6 +- .../direct/DirectStakingRecommendation.kt | 26 ++- .../pools/NominationPoolRecommendation.kt | 18 ++- .../AutomaticMultiStakingSelectionType.kt | 28 +++- 37 files changed, 741 insertions(+), 48 deletions(-) create mode 100644 common/src/main/res/values-tr/strings.xml create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/dashboard/PezkuwiDashboardInteractor.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt create mode 100644 feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt create mode 100644 feature-assets/src/main/res/drawable/bg_pezkuwi_dashboard.xml create mode 100644 feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml diff --git a/common/build.gradle b/common/build.gradle index 1fd35ec..0d9e848 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -48,7 +48,7 @@ android { buildConfigField "long", "CLOUD_PROJECT_NUMBER", "171267697857L" - buildConfigField "String", "GLOBAL_CONFIG_URL", "\"https://wallet.pezkuwichain.io/config/global_config_dev.json\"" + buildConfigField "String", "GLOBAL_CONFIG_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/staking/global_config_dev.json\"" } buildTypes { @@ -61,7 +61,7 @@ android { buildConfigField "long", "CLOUD_PROJECT_NUMBER", "802342409053L" - buildConfigField "String", "GLOBAL_CONFIG_URL", "\"https://wallet.pezkuwichain.io/config/global_config.json\"" + buildConfigField "String", "GLOBAL_CONFIG_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/staking/global_config.json\"" } releaseGithub { diff --git a/common/src/main/java/io/novafoundation/nova/common/data/memory/RealComputationalCache.kt b/common/src/main/java/io/novafoundation/nova/common/data/memory/RealComputationalCache.kt index 12f4b11..feba1dd 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/memory/RealComputationalCache.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/memory/RealComputationalCache.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.common.utils.flowOfAll import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.invokeOnCompletion import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -13,6 +14,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -72,18 +74,25 @@ internal class RealComputationalCache : ComputationalCache, CoroutineScope by Co cachedAction: AwaitableConstructor ): T { val awaitable = mutex.withLock { - if (key in memory) { + val existing = memory[key] + if (existing != null && existing.aggregateScope.isActive) { Log.d(LOG_TAG, "Key $key requested - already present") - val entry = memory.getValue(key) + existing.dependents += scope - entry.dependents += scope - - entry.awaitable + existing.awaitable } else { - Log.d(LOG_TAG, "Key $key requested - creating new operation") + if (existing != null) { + Log.d(LOG_TAG, "Key $key requested - stale (aggregateScope cancelled), recreating") + memory.remove(key) + } else { + Log.d(LOG_TAG, "Key $key requested - creating new operation") + } - val aggregateScope = CoroutineScope(Dispatchers.Default) + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.e(LOG_TAG, "Key $key - upstream error in aggregateScope", throwable) + } + val aggregateScope = CoroutineScope(Dispatchers.Default + exceptionHandler) val awaitable = cachedAction(aggregateScope) memory[key] = Entry(dependents = mutableSetOf(scope), aggregateScope, awaitable) diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index f808b89..4000266 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -2044,4 +2044,11 @@ Ingresa la cantidad Los intercambios HEZ→DOT pueden tener disponibilidad limitada según la liquidez. Los intercambios HEZ→DOT no están disponibles temporalmente. Inténtalo de nuevo cuando haya suficiente liquidez de DOT. + + Pezkuwi + Puntuación de confianza + Referido + Staking + Perwerde + Solicitar y Acciones diff --git a/common/src/main/res/values-fr-rFR/strings.xml b/common/src/main/res/values-fr-rFR/strings.xml index 064d306..423eae2 100644 --- a/common/src/main/res/values-fr-rFR/strings.xml +++ b/common/src/main/res/values-fr-rFR/strings.xml @@ -2044,4 +2044,11 @@ Entrez le montant Les échanges HEZ→DOT peuvent avoir une disponibilité limitée selon la liquidité. Les échanges HEZ→DOT sont temporairement indisponibles. Réessayez lorsque la liquidité DOT sera suffisante. + + Pezkuwi + Score de confiance + Parrainage + Staking + Perwerde + Demande et Actions diff --git a/common/src/main/res/values-hu/strings.xml b/common/src/main/res/values-hu/strings.xml index 3690a5c..73fb814 100644 --- a/common/src/main/res/values-hu/strings.xml +++ b/common/src/main/res/values-hu/strings.xml @@ -2044,4 +2044,11 @@ Írd be az összeget A HEZ→DOT cserék korlátozott elérhetőségűek lehetnek a likviditástól függően. A HEZ→DOT cserék ideiglenesen nem elérhetők. Próbáld újra, amikor elegendő DOT likviditás áll rendelkezésre. + + Pezkuwi + Bizalmi pontszám + Ajánlás + Staking + Perwerde + Jelentkezés és Műveletek diff --git a/common/src/main/res/values-in/strings.xml b/common/src/main/res/values-in/strings.xml index 0034f48..3f40ca8 100644 --- a/common/src/main/res/values-in/strings.xml +++ b/common/src/main/res/values-in/strings.xml @@ -2030,4 +2030,11 @@ Masukkan jumlah Penukaran HEZ→DOT mungkin terbatas tergantung pada likuiditas. Penukaran HEZ→DOT sementara tidak tersedia. Coba lagi saat likuiditas DOT mencukupi. + + Pezkuwi + Skor Kepercayaan + Referral + Staking + Perwerde + Ajukan & Tindakan diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index 75f1087..f70fa5d 100644 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -2044,4 +2044,11 @@ Inserisci importo Gli scambi HEZ→DOT potrebbero avere disponibilità limitata in base alla liquidità. Gli scambi HEZ→DOT sono temporaneamente non disponibili. Riprova quando la liquidità DOT sarà sufficiente. + + Pezkuwi + Punteggio di fiducia + Referral + Staking + Perwerde + Richiesta e Azioni diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index e2710f1..dba344f 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -2030,4 +2030,11 @@ 金額を入力 HEZ→DOT交換は流動性により制限される場合があります。 HEZ→DOT交換は一時的に利用できません。DOTの流動性が十分になったら再試行してください。 + + Pezkuwi + 信頼スコア + 紹介 + ステーキング + Perwerde + 申請とアクション diff --git a/common/src/main/res/values-ko/strings.xml b/common/src/main/res/values-ko/strings.xml index 73e109d..db9681b 100644 --- a/common/src/main/res/values-ko/strings.xml +++ b/common/src/main/res/values-ko/strings.xml @@ -2030,4 +2030,11 @@ 금액 입력 HEZ→DOT 교환은 유동성에 따라 제한될 수 있습니다. HEZ→DOT 교환은 일시적으로 사용할 수 없습니다. DOT 유동성이 충분해지면 다시 시도하세요. + + Pezkuwi + 신뢰 점수 + 추천 + 스테이킹 + Perwerde + 신청 및 작업 diff --git a/common/src/main/res/values-ku/strings.xml b/common/src/main/res/values-ku/strings.xml index ddb401e..62c22ce 100644 --- a/common/src/main/res/values-ku/strings.xml +++ b/common/src/main/res/values-ku/strings.xml @@ -2757,4 +2757,11 @@ Mîqdar binivîse Guherandina HEZ→DOT li gorî rewşa DOT sînordar dibe. Guherandina HEZ→DOT niha tune. Dema DOT têr bibe dîsa biceribîne. + + Pezkuwi + Pûana Pêbaweriyê + Referral + Staking + Perwerde + Serlêdan û Karên diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index 692102e..7ba546c 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -2072,4 +2072,11 @@ Wprowadź kwotę Wymiany HEZ→DOT mogą mieć ograniczoną dostępność w zależności od płynności. Wymiany HEZ→DOT są tymczasowo niedostępne. Spróbuj ponownie, gdy płynność DOT będzie wystarczająca. + + Pezkuwi + Wynik zaufania + Polecenie + Staking + Perwerde + Wniosek i Akcje diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index f44ee4a..8dd0bfd 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -2044,4 +2044,11 @@ Digite o valor As trocas HEZ→DOT podem ter disponibilidade limitada dependendo da liquidez. As trocas HEZ→DOT estão temporariamente indisponíveis. Tente novamente quando houver liquidez suficiente de DOT. + + Pezkuwi + Pontuação de confiança + Indicação + Staking + Perwerde + Candidatura e Ações diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 9ebce65..981876d 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -2072,4 +2072,11 @@ Введите сумму Обмен HEZ→DOT может быть ограничен в зависимости от ликвидности. Обмен HEZ→DOT временно недоступен. Повторите попытку при достаточной ликвидности DOT. + + Pezkuwi + Рейтинг доверия + Реферал + Стейкинг + Perwerde + Заявка и Действия diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..f7dd37b --- /dev/null +++ b/common/src/main/res/values-tr/strings.xml @@ -0,0 +1,9 @@ + + + Pezkuwi + Güven Puanı + Referans + Staking + Perwerde + Başvuru ve İşlemler + diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index 5001537..94610f7 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -2030,4 +2030,11 @@ Nhập số tiền Giao dịch HEZ→DOT có thể bị hạn chế tùy thuộc vào thanh khoản. Giao dịch HEZ→DOT tạm thời không khả dụng. Vui lòng thử lại khi thanh khoản DOT đủ. + + Pezkuwi + Điểm tin cậy + Giới thiệu + Staking + Perwerde + Đăng ký & Hành động diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml index 703db25..d5fd66f 100644 --- a/common/src/main/res/values-zh-rCN/strings.xml +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -2030,4 +2030,11 @@ 输入金额 HEZ→DOT兑换可能因流动性而受限。 HEZ→DOT兑换暂时不可用。请在DOT流动性充足时重试。 + + Pezkuwi + 信任评分 + 推荐 + 质押 + Perwerde + 申请与操作 diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 0369a00..fcf5c40 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -2759,4 +2759,11 @@ Yesterday pezkuwi-wallet.app.link pezkuwi-wallet-alternate.app.link + + Pezkuwi + Trust Score + Referral + Staking + Perwerde + Apply & Actions diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt new file mode 100644 index 0000000..f57589f --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt @@ -0,0 +1,11 @@ +package io.novafoundation.nova.feature_assets.data.model + +import java.math.BigInteger + +data class PezkuwiDashboardData( + val roles: List, + val trustScore: BigInteger, + val totalReferrals: Int, + val stakedAmount: BigInteger, + val perwerdePoints: Int +) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt new file mode 100644 index 0000000..4af84c6 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt @@ -0,0 +1,119 @@ +package io.novafoundation.nova.feature_assets.data.repository + +import io.novafoundation.nova.common.data.network.runtime.binding.bindInt +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum +import io.novafoundation.nova.common.data.network.runtime.binding.castToList +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.feature_assets.data.model.PezkuwiDashboardData +import io.novafoundation.nova.runtime.ext.ChainGeneses +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novasama.substrate_sdk_android.runtime.AccountId +import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum +import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull +import io.novasama.substrate_sdk_android.runtime.metadata.storage +import java.math.BigInteger + +class PezkuwiDashboardRepository( + private val remoteStorageDataSource: StorageDataSource +) { + + suspend fun getDashboard(accountId: AccountId): PezkuwiDashboardData { + val chainId = ChainGeneses.PEZKUWI_PEOPLE + + val roles = queryRoles(chainId, accountId) + val trustScore = queryTrustScore(chainId, accountId) + val totalReferrals = queryReferrals(chainId, accountId) + val stakedAmount = queryStakedAmount(chainId, accountId) + val perwerdePoints = queryPerwerdePoints(chainId, accountId) + + return PezkuwiDashboardData( + roles = roles, + trustScore = trustScore, + totalReferrals = totalReferrals, + stakedAmount = stakedAmount, + perwerdePoints = perwerdePoints + ) + } + + private suspend fun queryRoles(chainId: String, accountId: AccountId): List = runCatching { + remoteStorageDataSource.query(chainId) { + val tikiModule = runtime.metadata.moduleOrNull("Tiki") ?: return@query emptyList() + val result = tikiModule.storage("UserTikis").query(accountId, binding = { decoded -> + decoded?.castToList()?.map { entry -> + entry!!.castToDictEnum().name + } ?: emptyList() + }) + result + } + }.getOrDefault(emptyList()) + + private suspend fun queryTrustScore(chainId: String, accountId: AccountId): BigInteger = runCatching { + remoteStorageDataSource.query(chainId) { + val trustModule = runtime.metadata.moduleOrNull("Trust") ?: return@query BigInteger.ZERO + trustModule.storage("TrustScores").query(accountId, binding = { decoded -> + decoded?.let { bindNumber(it) } ?: BigInteger.ZERO + }) + } + }.getOrDefault(BigInteger.ZERO) + + private suspend fun queryReferrals(chainId: String, accountId: AccountId): Int = runCatching { + remoteStorageDataSource.query(chainId) { + val referralModule = runtime.metadata.moduleOrNull("Referral") ?: return@query 0 + referralModule.storage("ReferrerStatsStorage").query(accountId, binding = { decoded -> + decoded?.castToStruct()?.let { struct -> + bindInt(struct["total_referrals"]) + } ?: 0 + }) + } + }.getOrDefault(0) + + private suspend fun queryStakedAmount(chainId: String, accountId: AccountId): BigInteger = runCatching { + remoteStorageDataSource.query(chainId) { + val stakingModule = runtime.metadata.moduleOrNull("StakingScore") ?: return@query BigInteger.ZERO + + val relayChainKey = DictEnum.Entry("RelayChain", null) + val assetHubKey = DictEnum.Entry("AssetHub", null) + + val relayStaked = runCatching { + stakingModule.storage("CachedStakingDetails").query(accountId, relayChainKey, binding = { decoded -> + decoded?.castToStruct()?.let { struct -> + bindNumber(struct["staked_amount"]) + } ?: BigInteger.ZERO + }) + }.getOrDefault(BigInteger.ZERO) + + val assetHubStaked = runCatching { + stakingModule.storage("CachedStakingDetails").query(accountId, assetHubKey, binding = { decoded -> + decoded?.castToStruct()?.let { struct -> + bindNumber(struct["staked_amount"]) + } ?: BigInteger.ZERO + }) + }.getOrDefault(BigInteger.ZERO) + + relayStaked.add(assetHubStaked) + } + }.getOrDefault(BigInteger.ZERO) + + private suspend fun queryPerwerdePoints(chainId: String, accountId: AccountId): Int = runCatching { + remoteStorageDataSource.query(chainId) { + val perwerdeModule = runtime.metadata.moduleOrNull("Perwerde") ?: return@query 0 + + val courseIds = perwerdeModule.storage("StudentCourses").query(accountId, binding = { decoded -> + decoded?.castToList()?.map { bindInt(it) } ?: emptyList() + }) + + if (courseIds.isEmpty()) return@query 0 + + courseIds.sumOf { courseId -> + runCatching { + perwerdeModule.storage("Enrollments").query(courseId, accountId, binding = { decoded -> + decoded?.castToStruct()?.let { struct -> + bindInt(struct["points_earned"]) + } ?: 0 + }) + }.getOrDefault(0) + } + } + }.getOrDefault(0) +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/dashboard/PezkuwiDashboardInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/dashboard/PezkuwiDashboardInteractor.kt new file mode 100644 index 0000000..eae990d --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/dashboard/PezkuwiDashboardInteractor.kt @@ -0,0 +1,21 @@ +package io.novafoundation.nova.feature_assets.domain.dashboard + +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_assets.data.model.PezkuwiDashboardData +import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository +import io.novafoundation.nova.runtime.ext.ChainGeneses +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +class PezkuwiDashboardInteractor( + private val repository: PezkuwiDashboardRepository, + private val chainRegistry: ChainRegistry +) { + + suspend fun getDashboard(metaAccount: MetaAccount): Result = runCatching { + val peopleChain = chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE) + val accountId = metaAccount.accountIdIn(peopleChain) + ?: error("No account for People chain") + + repository.getDashboard(accountId) + } +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt index d1bc7a6..2979215 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt @@ -7,6 +7,7 @@ import coil.ImageLoader import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.list.EditablePlaceholderAdapter +import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets import io.novafoundation.nova.common.utils.hideKeyboard import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings @@ -31,6 +32,8 @@ import io.novafoundation.nova.feature_assets.presentation.balance.list.view.Asse import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetsHeaderHolder import io.novafoundation.nova.feature_assets.presentation.balance.list.view.ManageAssetsAdapter import io.novafoundation.nova.feature_assets.presentation.balance.list.view.ManageAssetsHolder +import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardAdapter +import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardHolder import io.novafoundation.nova.feature_banners_api.presentation.BannerHolder import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter import io.novafoundation.nova.feature_banners_api.presentation.bindWithAdapter @@ -41,7 +44,8 @@ class BalanceListFragment : BaseFragment(), BalanceListAdapter.ItemAssetHandler, AssetsHeaderAdapter.Handler, - ManageAssetsAdapter.Handler { + ManageAssetsAdapter.Handler, + PezkuwiDashboardAdapter.Handler { override fun createBinding() = FragmentBalanceListBinding.inflate(layoutInflater) @@ -54,6 +58,10 @@ class BalanceListFragment : AssetsHeaderAdapter(this) } + private val pezkuwiDashboardAdapter by lazy(LazyThreadSafetyMode.NONE) { + PezkuwiDashboardAdapter(this) + } + private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) { PromotionBannerAdapter(closable = true) } @@ -74,7 +82,7 @@ class BalanceListFragment : } private val adapter by lazy(LazyThreadSafetyMode.NONE) { - ConcatAdapter(headerAdapter, bannerAdapter, manageAssetsAdapter, emptyAssetsPlaceholder, assetsAdapter) + ConcatAdapter(headerAdapter, pezkuwiDashboardAdapter, bannerAdapter, manageAssetsAdapter, emptyAssetsPlaceholder, assetsAdapter) } override fun applyInsets(rootView: View) { @@ -111,6 +119,16 @@ class BalanceListFragment : override fun subscribe(viewModel: BalanceListViewModel) { setupBuySellSelectorMixin(viewModel.buySellSelectorMixin) + observeBrowserEvents(viewModel) + + viewModel.pezkuwiDashboardFlow.observe { model -> + if (model != null) { + pezkuwiDashboardAdapter.setModel(model) + pezkuwiDashboardAdapter.show(true) + } else { + pezkuwiDashboardAdapter.show(false) + } + } viewModel.bannersMixin.bindWithAdapter(bannerAdapter) { binder.balanceListAssets.invalidateItemDecorations() @@ -234,8 +252,15 @@ class BalanceListFragment : viewModel.giftClicked() } + override fun onBasvuruClicked() { + viewModel.basvuruClicked() + } + private fun setupRecyclerViewSpacing() { binder.balanceListAssets.addSpaceItemDecoration { + add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8)) + add(SpaceBetween(PezkuwiDashboardHolder, BannerHolder, spaceDp = 4)) + add(SpaceBetween(PezkuwiDashboardHolder, ManageAssetsHolder, spaceDp = 24)) add(SpaceBetween(AssetsHeaderHolder, BannerHolder, spaceDp = 4)) add(SpaceBetween(BannerHolder, ManageAssetsHolder, spaceDp = 4)) add(SpaceBetween(AssetsHeaderHolder, ManageAssetsHolder, spaceDp = 24)) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt index d42b1b5..fb4882a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.common.presentation.LoadingState import io.novafoundation.nova.common.presentation.masking.MaskableModel import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event +import io.novafoundation.nova.common.mixin.api.Browserable import io.novafoundation.nova.common.utils.formatting.format import io.novafoundation.nova.common.utils.formatting.formatAsPercentage import io.novafoundation.nova.common.utils.inBackground @@ -25,6 +26,8 @@ import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor +import io.novafoundation.nova.feature_assets.domain.dashboard.PezkuwiDashboardInteractor +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PezkuwiDashboardModel import io.novafoundation.nova.feature_assets.domain.assets.list.NftPreviews import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdown import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor @@ -97,8 +100,9 @@ class BalanceListViewModel( private val multisigPendingOperationsService: MultisigPendingOperationsService, private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin, private val maskingModeUseCase: MaskingModeUseCase, - private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin -) : BaseViewModel() { + private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin, + private val pezkuwiDashboardInteractor: PezkuwiDashboardInteractor +) : BaseViewModel(), Browserable.Presentation by Browserable() { private val maskableAmountFormatterFlow = maskableValueFormatterProvider.provideFormatter() .shareInBackground() @@ -218,6 +222,22 @@ class BalanceListViewModel( .combine(maskableAmountFormatterFlow, ::formatPendingOperationsCount) .shareInBackground() + val pezkuwiDashboardFlow = selectedMetaAccount + .mapLatest { metaAccount -> + pezkuwiDashboardInteractor.getDashboard(metaAccount) + .map { data -> + PezkuwiDashboardModel( + roles = data.roles, + trustScore = data.trustScore.toString(), + referralPoints = data.totalReferrals.toString(), + stakingPoints = data.stakedAmount.toString(), + perwerdePoints = data.perwerdePoints.toString() + ) + } + .getOrNull() + } + .shareInBackground() + init { selectedCurrency .onEach { fullSync() } @@ -381,6 +401,10 @@ class BalanceListViewModel( } } + fun basvuruClicked() { + showBrowser("https://t.me/pezkuwichainBot") + } + fun novaCardClicked() = launchUnit { novaCardRestrictionCheckMixin.checkRestrictionAndDo { router.openNovaCard() diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt index 741a769..5389db0 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt @@ -15,10 +15,16 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase +import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor +import io.novafoundation.nova.feature_assets.domain.dashboard.PezkuwiDashboardInteractor +import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import javax.inject.Named import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetListMixinFactory import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory @@ -79,6 +85,23 @@ class BalanceListModule { ) } + @Provides + @ScreenScope + fun providePezkuwiDashboardRepository( + @Named(REMOTE_STORAGE_SOURCE) remoteStorageDataSource: StorageDataSource + ): PezkuwiDashboardRepository { + return PezkuwiDashboardRepository(remoteStorageDataSource) + } + + @Provides + @ScreenScope + fun providePezkuwiDashboardInteractor( + repository: PezkuwiDashboardRepository, + chainRegistry: ChainRegistry + ): PezkuwiDashboardInteractor { + return PezkuwiDashboardInteractor(repository, chainRegistry) + } + @Provides @IntoMap @ViewModelKey(BalanceListViewModel::class) @@ -103,6 +126,7 @@ class BalanceListModule { maskingModeUseCase: MaskingModeUseCase, fiatFormatter: FiatFormatter, giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin, + pezkuwiDashboardInteractor: PezkuwiDashboardInteractor, ): ViewModel { return BalanceListViewModel( promotionBannersMixinFactory = promotionBannersMixinFactory, @@ -124,7 +148,8 @@ class BalanceListModule { novaCardRestrictionCheckMixin = novaCardRestrictionCheckMixin, maskingModeUseCase = maskingModeUseCase, fiatFormatter = fiatFormatter, - giftsRestrictionCheckMixin = giftsRestrictionCheckMixin + giftsRestrictionCheckMixin = giftsRestrictionCheckMixin, + pezkuwiDashboardInteractor = pezkuwiDashboardInteractor ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt new file mode 100644 index 0000000..a733505 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.model + +data class PezkuwiDashboardModel( + val roles: List, + val trustScore: String, + val referralPoints: String, + val stakingPoints: String, + val perwerdePoints: String +) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt new file mode 100644 index 0000000..ba1d458 --- /dev/null +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt @@ -0,0 +1,88 @@ +package io.novafoundation.nova.feature_assets.presentation.balance.list.view + +import android.content.res.ColorStateList +import android.graphics.Color +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import io.novafoundation.nova.common.list.SingleItemAdapter +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.utils.inflater +import io.novafoundation.nova.common.utils.recyclerView.WithViewType +import io.novafoundation.nova.feature_assets.R +import io.novafoundation.nova.feature_assets.databinding.ItemPezkuwiDashboardBinding +import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PezkuwiDashboardModel + +class PezkuwiDashboardAdapter( + private val handler: Handler +) : SingleItemAdapter(isShownByDefault = false) { + + interface Handler { + fun onBasvuruClicked() + } + + private var model: PezkuwiDashboardModel? = null + + fun setModel(model: PezkuwiDashboardModel) { + this.model = model + notifyChangedIfShown() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PezkuwiDashboardHolder { + val binding = ItemPezkuwiDashboardBinding.inflate(parent.inflater(), parent, false) + return PezkuwiDashboardHolder(binding, handler) + } + + override fun onBindViewHolder(holder: PezkuwiDashboardHolder, position: Int) { + model?.let { holder.bind(it) } + } + + override fun getItemViewType(position: Int): Int { + return PezkuwiDashboardHolder.viewType + } +} + +class PezkuwiDashboardHolder( + private val binder: ItemPezkuwiDashboardBinding, + handler: PezkuwiDashboardAdapter.Handler +) : RecyclerView.ViewHolder(binder.root) { + + companion object : WithViewType { + override val viewType: Int = R.layout.item_pezkuwi_dashboard + } + + init { + binder.pezkuwiDashboardBasvuruButton.setOnClickListener { handler.onBasvuruClicked() } + } + + fun bind(model: PezkuwiDashboardModel) { + bindRoles(model.roles) + binder.pezkuwiDashboardTrustValue.text = model.trustScore + binder.pezkuwiDashboardReferralValue.text = model.referralPoints + binder.pezkuwiDashboardStakingValue.text = model.stakingPoints + binder.pezkuwiDashboardPerwerdeValue.text = model.perwerdePoints + } + + private fun bindRoles(roles: List) { + val flexbox = binder.pezkuwiDashboardRoles + flexbox.removeAllViews() + + roles.forEach { role -> + val chip = Chip(flexbox.context).apply { + text = role + isClickable = false + isCheckable = false + setTextColor(Color.WHITE) + chipBackgroundColor = ColorStateList.valueOf(0x33FFFFFF) + chipStrokeWidth = 0f + val params = ViewGroup.MarginLayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.setMargins(0, 0, 8.dp(context), 4.dp(context)) + layoutParams = params + } + flexbox.addView(chip) + } + } +} diff --git a/feature-assets/src/main/res/drawable/bg_pezkuwi_dashboard.xml b/feature-assets/src/main/res/drawable/bg_pezkuwi_dashboard.xml new file mode 100644 index 0000000..17e46ef --- /dev/null +++ b/feature-assets/src/main/res/drawable/bg_pezkuwi_dashboard.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml b/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml new file mode 100644 index 0000000..c4d6609 --- /dev/null +++ b/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/BagListRepository.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/BagListRepository.kt index 92d2484..52a0998 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/BagListRepository.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/BagListRepository.kt @@ -38,6 +38,7 @@ suspend fun BagListRepository.bagListLocatorOrThrow(chainId: ChainId): BagListLo class LocalBagListRepository( private val localStorage: StorageDataSource, + private val remoteStorage: StorageDataSource, private val chainRegistry: ChainRegistry ) : BagListRepository { @@ -51,7 +52,7 @@ class LocalBagListRepository( override suspend fun bagListSize(chainId: ChainId): BigInteger? { return runCatching { - localStorage.query(chainId) { + remoteStorage.query(chainId) { runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.query(binding = ::bindNumber) } }.getOrNull() diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt index 15b9671..1c04de3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt @@ -346,7 +346,7 @@ class StakingRepositoryImpl( val runtime = runtimeFor(chainId) return runtime.metadata.staking().storageOrNull(storageName)?.let { storageEntry -> - localStorage.query( + remoteStorage.query( keyBuilder = { storageEntry.storageKey() }, binding = { scale, _ -> scale?.let { binder(scale, runtime, storageEntry.returnType()) } }, chainId = chainId diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt index aeca5c0..67c7775 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt @@ -225,8 +225,9 @@ class StakingFeatureModule { @FeatureScope fun provideBagListRepository( @Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource, + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, chainRegistry: ChainRegistry - ): BagListRepository = LocalBagListRepository(localStorageSource, chainRegistry) + ): BagListRepository = LocalBagListRepository(localStorageSource, remoteStorageSource, chainRegistry) @Provides @FeatureScope diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt index d17ad39..b7aafab 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/common/StakingSharedComputation.kt @@ -22,10 +22,12 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest @@ -61,7 +63,15 @@ class StakingSharedComputation( val key = "ACTIVE_ERA:$chainId" return computationalCache.useSharedFlow(key, scope) { - stakingRepository.observeActiveEraIndex(chainId) + flow { + Log.d("PEZ_STAKE", "activeEraFlow: fetching remote activeEra for chainId=$chainId") + val era = stakingRepository.getActiveEraIndex(chainId) + Log.d("PEZ_STAKE", "activeEraFlow: got remote activeEra=$era") + emit(era) + + Log.d("PEZ_STAKE", "activeEraFlow: starting local observation for chainId=$chainId") + emitAll(stakingRepository.observeActiveEraIndex(chainId)) + } } } @@ -70,7 +80,15 @@ class StakingSharedComputation( return computationalCache.useSharedFlow(key, scope) { activeEraFlow(chainId, scope).map { eraIndex -> - stakingRepository.getElectedValidatorsExposure(chainId, eraIndex) to eraIndex + Log.d("PEZ_STAKE", "electedExposures: fetching validators for chainId=$chainId, era=$eraIndex") + try { + val exposures = stakingRepository.getElectedValidatorsExposure(chainId, eraIndex) + Log.d("PEZ_STAKE", "electedExposures: got ${exposures.size} validators for chainId=$chainId") + exposures to eraIndex + } catch (e: Exception) { + Log.e("PEZ_STAKE", "electedExposures: FAILED for chainId=$chainId, era=$eraIndex", e) + throw e + } } } } @@ -79,24 +97,33 @@ class StakingSharedComputation( val key = "MIN_STAKE:$chainId" return computationalCache.useSharedFlow(key, scope) { - val minBond = stakingRepository.minimumNominatorBond(chainId) - val bagListLocator = bagListRepository.bagListLocatorOrNull(chainId) - val totalIssuance = totalIssuanceRepository.getTotalIssuance(chainId) - val bagListScoreConverter = BagListScoreConverter.U128(totalIssuance) - val maxElectingVoters = bagListRepository.maxElectingVotes(chainId) - val bagListSize = bagListRepository.bagListSize(chainId) - electedExposuresWithActiveEraFlow(chainId, scope).map { (exposures, activeEraIndex) -> - val minStake = minimumStake( - exposures = exposures.values, - minimumNominatorBond = minBond, - bagListLocator = bagListLocator, - bagListScoreConverter = bagListScoreConverter, - bagListSize = bagListSize, - maxElectingVoters = maxElectingVoters - ) + Log.d("PEZ_STAKE", "activeEraInfo: calculating minStake for chainId=$chainId, era=$activeEraIndex, validators=${exposures.size}") + try { + val minBond = stakingRepository.minimumNominatorBond(chainId) + Log.d("PEZ_STAKE", "activeEraInfo: minBond=$minBond") + val bagListLocator = bagListRepository.bagListLocatorOrNull(chainId) + val totalIssuance = totalIssuanceRepository.getTotalIssuance(chainId) + val bagListScoreConverter = BagListScoreConverter.U128(totalIssuance) + val maxElectingVoters = bagListRepository.maxElectingVotes(chainId) + val bagListSize = bagListRepository.bagListSize(chainId) + Log.d("PEZ_STAKE", "activeEraInfo: bagListSize=$bagListSize, maxElectingVoters=$maxElectingVoters") - ActiveEraInfo(activeEraIndex, exposures, minStake) + val minStake = minimumStake( + exposures = exposures.values, + minimumNominatorBond = minBond, + bagListLocator = bagListLocator, + bagListScoreConverter = bagListScoreConverter, + bagListSize = bagListSize, + maxElectingVoters = maxElectingVoters + ) + Log.d("PEZ_STAKE", "activeEraInfo: minStake=$minStake") + + ActiveEraInfo(activeEraIndex, exposures, minStake) + } catch (e: Exception) { + Log.e("PEZ_STAKE", "activeEraInfo: FAILED for chainId=$chainId", e) + throw e + } } } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt index 2c7aa85..3eca41c 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/nominationPools/common/rewards/RealNominationPoolRewardCalculator.kt @@ -33,20 +33,16 @@ class NominationPoolRewardCalculatorFactory( // For parachains, staking exposures live on the parent relay chain val exposureChainId = chain.parentId ?: chainId - android.util.Log.d("PEZ_STAKING", "NomPoolRewardCalcFactory.create() chainId=${chainId.take(12)} exposureChainId=${exposureChainId.take(12)}") - val delegateOption = stakingOption.unwrapNominationPools() val delegate = sharedStakingSharedComputation.rewardCalculator(delegateOption, sharedComputationScope) val allPoolAccounts = nominationPoolSharedComputation.allBondedPoolAccounts(chainId, sharedComputationScope) - android.util.Log.d("PEZ_STAKING", "Pool accounts: ${allPoolAccounts.size}") val poolCommissions = nominationPoolSharedComputation.allBondedPools(chainId, sharedComputationScope) .mapValues { (_, pool) -> pool.commission?.current?.perbill } val activeEra = stakingRepository.getActiveEraIndex(exposureChainId) val exposures = stakingRepository.getElectedValidatorsExposure(exposureChainId, activeEra) - android.util.Log.d("PEZ_STAKING", "NomPool exposures: ${exposures.size} (era=$activeEra)") return RealNominationPoolRewardCalculator( directStakingDelegate = delegate, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt index e0c6e5a..6e52c4a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/common/StartMultiStakingInteractor.kt @@ -26,7 +26,8 @@ class RealStartMultiStakingInteractor( override suspend fun calculateFee(selection: StartMultiStakingSelection): Fee { return withContext(Dispatchers.IO) { - extrinsicService.estimateFee(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) { + val chain = selection.stakingOption.chain + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { startStaking(selection) } } @@ -34,7 +35,8 @@ class RealStartMultiStakingInteractor( override suspend fun startStaking(selection: StartMultiStakingSelection): Result { return withContext(Dispatchers.IO) { - extrinsicService.submitExtrinsicAndAwaitExecution(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) { + val chain = selection.stakingOption.chain + extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) { startStaking(selection) }.requireOk() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt index ae82c35..f0d5f53 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingProperties.kt @@ -29,6 +29,7 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import android.util.Log import kotlinx.coroutines.CoroutineScope class DirectStakingPropertiesFactory( @@ -101,7 +102,10 @@ private class DirectStakingProperties( private val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id override suspend fun minStake(): Balance { - return stakingSharedComputation.minStake(stakingChainId, scope) + Log.d("PEZ_STAKE", "DirectStaking.minStake() called, stakingChainId=$stakingChainId") + val result = stakingSharedComputation.minStake(stakingChainId, scope) + Log.d("PEZ_STAKE", "DirectStaking.minStake() returned: $result") + return result } private fun StartMultiStakingValidationSystemBuilder.noConflictingStaking() { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingRecommendation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingRecommendation.kt index 56f1ad1..b50ecb1 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingRecommendation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/direct/DirectStakingRecommendation.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.recommendations.settin import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.StartMultiStakingSelection import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmount.SingleStakingRecommendation import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -20,21 +21,42 @@ class DirectStakingRecommendation( ) : SingleStakingRecommendation { private val recommendator = scope.async { - validatorRecommenderFactory.create(scope) + Log.d("PEZ_STAKE", "DirectRecommendation: creating validator recommender...") + try { + val result = validatorRecommenderFactory.create(scope) + Log.d("PEZ_STAKE", "DirectRecommendation: validator recommender created") + result + } catch (e: Exception) { + Log.e("PEZ_STAKE", "DirectRecommendation: validator recommender FAILED", e) + throw e + } } private val recommendationSettingsProvider = scope.async { - recommendationSettingsProviderFactory.create(scope) + Log.d("PEZ_STAKE", "DirectRecommendation: creating settings provider...") + try { + val result = recommendationSettingsProviderFactory.create(scope) + Log.d("PEZ_STAKE", "DirectRecommendation: settings provider created") + result + } catch (e: Exception) { + Log.e("PEZ_STAKE", "DirectRecommendation: settings provider FAILED", e) + throw e + } } override suspend fun recommendedSelection(stake: Balance): StartMultiStakingSelection { + Log.d("PEZ_STAKE", "DirectRecommendation: awaiting settings provider...") val provider = recommendationSettingsProvider.await() + Log.d("PEZ_STAKE", "DirectRecommendation: got settings provider") val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id val maximumValidatorsPerNominator = stakingConstantsRepository.maxValidatorsPerNominator(stakingChainId, stake) val recommendationSettings = provider.recommendedSettings(maximumValidatorsPerNominator) + Log.d("PEZ_STAKE", "DirectRecommendation: awaiting recommender...") val recommendator = recommendator.await() + Log.d("PEZ_STAKE", "DirectRecommendation: got recommender, getting recommendations...") val recommendedValidators = recommendator.recommendations(recommendationSettings) + Log.d("PEZ_STAKE", "DirectRecommendation: got ${recommendedValidators.size} recommended validators") return DirectStakingSelection( validators = recommendedValidators, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolRecommendation.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolRecommendation.kt index fce77ad..b060bc3 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolRecommendation.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/pools/NominationPoolRecommendation.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.pools. import io.novafoundation.nova.feature_staking_impl.domain.staking.start.common.selection.StartMultiStakingSelection import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmount.SingleStakingRecommendation import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -15,11 +16,24 @@ class NominationPoolRecommendation( ) : SingleStakingRecommendation { private val recommendator = scope.async { - nominationPoolRecommenderFactory.create(stakingOption, scope) + Log.d("PEZ_STAKE", "NomPoolRecommendation: creating recommender...") + try { + val result = nominationPoolRecommenderFactory.create(stakingOption, scope) + Log.d("PEZ_STAKE", "NomPoolRecommendation: recommender created successfully") + result + } catch (e: Exception) { + Log.e("PEZ_STAKE", "NomPoolRecommendation: recommender creation FAILED", e) + throw e + } } override suspend fun recommendedSelection(stake: Balance): StartMultiStakingSelection? { - val recommendedPool = recommendator.await().recommendedPool() ?: return null + Log.d("PEZ_STAKE", "NomPoolRecommendation: awaiting recommender...") + val recommendedPool = recommendator.await().recommendedPool() ?: run { + Log.d("PEZ_STAKE", "NomPoolRecommendation: no recommended pool found") + return null + } + Log.d("PEZ_STAKE", "NomPoolRecommendation: recommended pool=${recommendedPool.id}") return NominationPoolSelection(recommendedPool, stakingOption, stake) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/AutomaticMultiStakingSelectionType.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/AutomaticMultiStakingSelectionType.kt index f64b434..ef64ef0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/AutomaticMultiStakingSelectionType.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/staking/start/setupAmount/selectionType/AutomaticMultiStakingSelectionType.kt @@ -13,6 +13,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.staking.start.setupAmo import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import android.util.Log +import kotlin.coroutines.cancellation.CancellationException class AutomaticMultiStakingSelectionType( private val candidates: List, @@ -44,8 +46,14 @@ class AutomaticMultiStakingSelectionType( } override suspend fun updateSelectionFor(stake: Balance) { + Log.d("PEZ_STAKE", "updateSelectionFor: stake=$stake") val stakingProperties = typePropertiesFor(stake) - val candidates = stakingProperties.recommendation.recommendedSelection(stake) ?: return + Log.d("PEZ_STAKE", "updateSelectionFor: got properties type=${stakingProperties.stakingType}") + val candidates = stakingProperties.recommendation.recommendedSelection(stake) ?: run { + Log.d("PEZ_STAKE", "updateSelectionFor: recommendedSelection returned null, returning") + return + } + Log.d("PEZ_STAKE", "updateSelectionFor: got recommended selection") val recommendableSelection = RecommendableMultiStakingSelection( source = SelectionTypeSource.Automatic, @@ -54,10 +62,26 @@ class AutomaticMultiStakingSelectionType( ) selectionStore.updateSelection(recommendableSelection) + Log.d("PEZ_STAKE", "updateSelectionFor: selection updated successfully") } private suspend fun typePropertiesFor(stake: Balance): SingleStakingProperties { - return candidates.firstAllowingToStake(stake) ?: candidates.findWithMinimumStake() + Log.d("PEZ_STAKE", "typePropertiesFor: trying ${candidates.size} candidates") + for ((index, candidate) in candidates.withIndex()) { + Log.d("PEZ_STAKE", "typePropertiesFor: checking candidate $index type=${candidate.stakingType}") + try { + val minStake = candidate.minStake() + Log.d("PEZ_STAKE", "typePropertiesFor: candidate $index minStake=$minStake, stake=$stake, allows=${minStake <= stake}") + if (minStake <= stake) return candidate + } catch (e: CancellationException) { + Log.d("PEZ_STAKE", "typePropertiesFor: candidate $index cancelled, rethrowing") + throw e + } catch (e: Exception) { + Log.e("PEZ_STAKE", "typePropertiesFor: candidate $index minStake() threw", e) + } + } + Log.d("PEZ_STAKE", "typePropertiesFor: no candidate allows, finding minimum") + return candidates.findWithMinimumStake() } private suspend fun List.firstAllowingToStake(stake: Balance): SingleStakingProperties? {