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
This commit is contained in:
2026-02-17 00:10:23 +03:00
parent 9899bb5c40
commit 93e94cbf15
37 changed files with 741 additions and 48 deletions
+2 -2
View File
@@ -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 {
@@ -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>
): 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)
@@ -2044,4 +2044,11 @@
<string name="bridge_enter_amount">Ingresa la cantidad</string>
<string name="bridge_hez_to_dot_warning">Los intercambios HEZ→DOT pueden tener disponibilidad limitada según la liquidez.</string>
<string name="bridge_hez_to_dot_blocked">Los intercambios HEZ→DOT no están disponibles temporalmente. Inténtalo de nuevo cuando haya suficiente liquidez de DOT.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Puntuación de confianza</string>
<string name="pezkuwi_dashboard_referral">Referido</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Solicitar y Acciones</string>
</resources>
@@ -2044,4 +2044,11 @@
<string name="bridge_enter_amount">Entrez le montant</string>
<string name="bridge_hez_to_dot_warning">Les échanges HEZ→DOT peuvent avoir une disponibilité limitée selon la liquidité.</string>
<string name="bridge_hez_to_dot_blocked">Les échanges HEZ→DOT sont temporairement indisponibles. Réessayez lorsque la liquidité DOT sera suffisante.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Score de confiance</string>
<string name="pezkuwi_dashboard_referral">Parrainage</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Demande et Actions</string>
</resources>
@@ -2044,4 +2044,11 @@
<string name="bridge_enter_amount">Írd be az összeget</string>
<string name="bridge_hez_to_dot_warning">A HEZ→DOT cserék korlátozott elérhetőségűek lehetnek a likviditástól függően.</string>
<string name="bridge_hez_to_dot_blocked">A HEZ→DOT cserék ideiglenesen nem elérhetők. Próbáld újra, amikor elegendő DOT likviditás áll rendelkezésre.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Bizalmi pontszám</string>
<string name="pezkuwi_dashboard_referral">Ajánlás</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Jelentkezés és Műveletek</string>
</resources>
@@ -2030,4 +2030,11 @@
<string name="bridge_enter_amount">Masukkan jumlah</string>
<string name="bridge_hez_to_dot_warning">Penukaran HEZ→DOT mungkin terbatas tergantung pada likuiditas.</string>
<string name="bridge_hez_to_dot_blocked">Penukaran HEZ→DOT sementara tidak tersedia. Coba lagi saat likuiditas DOT mencukupi.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Skor Kepercayaan</string>
<string name="pezkuwi_dashboard_referral">Referral</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Ajukan &amp; Tindakan</string>
</resources>
@@ -2044,4 +2044,11 @@
<string name="bridge_enter_amount">Inserisci importo</string>
<string name="bridge_hez_to_dot_warning">Gli scambi HEZ→DOT potrebbero avere disponibilità limitata in base alla liquidità.</string>
<string name="bridge_hez_to_dot_blocked">Gli scambi HEZ→DOT sono temporaneamente non disponibili. Riprova quando la liquidità DOT sarà sufficiente.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Punteggio di fiducia</string>
<string name="pezkuwi_dashboard_referral">Referral</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Richiesta e Azioni</string>
</resources>
@@ -2030,4 +2030,11 @@
<string name="bridge_enter_amount">金額を入力</string>
<string name="bridge_hez_to_dot_warning">HEZ→DOT交換は流動性により制限される場合があります。</string>
<string name="bridge_hez_to_dot_blocked">HEZ→DOT交換は一時的に利用できません。DOTの流動性が十分になったら再試行してください。</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">信頼スコア</string>
<string name="pezkuwi_dashboard_referral">紹介</string>
<string name="pezkuwi_dashboard_staking">ステーキング</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">申請とアクション</string>
</resources>
@@ -2030,4 +2030,11 @@
<string name="bridge_enter_amount">금액 입력</string>
<string name="bridge_hez_to_dot_warning">HEZ→DOT 교환은 유동성에 따라 제한될 수 있습니다.</string>
<string name="bridge_hez_to_dot_blocked">HEZ→DOT 교환은 일시적으로 사용할 수 없습니다. DOT 유동성이 충분해지면 다시 시도하세요.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">신뢰 점수</string>
<string name="pezkuwi_dashboard_referral">추천</string>
<string name="pezkuwi_dashboard_staking">스테이킹</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">신청 및 작업</string>
</resources>
@@ -2757,4 +2757,11 @@
<string name="bridge_enter_amount">Mîqdar binivîse</string>
<string name="bridge_hez_to_dot_warning">Guherandina HEZ→DOT li gorî rewşa DOT sînordar dibe.</string>
<string name="bridge_hez_to_dot_blocked">Guherandina HEZ→DOT niha tune. Dema DOT têr bibe dîsa biceribîne.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Pûana Pêbaweriyê</string>
<string name="pezkuwi_dashboard_referral">Referral</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Serlêdan û Karên</string>
</resources>
@@ -2072,4 +2072,11 @@
<string name="bridge_enter_amount">Wprowadź kwotę</string>
<string name="bridge_hez_to_dot_warning">Wymiany HEZ→DOT mogą mieć ograniczoną dostępność w zależności od płynności.</string>
<string name="bridge_hez_to_dot_blocked">Wymiany HEZ→DOT są tymczasowo niedostępne. Spróbuj ponownie, gdy płynność DOT będzie wystarczająca.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Wynik zaufania</string>
<string name="pezkuwi_dashboard_referral">Polecenie</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Wniosek i Akcje</string>
</resources>
@@ -2044,4 +2044,11 @@
<string name="bridge_enter_amount">Digite o valor</string>
<string name="bridge_hez_to_dot_warning">As trocas HEZ→DOT podem ter disponibilidade limitada dependendo da liquidez.</string>
<string name="bridge_hez_to_dot_blocked">As trocas HEZ→DOT estão temporariamente indisponíveis. Tente novamente quando houver liquidez suficiente de DOT.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Pontuação de confiança</string>
<string name="pezkuwi_dashboard_referral">Indicação</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Candidatura e Ações</string>
</resources>
@@ -2072,4 +2072,11 @@
<string name="bridge_enter_amount">Введите сумму</string>
<string name="bridge_hez_to_dot_warning">Обмен HEZ→DOT может быть ограничен в зависимости от ликвидности.</string>
<string name="bridge_hez_to_dot_blocked">Обмен HEZ→DOT временно недоступен. Повторите попытку при достаточной ликвидности DOT.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Рейтинг доверия</string>
<string name="pezkuwi_dashboard_referral">Реферал</string>
<string name="pezkuwi_dashboard_staking">Стейкинг</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Заявка и Действия</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Güven Puanı</string>
<string name="pezkuwi_dashboard_referral">Referans</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Başvuru ve İşlemler</string>
</resources>
@@ -2030,4 +2030,11 @@
<string name="bridge_enter_amount">Nhập số tiền</string>
<string name="bridge_hez_to_dot_warning">Giao dịch HEZ→DOT có thể bị hạn chế tùy thuộc vào thanh khoản.</string>
<string name="bridge_hez_to_dot_blocked">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 đủ.</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Điểm tin cậy</string>
<string name="pezkuwi_dashboard_referral">Giới thiệu</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Đăng ký &amp; Hành động</string>
</resources>
@@ -2030,4 +2030,11 @@
<string name="bridge_enter_amount">输入金额</string>
<string name="bridge_hez_to_dot_warning">HEZ→DOT兑换可能因流动性而受限。</string>
<string name="bridge_hez_to_dot_blocked">HEZ→DOT兑换暂时不可用。请在DOT流动性充足时重试。</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">信任评分</string>
<string name="pezkuwi_dashboard_referral">推荐</string>
<string name="pezkuwi_dashboard_staking">质押</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">申请与操作</string>
</resources>
+7
View File
@@ -2759,4 +2759,11 @@
<string name="yesterday">Yesterday</string>
<string name="branch_io_link_host" translatable="false">pezkuwi-wallet.app.link</string>
<string name="branch_io_link_host_alternate" translatable="false">pezkuwi-wallet-alternate.app.link</string>
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
<string name="pezkuwi_dashboard_trust_score">Trust Score</string>
<string name="pezkuwi_dashboard_referral">Referral</string>
<string name="pezkuwi_dashboard_staking">Staking</string>
<string name="pezkuwi_dashboard_perwerde">Perwerde</string>
<string name="pezkuwi_dashboard_basvuru">Apply &amp; Actions</string>
</resources>
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_assets.data.model
import java.math.BigInteger
data class PezkuwiDashboardData(
val roles: List<String>,
val trustScore: BigInteger,
val totalReferrals: Int,
val stakedAmount: BigInteger,
val perwerdePoints: Int
)
@@ -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<String> = 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)
}
@@ -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<PezkuwiDashboardData> = runCatching {
val peopleChain = chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
val accountId = metaAccount.accountIdIn(peopleChain)
?: error("No account for People chain")
repository.getDashboard(accountId)
}
}
@@ -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<BalanceListViewModel, FragmentBalanceListBinding>(),
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))
@@ -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()
@@ -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
)
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_assets.presentation.balance.list.model
data class PezkuwiDashboardModel(
val roles: List<String>,
val trustScore: String,
val referralPoints: String,
val stakingPoints: String,
val perwerdePoints: String
)
@@ -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<PezkuwiDashboardHolder>(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<String>) {
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)
}
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:startColor="#FF1A237E"
android:centerColor="#FF283593"
android:endColor="#FF3949AB"
android:type="linear" />
<corners android:radius="16dp" />
</shape>
@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_pezkuwi_dashboard"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/pezkuwiDashboardTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pezkuwi_dashboard_title"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/pezkuwiDashboardRoles"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:flexWrap="wrap"
app:alignItems="center"
app:justifyContent="flex_start" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pezkuwi_dashboard_trust_score"
android:textColor="#B0BEC5"
android:textSize="12sp" />
<TextView
android:id="@+id/pezkuwiDashboardTrustValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pezkuwi_dashboard_referral"
android:textColor="#B0BEC5"
android:textSize="12sp" />
<TextView
android:id="@+id/pezkuwiDashboardReferralValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pezkuwi_dashboard_staking"
android:textColor="#B0BEC5"
android:textSize="12sp" />
<TextView
android:id="@+id/pezkuwiDashboardStakingValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pezkuwi_dashboard_perwerde"
android:textColor="#B0BEC5"
android:textSize="12sp" />
<TextView
android:id="@+id/pezkuwiDashboardPerwerdeValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/pezkuwiDashboardBasvuruButton"
style="@style/Widget.Nova.MaterialButton.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/pezkuwi_dashboard_basvuru"
android:textAllCaps="false" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -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()
@@ -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
@@ -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
@@ -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
}
}
}
}
@@ -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,
@@ -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<ExtrinsicExecutionResult> {
return withContext(Dispatchers.IO) {
extrinsicService.submitExtrinsicAndAwaitExecution(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) {
val chain = selection.stakingOption.chain
extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) {
startStaking(selection)
}.requireOk()
}
@@ -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() {
@@ -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,
@@ -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)
}
@@ -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<SingleStakingProperties>,
@@ -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<SingleStakingProperties>.firstAllowingToStake(stake: Balance): SingleStakingProperties? {