mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-21 23:48:00 +00:00
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:
+2
-2
@@ -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 {
|
||||
|
||||
+16
-7
@@ -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 & 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ý & 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>
|
||||
|
||||
@@ -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 & Actions</string>
|
||||
</resources>
|
||||
|
||||
+11
@@ -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
|
||||
)
|
||||
+119
@@ -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)
|
||||
}
|
||||
+21
@@ -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)
|
||||
}
|
||||
}
|
||||
+27
-2
@@ -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))
|
||||
|
||||
+26
-2
@@ -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()
|
||||
|
||||
+26
-1
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -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
|
||||
)
|
||||
+88
@@ -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>
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+45
-18
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -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,
|
||||
|
||||
+4
-2
@@ -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()
|
||||
}
|
||||
|
||||
+5
-1
@@ -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() {
|
||||
|
||||
+24
-2
@@ -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,
|
||||
|
||||
+16
-2
@@ -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)
|
||||
}
|
||||
|
||||
+26
-2
@@ -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? {
|
||||
|
||||
Reference in New Issue
Block a user