mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
Fix Polkadot staking, add USDT bridge, update dashboard card
- Restore staking module to Nova upstream (fix Polkadot ACTIVE/shimmer) - Add USDT(DOT) <-> USDT(HEZ) bridge option to Buy/Sell screen - Dashboard card: add telegram_welcome image, info text, Non-Citizen role - Add non-citizen info text to all 12 locale files - Simplify ComputationalCache (remove stale scope recreation)
This commit is contained in:
+7
-16
@@ -6,7 +6,6 @@ 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
|
||||
@@ -14,7 +13,6 @@ 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
|
||||
@@ -74,25 +72,18 @@ internal class RealComputationalCache : ComputationalCache, CoroutineScope by Co
|
||||
cachedAction: AwaitableConstructor<T>
|
||||
): T {
|
||||
val awaitable = mutex.withLock {
|
||||
val existing = memory[key]
|
||||
if (existing != null && existing.aggregateScope.isActive) {
|
||||
if (key in memory) {
|
||||
Log.d(LOG_TAG, "Key $key requested - already present")
|
||||
|
||||
existing.dependents += scope
|
||||
val entry = memory.getValue(key)
|
||||
|
||||
existing.awaitable
|
||||
entry.dependents += scope
|
||||
|
||||
entry.awaitable
|
||||
} else {
|
||||
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")
|
||||
}
|
||||
Log.d(LOG_TAG, "Key $key requested - creating new operation")
|
||||
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e(LOG_TAG, "Key $key - upstream error in aggregateScope", throwable)
|
||||
}
|
||||
val aggregateScope = CoroutineScope(Dispatchers.Default + exceptionHandler)
|
||||
val aggregateScope = CoroutineScope(Dispatchers.Default)
|
||||
val awaitable = cachedAction(aggregateScope)
|
||||
|
||||
memory[key] = Entry(dependents = mutableSetOf(scope), aggregateScope, awaitable)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 388 KiB |
@@ -2029,6 +2029,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Puente DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Puente USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Puente DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Envías</string>
|
||||
<string name="bridge_you_receive">Recibes (estimado)</string>
|
||||
@@ -2048,4 +2049,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Puntuación de confianza</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Solicitar y Acciones</string>
|
||||
<string name="pezkuwi_dashboard_info">Usa nuestro Telegram MiniApp para servicios de ciudadanía del Kurdistán Digital.\n\nPara ganar recompensas PEZ, debes tener un ticket Welatî y haber apostado al menos 10 HEZ.\n\nLos no ciudadanos solo pueden beneficiarse de las recompensas HEZ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2029,6 +2029,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Pont DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Pont USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Pont DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Vous envoyez</string>
|
||||
<string name="bridge_you_receive">Vous recevez (estimé)</string>
|
||||
@@ -2048,4 +2049,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Score de confiance</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Demande et Actions</string>
|
||||
<string name="pezkuwi_dashboard_info">Utilisez notre Telegram MiniApp pour les services de citoyenneté du Kurdistan numérique.\n\nPour gagner des récompenses PEZ, vous devez détenir un ticket Welatî et avoir staké au moins 10 HEZ.\n\nLes non-citoyens ne peuvent bénéficier que des récompenses HEZ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2029,6 +2029,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">DOT ↔ HEZ híd</string>
|
||||
<string name="wallet_asset_bridge_usdt">USDT(DOT) ↔ USDT(HEZ) híd</string>
|
||||
<string name="bridge_title">DOT ↔ HEZ híd</string>
|
||||
<string name="bridge_you_send">Küldöd</string>
|
||||
<string name="bridge_you_receive">Kapod (becsült)</string>
|
||||
|
||||
@@ -2015,6 +2015,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Jembatan DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Jembatan USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Jembatan DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Anda kirim</string>
|
||||
<string name="bridge_you_receive">Anda terima (perkiraan)</string>
|
||||
@@ -2034,4 +2035,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Skor Kepercayaan</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Ajukan & Tindakan</string>
|
||||
<string name="pezkuwi_dashboard_info">Gunakan Telegram MiniApp kami untuk layanan kewarganegaraan Kurdistan Digital.\n\nUntuk mendapatkan hadiah PEZ, Anda harus memiliki tiket Welatî dan telah staking minimal 10 HEZ.\n\nNon-warga negara hanya dapat memperoleh manfaat dari hadiah HEZ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2029,6 +2029,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Ponte DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Ponte USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Ponte DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Invii</string>
|
||||
<string name="bridge_you_receive">Ricevi (stimato)</string>
|
||||
|
||||
@@ -2015,6 +2015,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">DOT ↔ HEZ ブリッジ</string>
|
||||
<string name="wallet_asset_bridge_usdt">USDT(DOT) ↔ USDT(HEZ) ブリッジ</string>
|
||||
<string name="bridge_title">DOT ↔ HEZ ブリッジ</string>
|
||||
<string name="bridge_you_send">送金額</string>
|
||||
<string name="bridge_you_receive">受取額(概算)</string>
|
||||
@@ -2034,4 +2035,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">信頼スコア</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">申請とアクション</string>
|
||||
<string name="pezkuwi_dashboard_info">デジタルクルディスタンの市民サービスにはTelegram MiniAppをご利用ください。\n\nPEZ報酬を獲得するには、Welatîチケットを保有し、最低10 HEZをステーキングする必要があります。\n\n非市民はHEZ報酬のみを受け取ることができます。</string>
|
||||
</resources>
|
||||
|
||||
@@ -2015,6 +2015,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">DOT ↔ HEZ 브릿지</string>
|
||||
<string name="wallet_asset_bridge_usdt">USDT(DOT) ↔ USDT(HEZ) 브릿지</string>
|
||||
<string name="bridge_title">DOT ↔ HEZ 브릿지</string>
|
||||
<string name="bridge_you_send">보내는 금액</string>
|
||||
<string name="bridge_you_receive">받는 금액 (예상)</string>
|
||||
@@ -2034,4 +2035,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">신뢰 점수</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">신청 및 작업</string>
|
||||
<string name="pezkuwi_dashboard_info">디지털 쿠르디스탄 시민권 서비스를 위해 Telegram MiniApp을 사용하세요.\n\nPEZ 보상을 받으려면 Welatî 티켓을 보유하고 최소 10 HEZ를 스테이킹해야 합니다.\n\n비시민권자는 HEZ 보상만 받을 수 있습니다.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2742,6 +2742,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Pira DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Pira USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Pira DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Tu dişînî</string>
|
||||
<string name="bridge_you_receive">Tu distînî (texmîn)</string>
|
||||
@@ -2761,4 +2762,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Pûana Pêbaweriyê</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Serlêdan û Karên</string>
|
||||
<string name="pezkuwi_dashboard_info">Ji bo karên hemwelatîbûna Kurdistana Dîjîtal MiniApp\'a me ya Telegram bikar bînin.\n\nJi bo qezenckirina xelatên PEZ, divê hûn xwediyê bilêta Welatî bin û herî kêm 10 HEZ stake kiribe bin.\n\nKesên ne-hemwelatî tenê dikarin ji xelatên HEZ sûd werbigirin.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2057,6 +2057,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Most DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Most USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Most DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Wysyłasz</string>
|
||||
<string name="bridge_you_receive">Otrzymasz (szacunkowo)</string>
|
||||
|
||||
@@ -2029,6 +2029,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Ponte DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Ponte USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Ponte DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Você envia</string>
|
||||
<string name="bridge_you_receive">Você recebe (estimado)</string>
|
||||
@@ -2048,4 +2049,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Pontuação de confiança</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Candidatura e Ações</string>
|
||||
<string name="pezkuwi_dashboard_info">Use nosso Telegram MiniApp para serviços de cidadania do Curdistão Digital.\n\nPara ganhar recompensas PEZ, você deve possuir um bilhete Welatî e ter pelo menos 10 HEZ em stake.\n\nNão-cidadãos só podem se beneficiar das recompensas HEZ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2057,6 +2057,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Мост DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Мост USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Мост DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Вы отправляете</string>
|
||||
<string name="bridge_you_receive">Вы получите (примерно)</string>
|
||||
@@ -2076,4 +2077,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Рейтинг доверия</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Заявка и Действия</string>
|
||||
<string name="pezkuwi_dashboard_info">Используйте наше Telegram MiniApp для услуг цифрового гражданства Курдистана.\n\nДля получения наград PEZ необходимо иметь билет Welatî и застейкать минимум 10 HEZ.\n\nНе-граждане могут получать только награды HEZ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Güven Puanı</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Başvuru ve İşlemler</string>
|
||||
<string name="pezkuwi_dashboard_info">Dijital Kurdistan vatandaşlık işlemleri için Telegram MiniApp\'imizi kullanın.\n\nPEZ ödülleri kazanmak için Welatî tikesi sahibi olmanız ve en az 10 HEZ stake etmiş olmanız gereklidir.\n\nVatandaş olmayanlar yalnızca HEZ ödüllerinden yararlanabilir.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2015,6 +2015,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">Cầu nối DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Cầu nối USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
<string name="bridge_title">Cầu nối DOT ↔ HEZ</string>
|
||||
<string name="bridge_you_send">Bạn gửi</string>
|
||||
<string name="bridge_you_receive">Bạn nhận (ước tính)</string>
|
||||
@@ -2034,4 +2035,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Điểm tin cậy</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Đăng ký & Hành động</string>
|
||||
<string name="pezkuwi_dashboard_info">Sử dụng Telegram MiniApp của chúng tôi cho dịch vụ công dân Kurdistan kỹ thuật số.\n\nĐể nhận phần thưởng PEZ, bạn phải sở hữu vé Welatî và đã stake ít nhất 10 HEZ.\n\nNgười không phải công dân chỉ có thể hưởng lợi từ phần thưởng HEZ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2015,6 +2015,7 @@
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="wallet_asset_bridge">DOT ↔ HEZ 跨链桥</string>
|
||||
<string name="wallet_asset_bridge_usdt">USDT(DOT) ↔ USDT(HEZ) 跨链桥</string>
|
||||
<string name="bridge_title">DOT ↔ HEZ 跨链桥</string>
|
||||
<string name="bridge_you_send">发送</string>
|
||||
<string name="bridge_you_receive">接收(预计)</string>
|
||||
@@ -2034,4 +2035,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">信任评分</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">申请与操作</string>
|
||||
<string name="pezkuwi_dashboard_info">使用我们的 Telegram MiniApp 进行数字库尔德斯坦公民服务。\n\n要获得 PEZ 奖励,您必须持有 Welatî 票并至少质押 10 HEZ。\n\n非公民只能从 HEZ 奖励中受益。</string>
|
||||
</resources>
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
<string name="wallet_asset_sell_tokens">Sell tokens</string>
|
||||
<string name="wallet_asset_buy_tokens">Buy tokens</string>
|
||||
<string name="wallet_asset_bridge">Bridge DOT ↔ HEZ</string>
|
||||
<string name="wallet_asset_bridge_usdt">Bridge USDT(DOT) ↔ USDT(HEZ)</string>
|
||||
|
||||
<!-- Bridge Screen -->
|
||||
<string name="bridge_title">DOT ↔ HEZ Bridge</string>
|
||||
@@ -368,6 +369,7 @@
|
||||
<string name="bridge_enter_amount">Enter amount</string>
|
||||
<string name="bridge_hez_to_dot_warning">HEZ→DOT swaps may have limited availability based on current liquidity.</string>
|
||||
<string name="bridge_hez_to_dot_blocked">HEZ→DOT swaps are temporarily unavailable. Please try again when DOT liquidity is sufficient.</string>
|
||||
<string name="bridge_wusdt_to_usdt_blocked">USDT(Pez)→USDT(Pol) swaps are temporarily unavailable. Waiting for 1:1 liquidity to be established.</string>
|
||||
|
||||
<string name="wallet_asset_buy_sell">Buy/Sell</string>
|
||||
|
||||
@@ -2763,4 +2765,5 @@
|
||||
<string name="pezkuwi_dashboard_title">Pezkuwi</string>
|
||||
<string name="pezkuwi_dashboard_trust_score">Trust Score</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Apply & Actions</string>
|
||||
<string name="pezkuwi_dashboard_info">Use our Telegram MiniApp for Digital Kurdistan citizenship services.\n\nTo earn PEZ rewards, you must hold a Welatî ticket and stake at least 10 HEZ.\n\nNon-citizens can only benefit from HEZ rewards.</string>
|
||||
</resources>
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class PezkuwiDashboardRepository(
|
||||
val trustScore = queryTrustScore(chainId, accountId)
|
||||
|
||||
return PezkuwiDashboardData(
|
||||
roles = roles,
|
||||
roles = roles.ifEmpty { listOf("Non-Citizen") },
|
||||
trustScore = trustScore
|
||||
)
|
||||
}
|
||||
|
||||
+12
-1
@@ -75,7 +75,8 @@ class RealBuySellSelectorMixin(
|
||||
private suspend fun openAllAssetsSelector() = BuySellSelectorMixin.SelectorPayload(
|
||||
buyItem(enabled = true) { router.openBuyFlow() },
|
||||
sellItem(enabled = buySellRestrictionCheckMixin.isAllowed()) { router.openSellFlow() },
|
||||
bridgeItem(enabled = true) { router.openBridgeFlow() }
|
||||
bridgeItem(enabled = true) { router.openBridgeFlow() },
|
||||
bridgeUsdtItem(enabled = true) { router.openBridgeFlow() }
|
||||
)
|
||||
|
||||
private suspend fun openSpecifiedAssetSelector(selectorType: SelectorType.Asset): BuySellSelectorMixin.SelectorPayload? {
|
||||
@@ -125,6 +126,16 @@ class RealBuySellSelectorMixin(
|
||||
)
|
||||
}
|
||||
|
||||
private fun bridgeUsdtItem(enabled: Boolean, action: () -> Unit): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_bridge,
|
||||
if (enabled) R.color.icon_primary else R.color.icon_inactive,
|
||||
R.string.wallet_asset_bridge_usdt,
|
||||
if (enabled) R.color.text_primary else R.color.button_text_inactive,
|
||||
action
|
||||
)
|
||||
}
|
||||
|
||||
private fun sellErrorAction(): () -> Unit = {
|
||||
coroutineScope.launch {
|
||||
buySellRestrictionCheckMixin.checkRestrictionAndDo {
|
||||
|
||||
+78
-23
@@ -18,13 +18,22 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
override fun initViews() {
|
||||
binder.bridgeToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
// Direction toggle
|
||||
binder.bridgeDirectionDotToHez.setOnClickListener {
|
||||
viewModel.setDirection(BridgeDirection.DOT_TO_HEZ)
|
||||
// Pair selector
|
||||
binder.bridgePairDotHez.setOnClickListener {
|
||||
viewModel.setPair(BridgePair.DOT_HEZ)
|
||||
}
|
||||
|
||||
binder.bridgeDirectionHezToDot.setOnClickListener {
|
||||
viewModel.setDirection(BridgeDirection.HEZ_TO_DOT)
|
||||
binder.bridgePairUsdt.setOnClickListener {
|
||||
viewModel.setPair(BridgePair.USDT)
|
||||
}
|
||||
|
||||
// Direction toggle
|
||||
binder.bridgeDirectionLeft.setOnClickListener {
|
||||
viewModel.setDirectionLeft()
|
||||
}
|
||||
|
||||
binder.bridgeDirectionRight.setOnClickListener {
|
||||
viewModel.setDirectionRight()
|
||||
}
|
||||
|
||||
// Amount input
|
||||
@@ -54,6 +63,10 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: BridgeViewModel) {
|
||||
viewModel.pair.observe { pair ->
|
||||
updatePairUI(pair)
|
||||
}
|
||||
|
||||
viewModel.direction.observe { direction ->
|
||||
updateDirectionUI(direction)
|
||||
}
|
||||
@@ -74,11 +87,11 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
binder.bridgeSwapButton.setState(state)
|
||||
}
|
||||
|
||||
viewModel.showHezToDotWarning.observe { show ->
|
||||
viewModel.showWarning.observe { show ->
|
||||
binder.bridgeHezToDotWarning.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
viewModel.hezToDotBlocked.observe { blocked ->
|
||||
viewModel.warningBlocked.observe { blocked ->
|
||||
if (blocked) {
|
||||
binder.bridgeHezToDotWarning.setBackgroundColor(resources.getColor(R.color.error_block_background, null))
|
||||
} else {
|
||||
@@ -86,40 +99,82 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.blockReason.observe { reason ->
|
||||
if (reason.isNotEmpty()) {
|
||||
binder.bridgeHezToDotWarning.text = reason
|
||||
} else {
|
||||
binder.bridgeHezToDotWarning.text = getString(R.string.bridge_hez_to_dot_warning)
|
||||
viewModel.warningText.observe { text ->
|
||||
if (text.isNotEmpty()) {
|
||||
binder.bridgeHezToDotWarning.text = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePairUI(pair: BridgePair) {
|
||||
when (pair) {
|
||||
BridgePair.DOT_HEZ -> {
|
||||
binder.bridgePairDotHez.setBackgroundResource(R.drawable.bg_button_primary)
|
||||
binder.bridgePairDotHez.setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
binder.bridgePairUsdt.background = null
|
||||
binder.bridgePairUsdt.setTextColor(resources.getColor(R.color.text_secondary, null))
|
||||
}
|
||||
BridgePair.USDT -> {
|
||||
binder.bridgePairUsdt.setBackgroundResource(R.drawable.bg_button_primary)
|
||||
binder.bridgePairUsdt.setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
binder.bridgePairDotHez.background = null
|
||||
binder.bridgePairDotHez.setTextColor(resources.getColor(R.color.text_secondary, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDirectionUI(direction: BridgeDirection) {
|
||||
val isLeft = direction == BridgeDirection.DOT_TO_HEZ || direction == BridgeDirection.USDT_TO_WUSDT
|
||||
|
||||
if (isLeft) {
|
||||
binder.bridgeDirectionLeft.setBackgroundResource(R.drawable.bg_button_primary)
|
||||
binder.bridgeDirectionLeft.setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
binder.bridgeDirectionRight.background = null
|
||||
binder.bridgeDirectionRight.setTextColor(resources.getColor(R.color.text_secondary, null))
|
||||
} else {
|
||||
binder.bridgeDirectionRight.setBackgroundResource(R.drawable.bg_button_primary)
|
||||
binder.bridgeDirectionRight.setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
binder.bridgeDirectionLeft.background = null
|
||||
binder.bridgeDirectionLeft.setTextColor(resources.getColor(R.color.text_secondary, null))
|
||||
}
|
||||
|
||||
when (direction) {
|
||||
BridgeDirection.DOT_TO_HEZ -> {
|
||||
binder.bridgeDirectionDotToHez.setBackgroundResource(R.drawable.bg_button_primary)
|
||||
binder.bridgeDirectionDotToHez.setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
binder.bridgeDirectionHezToDot.background = null
|
||||
binder.bridgeDirectionHezToDot.setTextColor(resources.getColor(R.color.text_secondary, null))
|
||||
|
||||
binder.bridgeDirectionLeft.text = "DOT → HEZ"
|
||||
binder.bridgeDirectionRight.text = "HEZ → DOT"
|
||||
binder.bridgeFromToken.text = "DOT"
|
||||
binder.bridgeToToken.text = "HEZ"
|
||||
}
|
||||
BridgeDirection.HEZ_TO_DOT -> {
|
||||
binder.bridgeDirectionHezToDot.setBackgroundResource(R.drawable.bg_button_primary)
|
||||
binder.bridgeDirectionHezToDot.setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
binder.bridgeDirectionDotToHez.background = null
|
||||
binder.bridgeDirectionDotToHez.setTextColor(resources.getColor(R.color.text_secondary, null))
|
||||
|
||||
binder.bridgeDirectionLeft.text = "DOT → HEZ"
|
||||
binder.bridgeDirectionRight.text = "HEZ → DOT"
|
||||
binder.bridgeFromToken.text = "HEZ"
|
||||
binder.bridgeToToken.text = "DOT"
|
||||
}
|
||||
BridgeDirection.USDT_TO_WUSDT -> {
|
||||
binder.bridgeDirectionLeft.text = "USDT(Pol) → USDT(Pez)"
|
||||
binder.bridgeDirectionRight.text = "USDT(Pez) → USDT(Pol)"
|
||||
binder.bridgeFromToken.text = "USDT"
|
||||
binder.bridgeToToken.text = "USDT"
|
||||
}
|
||||
BridgeDirection.WUSDT_TO_USDT -> {
|
||||
binder.bridgeDirectionLeft.text = "USDT(Pol) → USDT(Pez)"
|
||||
binder.bridgeDirectionRight.text = "USDT(Pez) → USDT(Pol)"
|
||||
binder.bridgeFromToken.text = "USDT"
|
||||
binder.bridgeToToken.text = "USDT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BridgePair {
|
||||
DOT_HEZ,
|
||||
USDT
|
||||
}
|
||||
|
||||
enum class BridgeDirection {
|
||||
DOT_TO_HEZ,
|
||||
HEZ_TO_DOT
|
||||
HEZ_TO_DOT,
|
||||
USDT_TO_WUSDT,
|
||||
WUSDT_TO_USDT
|
||||
}
|
||||
|
||||
+166
-129
@@ -28,34 +28,30 @@ class BridgeViewModel(
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
// Bridge wallet account ID (derived from seed, same on all chains)
|
||||
// Address: 5C5CW7xDmiXtCgfUCbKFF4ViJuCJJQpDZqWQ1mSTjehGzE3p (generic format)
|
||||
private const val BRIDGE_ADDRESS_GENERIC = "5C5CW7xDmiXtCgfUCbKFF4ViJuCJJQpDZqWQ1mSTjehGzE3p"
|
||||
|
||||
// Chain IDs
|
||||
val POLKADOT_ASSET_HUB_ID = ChainGeneses.POLKADOT_ASSET_HUB
|
||||
val PEZKUWI_ASSET_HUB_ID = ChainGeneses.PEZKUWI_ASSET_HUB
|
||||
|
||||
// Utility asset ID (native token)
|
||||
const val UTILITY_ASSET_ID = 0
|
||||
|
||||
// Fallback rate: 1 DOT = 3 HEZ (only if CoinGecko unavailable)
|
||||
// USDT asset IDs in chain config
|
||||
const val POLKADOT_USDT_ASSET_ID = 1 // assetId in chains.json for Polkadot AH
|
||||
const val PEZKUWI_USDT_ASSET_ID = 1000 // assetId in chains.json for Pezkuwi AH
|
||||
|
||||
const val FALLBACK_RATE = 3.0
|
||||
|
||||
// Fee: 0.1%
|
||||
const val FEE_PERCENT = 0.001
|
||||
|
||||
// Minimums
|
||||
const val MIN_DOT = 0.1
|
||||
const val MIN_HEZ = 0.3
|
||||
const val MIN_USDT = 1.0
|
||||
|
||||
// CoinGecko API
|
||||
const val COINGECKO_API = "https://api.coingecko.com/api/v3/simple/price?ids=polkadot,hezkurd&vs_currencies=usd"
|
||||
|
||||
// Bridge Status API
|
||||
const val BRIDGE_STATUS_API = "http://217.77.6.126:3030/status"
|
||||
}
|
||||
|
||||
private val _pair = MutableLiveData(BridgePair.DOT_HEZ)
|
||||
val pair: LiveData<BridgePair> = _pair
|
||||
|
||||
private val _direction = MutableLiveData(BridgeDirection.DOT_TO_HEZ)
|
||||
val direction: LiveData<BridgeDirection> = _direction
|
||||
|
||||
@@ -71,122 +67,64 @@ class BridgeViewModel(
|
||||
private val _buttonState = MutableLiveData<ButtonState>()
|
||||
val buttonState: LiveData<ButtonState> = _buttonState
|
||||
|
||||
private val _showHezToDotWarning = MutableLiveData(false)
|
||||
val showHezToDotWarning: LiveData<Boolean> = _showHezToDotWarning
|
||||
private val _showWarning = MutableLiveData(false)
|
||||
val showWarning: LiveData<Boolean> = _showWarning
|
||||
|
||||
private val _hezToDotBlocked = MutableLiveData(false)
|
||||
val hezToDotBlocked: LiveData<Boolean> = _hezToDotBlocked
|
||||
private val _warningBlocked = MutableLiveData(false)
|
||||
val warningBlocked: LiveData<Boolean> = _warningBlocked
|
||||
|
||||
private val _blockReason = MutableLiveData<String>()
|
||||
val blockReason: LiveData<String> = _blockReason
|
||||
|
||||
private val _rateSource = MutableLiveData<String>()
|
||||
val rateSource: LiveData<String> = _rateSource
|
||||
private val _warningText = MutableLiveData<String>()
|
||||
val warningText: LiveData<String> = _warningText
|
||||
|
||||
private var currentAmount: Double = 0.0
|
||||
private var dotToHezRate: Double = FALLBACK_RATE
|
||||
private var isUsingFallback: Boolean = true
|
||||
private var isHezToDotActive: Boolean = false
|
||||
private var isWusdtToUsdtActive: Boolean = false
|
||||
|
||||
init {
|
||||
fetchExchangeRate()
|
||||
fetchBridgeStatus()
|
||||
}
|
||||
|
||||
private fun fetchExchangeRate() {
|
||||
launch {
|
||||
try {
|
||||
val (rate, source) = withContext(Dispatchers.IO) {
|
||||
fetchRateFromCoinGecko()
|
||||
}
|
||||
dotToHezRate = rate
|
||||
isUsingFallback = source == "fallback"
|
||||
_rateSource.postValue(source)
|
||||
updateUI()
|
||||
calculateOutput()
|
||||
} catch (e: Exception) {
|
||||
// Use fallback
|
||||
dotToHezRate = FALLBACK_RATE
|
||||
isUsingFallback = true
|
||||
_rateSource.postValue("fallback")
|
||||
updateUI()
|
||||
fun setPair(newPair: BridgePair) {
|
||||
if (_pair.value != newPair) {
|
||||
_pair.value = newPair
|
||||
// Reset direction to left (forward) when switching pair
|
||||
_direction.value = when (newPair) {
|
||||
BridgePair.DOT_HEZ -> BridgeDirection.DOT_TO_HEZ
|
||||
BridgePair.USDT -> BridgeDirection.USDT_TO_WUSDT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchRateFromCoinGecko(): Pair<Double, String> {
|
||||
return try {
|
||||
val response = URL(COINGECKO_API).readText()
|
||||
val json = JSONObject(response)
|
||||
|
||||
val dotPrice = json.optJSONObject("polkadot")?.optDouble("usd", 0.0) ?: 0.0
|
||||
val hezPrice = json.optJSONObject("hezkurd")?.optDouble("usd", 0.0) ?: 0.0
|
||||
|
||||
when {
|
||||
dotPrice > 0 && hezPrice > 0 -> {
|
||||
// Both prices available - calculate real rate
|
||||
val rate = dotPrice / hezPrice
|
||||
Pair(rate, "coingecko")
|
||||
}
|
||||
dotPrice > 0 -> {
|
||||
// Only DOT price - use fallback for HEZ (1 DOT = 3 HEZ means HEZ = DOT/3)
|
||||
Pair(FALLBACK_RATE, "coingecko+fallback")
|
||||
}
|
||||
else -> {
|
||||
// No prices - use pure fallback
|
||||
Pair(FALLBACK_RATE, "fallback")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Pair(FALLBACK_RATE, "fallback")
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchBridgeStatus() {
|
||||
launch {
|
||||
try {
|
||||
val active = withContext(Dispatchers.IO) {
|
||||
fetchHezToDotStatus()
|
||||
}
|
||||
isHezToDotActive = active
|
||||
updateHezToDotState()
|
||||
} catch (e: Exception) {
|
||||
// If API unavailable, assume not active for safety
|
||||
isHezToDotActive = false
|
||||
updateHezToDotState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchHezToDotStatus(): Boolean {
|
||||
return try {
|
||||
val response = URL(BRIDGE_STATUS_API).readText()
|
||||
val json = JSONObject(response)
|
||||
json.optBoolean("hezToDotActive", false)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateHezToDotState() {
|
||||
val dir = _direction.value ?: return
|
||||
if (dir == BridgeDirection.HEZ_TO_DOT && !isHezToDotActive) {
|
||||
_hezToDotBlocked.postValue(true)
|
||||
_blockReason.postValue(resourceManager.getString(R.string.bridge_hez_to_dot_blocked))
|
||||
} else {
|
||||
_hezToDotBlocked.postValue(false)
|
||||
_blockReason.postValue("")
|
||||
}
|
||||
updateButtonState()
|
||||
}
|
||||
|
||||
fun setDirection(newDirection: BridgeDirection) {
|
||||
if (_direction.value != newDirection) {
|
||||
_direction.value = newDirection
|
||||
_showHezToDotWarning.value = newDirection == BridgeDirection.HEZ_TO_DOT
|
||||
updateHezToDotState()
|
||||
updateUI()
|
||||
calculateOutput()
|
||||
updateWarningState()
|
||||
}
|
||||
}
|
||||
|
||||
fun setDirectionLeft() {
|
||||
val newDir = when (_pair.value) {
|
||||
BridgePair.DOT_HEZ -> BridgeDirection.DOT_TO_HEZ
|
||||
BridgePair.USDT -> BridgeDirection.USDT_TO_WUSDT
|
||||
null -> BridgeDirection.DOT_TO_HEZ
|
||||
}
|
||||
if (_direction.value != newDir) {
|
||||
_direction.value = newDir
|
||||
updateUI()
|
||||
calculateOutput()
|
||||
updateWarningState()
|
||||
}
|
||||
}
|
||||
|
||||
fun setDirectionRight() {
|
||||
val newDir = when (_pair.value) {
|
||||
BridgePair.DOT_HEZ -> BridgeDirection.HEZ_TO_DOT
|
||||
BridgePair.USDT -> BridgeDirection.WUSDT_TO_USDT
|
||||
null -> BridgeDirection.HEZ_TO_DOT
|
||||
}
|
||||
if (_direction.value != newDir) {
|
||||
_direction.value = newDir
|
||||
updateUI()
|
||||
calculateOutput()
|
||||
updateWarningState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,26 +139,27 @@ class BridgeViewModel(
|
||||
if (currentAmount <= 0) return
|
||||
|
||||
launch {
|
||||
// Determine which chain and asset to send from based on direction
|
||||
val chainId = when (dir) {
|
||||
BridgeDirection.DOT_TO_HEZ -> POLKADOT_ASSET_HUB_ID // Send DOT from Polkadot Asset Hub
|
||||
BridgeDirection.HEZ_TO_DOT -> PEZKUWI_ASSET_HUB_ID // Send HEZ from Pezkuwi Asset Hub
|
||||
BridgeDirection.DOT_TO_HEZ -> POLKADOT_ASSET_HUB_ID
|
||||
BridgeDirection.HEZ_TO_DOT -> PEZKUWI_ASSET_HUB_ID
|
||||
BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
|
||||
BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
|
||||
}
|
||||
|
||||
// Get the chain to convert address to correct format
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val assetId = when (dir) {
|
||||
BridgeDirection.DOT_TO_HEZ -> UTILITY_ASSET_ID
|
||||
BridgeDirection.HEZ_TO_DOT -> UTILITY_ASSET_ID
|
||||
BridgeDirection.USDT_TO_WUSDT -> POLKADOT_USDT_ASSET_ID
|
||||
BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_USDT_ASSET_ID
|
||||
}
|
||||
|
||||
// Convert generic address to chain-specific format
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val accountId = BRIDGE_ADDRESS_GENERIC.toAccountId()
|
||||
val bridgeAddress = chain.addressOf(accountId)
|
||||
|
||||
// Create asset payload (utility asset = native token)
|
||||
val assetPayload = AssetPayload(chainId, UTILITY_ASSET_ID)
|
||||
|
||||
// Create send payload specifying the origin asset
|
||||
val assetPayload = AssetPayload(chainId, assetId)
|
||||
val sendPayload = SendPayload.SpecifiedOrigin(assetPayload)
|
||||
|
||||
// Open send screen with pre-filled bridge address AND amount
|
||||
router.openSend(sendPayload, bridgeAddress, currentAmount)
|
||||
}
|
||||
}
|
||||
@@ -229,15 +168,108 @@ class BridgeViewModel(
|
||||
router.back()
|
||||
}
|
||||
|
||||
private fun fetchExchangeRate() {
|
||||
launch {
|
||||
try {
|
||||
val (rate, _) = withContext(Dispatchers.IO) {
|
||||
fetchRateFromCoinGecko()
|
||||
}
|
||||
dotToHezRate = rate
|
||||
updateUI()
|
||||
calculateOutput()
|
||||
} catch (e: Exception) {
|
||||
dotToHezRate = FALLBACK_RATE
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchRateFromCoinGecko(): Pair<Double, String> {
|
||||
return try {
|
||||
val response = URL(COINGECKO_API).readText()
|
||||
val json = JSONObject(response)
|
||||
val dotPrice = json.optJSONObject("polkadot")?.optDouble("usd", 0.0) ?: 0.0
|
||||
val hezPrice = json.optJSONObject("hezkurd")?.optDouble("usd", 0.0) ?: 0.0
|
||||
when {
|
||||
dotPrice > 0 && hezPrice > 0 -> Pair(dotPrice / hezPrice, "coingecko")
|
||||
else -> Pair(FALLBACK_RATE, "fallback")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Pair(FALLBACK_RATE, "fallback")
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchBridgeStatus() {
|
||||
launch {
|
||||
try {
|
||||
val (hezToDot, wusdtToUsdt) = withContext(Dispatchers.IO) {
|
||||
fetchStatusFromApi()
|
||||
}
|
||||
isHezToDotActive = hezToDot
|
||||
isWusdtToUsdtActive = wusdtToUsdt
|
||||
updateWarningState()
|
||||
} catch (e: Exception) {
|
||||
isHezToDotActive = false
|
||||
isWusdtToUsdtActive = false
|
||||
updateWarningState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchStatusFromApi(): Pair<Boolean, Boolean> {
|
||||
return try {
|
||||
val response = URL(BRIDGE_STATUS_API).readText()
|
||||
val json = JSONObject(response)
|
||||
val hezToDot = json.optBoolean("hezToDotActive", false)
|
||||
val wusdtToUsdt = json.optBoolean("wusdtToUsdtActive", false)
|
||||
Pair(hezToDot, wusdtToUsdt)
|
||||
} catch (e: Exception) {
|
||||
Pair(false, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWarningState() {
|
||||
val dir = _direction.value ?: return
|
||||
|
||||
when (dir) {
|
||||
BridgeDirection.HEZ_TO_DOT -> {
|
||||
_showWarning.postValue(true)
|
||||
if (!isHezToDotActive) {
|
||||
_warningBlocked.postValue(true)
|
||||
_warningText.postValue(resourceManager.getString(R.string.bridge_hez_to_dot_blocked))
|
||||
} else {
|
||||
_warningBlocked.postValue(false)
|
||||
_warningText.postValue(resourceManager.getString(R.string.bridge_hez_to_dot_warning))
|
||||
}
|
||||
}
|
||||
BridgeDirection.WUSDT_TO_USDT -> {
|
||||
_showWarning.postValue(true)
|
||||
if (!isWusdtToUsdtActive) {
|
||||
_warningBlocked.postValue(true)
|
||||
_warningText.postValue(resourceManager.getString(R.string.bridge_wusdt_to_usdt_blocked))
|
||||
} else {
|
||||
_warningBlocked.postValue(false)
|
||||
_warningText.postValue("")
|
||||
_showWarning.postValue(false)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
_showWarning.postValue(false)
|
||||
}
|
||||
}
|
||||
updateButtonState()
|
||||
}
|
||||
|
||||
private fun calculateOutput() {
|
||||
val dir = _direction.value ?: return
|
||||
|
||||
val grossOutput = when (dir) {
|
||||
BridgeDirection.DOT_TO_HEZ -> currentAmount * dotToHezRate
|
||||
BridgeDirection.HEZ_TO_DOT -> currentAmount / dotToHezRate
|
||||
BridgeDirection.USDT_TO_WUSDT -> currentAmount // 1:1
|
||||
BridgeDirection.WUSDT_TO_USDT -> currentAmount // 1:1
|
||||
}
|
||||
|
||||
// Apply fee
|
||||
val netOutput = grossOutput * (1 - FEE_PERCENT)
|
||||
|
||||
_outputAmount.value = if (netOutput > 0) {
|
||||
@@ -250,18 +282,21 @@ class BridgeViewModel(
|
||||
private fun updateUI() {
|
||||
val dir = _direction.value ?: return
|
||||
|
||||
val rateFormatted = BigDecimal(dotToHezRate).setScale(4, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString()
|
||||
val reverseRateFormatted = BigDecimal(1.0 / dotToHezRate).setScale(6, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString()
|
||||
|
||||
when (dir) {
|
||||
BridgeDirection.DOT_TO_HEZ -> {
|
||||
val rateFormatted = BigDecimal(dotToHezRate).setScale(4, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString()
|
||||
_exchangeRateText.value = "1 DOT = $rateFormatted HEZ"
|
||||
_minimumText.value = "$MIN_DOT DOT"
|
||||
}
|
||||
BridgeDirection.HEZ_TO_DOT -> {
|
||||
_exchangeRateText.value = "1 HEZ = $reverseRateFormatted DOT"
|
||||
val reverseRate = BigDecimal(1.0 / dotToHezRate).setScale(6, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString()
|
||||
_exchangeRateText.value = "1 HEZ = $reverseRate DOT"
|
||||
_minimumText.value = "$MIN_HEZ HEZ"
|
||||
}
|
||||
BridgeDirection.USDT_TO_WUSDT, BridgeDirection.WUSDT_TO_USDT -> {
|
||||
_exchangeRateText.value = "1:1 (fee 0.1%)"
|
||||
_minimumText.value = "$MIN_USDT USDT"
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonState()
|
||||
@@ -272,12 +307,14 @@ class BridgeViewModel(
|
||||
val minimum = when (dir) {
|
||||
BridgeDirection.DOT_TO_HEZ -> MIN_DOT
|
||||
BridgeDirection.HEZ_TO_DOT -> MIN_HEZ
|
||||
BridgeDirection.USDT_TO_WUSDT, BridgeDirection.WUSDT_TO_USDT -> MIN_USDT
|
||||
}
|
||||
|
||||
_buttonState.value = when {
|
||||
currentAmount <= 0 -> ButtonState.DISABLED
|
||||
currentAmount < minimum -> ButtonState.DISABLED
|
||||
dir == BridgeDirection.HEZ_TO_DOT && !isHezToDotActive -> ButtonState.DISABLED
|
||||
dir == BridgeDirection.WUSDT_TO_USDT && !isWusdtToUsdtActive -> ButtonState.DISABLED
|
||||
else -> ButtonState.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,41 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Pair Selector -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/bg_primary_list_item"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgePairDotHez"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_button_primary"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:text="DOT / HEZ"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgePairUsdt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:text="USDT"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Direction Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -35,7 +70,7 @@
|
||||
android:padding="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgeDirectionDotToHez"
|
||||
android:id="@+id/bridgeDirectionLeft"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
@@ -48,7 +83,7 @@
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bridgeDirectionHezToDot"
|
||||
android:id="@+id/bridgeDirectionRight"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
||||
@@ -15,53 +15,92 @@
|
||||
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" />
|
||||
|
||||
<!-- Top row: Title + Logo -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical">
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pezkuwi_dashboard_trust_score"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="12sp" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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" />
|
||||
<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="4dp"
|
||||
app:flexWrap="wrap"
|
||||
app:alignItems="center"
|
||||
app:justifyContent="flex_start" />
|
||||
|
||||
<!-- Trust Score -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_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_marginStart="8dp"
|
||||
android:textColor="#FFD54F"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pezkuwiDashboardLogo"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:src="@drawable/telegram_welcome"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/pezkuwi_dashboard_title" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Info text -->
|
||||
<TextView
|
||||
android:id="@+id/pezkuwiDashboardInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/pezkuwi_dashboard_info"
|
||||
android:textColor="#CFD8DC"
|
||||
android:textSize="11sp"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
<!-- Action button -->
|
||||
<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:layout_marginTop="10dp"
|
||||
android:text="@string/pezkuwi_dashboard_basvuru"
|
||||
android:textAllCaps="false" />
|
||||
</LinearLayout>
|
||||
|
||||
+6
-11
@@ -38,24 +38,19 @@ suspend fun BagListRepository.bagListLocatorOrThrow(chainId: ChainId): BagListLo
|
||||
|
||||
class LocalBagListRepository(
|
||||
private val localStorage: StorageDataSource,
|
||||
private val remoteStorage: StorageDataSource,
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : BagListRepository {
|
||||
|
||||
override suspend fun bagThresholds(chainId: ChainId): List<BagListNode.Score>? {
|
||||
return runCatching {
|
||||
chainRegistry.withRuntime(chainId) {
|
||||
runtime.metadata.voterListOrNull()?.constant("BagThresholds")?.getAs(collectionOf(::score))
|
||||
}
|
||||
}.getOrNull()
|
||||
return chainRegistry.withRuntime(chainId) {
|
||||
runtime.metadata.voterListOrNull()?.constant("BagThresholds")?.getAs(collectionOf(::score))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun bagListSize(chainId: ChainId): BigInteger? {
|
||||
return runCatching {
|
||||
remoteStorage.query(chainId) {
|
||||
runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.query(binding = ::bindNumber)
|
||||
}
|
||||
}.getOrNull()
|
||||
return localStorage.query(chainId) {
|
||||
runtime.metadata.voterListOrNull()?.storage("CounterForListNodes")?.query(binding = ::bindNumber)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun maxElectingVotes(chainId: ChainId): BigInteger? {
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.parasOrNull
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
|
||||
interface ParasRepository {
|
||||
|
||||
@@ -21,7 +21,7 @@ class RealParasRepository(
|
||||
|
||||
override suspend fun activePublicParachains(chainId: ChainId): Int? {
|
||||
return localSource.query(chainId) {
|
||||
val parachains = runtime.metadata.parasOrNull()?.storageOrNull("Parachains")
|
||||
val parachains = runtime.metadata.parasOrNull()?.storage("Parachains")
|
||||
?.query(binding = ::bindParachains) ?: return@query null
|
||||
|
||||
parachains.count { it >= LOWEST_PUBLIC_ID }
|
||||
|
||||
+10
-6
@@ -13,6 +13,7 @@ import io.novafoundation.nova.common.utils.metadata
|
||||
import io.novafoundation.nova.common.utils.numberConstant
|
||||
import io.novafoundation.nova.common.utils.numberConstantOrNull
|
||||
import io.novafoundation.nova.common.utils.staking
|
||||
import io.novafoundation.nova.core.storage.StorageCache
|
||||
import io.novafoundation.nova.core_db.dao.AccountStakingDao
|
||||
import io.novafoundation.nova.core_db.model.AccountStakingLocal
|
||||
import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap
|
||||
@@ -49,6 +50,7 @@ import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindin
|
||||
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlashDeferDuration
|
||||
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindSlashingSpans
|
||||
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.bindings.bindValidatorPrefs
|
||||
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.ValidatorExposureUpdater
|
||||
import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.activeEraStorageKey
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants
|
||||
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
|
||||
@@ -86,6 +88,7 @@ class StakingRepositoryImpl(
|
||||
private val localStorage: StorageDataSource,
|
||||
private val walletConstants: WalletConstants,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val storageCache: StorageCache,
|
||||
private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi,
|
||||
) : StakingRepository {
|
||||
|
||||
@@ -124,7 +127,7 @@ class StakingRepositoryImpl(
|
||||
return runtime.metadata.staking().numberConstant("SessionsPerEra", runtime) // How many sessions per era
|
||||
}
|
||||
|
||||
override suspend fun getActiveEraIndex(chainId: ChainId): EraIndex = remoteStorage.query(chainId) {
|
||||
override suspend fun getActiveEraIndex(chainId: ChainId): EraIndex = localStorage.query(chainId) {
|
||||
metadata.staking.activeEra.queryNonNull()
|
||||
}
|
||||
|
||||
@@ -161,7 +164,7 @@ class StakingRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchPagedEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap<Exposure> = remoteStorage.query(chainId) {
|
||||
private suspend fun fetchPagedEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap<Exposure> = localStorage.query(chainId) {
|
||||
val eraStakersOverview = metadata.staking().storage("ErasStakersOverview").entries(
|
||||
eraIndex,
|
||||
keyExtractor = { (_: BigInteger, accountId: ByteArray) -> accountId.toHexString() },
|
||||
@@ -204,7 +207,7 @@ class StakingRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchLegacyEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap<Exposure> = remoteStorage.query(chainId) {
|
||||
private suspend fun fetchLegacyEraStakers(chainId: ChainId, eraIndex: EraIndex): AccountIdMap<Exposure> = localStorage.query(chainId) {
|
||||
runtime.metadata.staking().storage("ErasStakers").entries(
|
||||
eraIndex,
|
||||
keyExtractor = { (_: BigInteger, accountId: ByteArray) -> accountId.toHexString() },
|
||||
@@ -346,7 +349,7 @@ class StakingRepositoryImpl(
|
||||
val runtime = runtimeFor(chainId)
|
||||
|
||||
return runtime.metadata.staking().storageOrNull(storageName)?.let { storageEntry ->
|
||||
remoteStorage.query(
|
||||
localStorage.query(
|
||||
keyBuilder = { storageEntry.storageKey() },
|
||||
binding = { scale, _ -> scale?.let { binder(scale, runtime, storageEntry.returnType()) } },
|
||||
chainId = chainId
|
||||
@@ -402,8 +405,9 @@ class StakingRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun isPagedExposuresUsed(chainId: ChainId): Boolean {
|
||||
val runtime = runtimeFor(chainId)
|
||||
return runtime.metadata.staking().storageOrNull("ErasStakersOverview") != null
|
||||
val isPagedExposuresValue = storageCache.getEntry(ValidatorExposureUpdater.STORAGE_KEY_PAGED_EXPOSURES, chainId)
|
||||
|
||||
return ValidatorExposureUpdater.decodeIsPagedExposuresValue(isPagedExposuresValue.content)
|
||||
}
|
||||
|
||||
private fun observeAccountValidatorPrefs(chainId: ChainId, stashId: AccountId): Flow<ValidatorPrefs?> {
|
||||
|
||||
+4
-2
@@ -11,6 +11,7 @@ import io.novafoundation.nova.common.data.network.rpc.BulkRetriever
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.core.storage.StorageCache
|
||||
import io.novafoundation.nova.core_db.dao.AccountStakingDao
|
||||
import io.novafoundation.nova.core_db.dao.ExternalBalanceDao
|
||||
import io.novafoundation.nova.core_db.dao.StakingRewardPeriodDao
|
||||
@@ -189,6 +190,7 @@ class StakingFeatureModule {
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
|
||||
walletConstants: WalletConstants,
|
||||
chainRegistry: ChainRegistry,
|
||||
storageCache: StorageCache,
|
||||
multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi
|
||||
): StakingRepository = StakingRepositoryImpl(
|
||||
accountStakingDao = accountStakingDao,
|
||||
@@ -196,6 +198,7 @@ class StakingFeatureModule {
|
||||
localStorage = localStorageSource,
|
||||
walletConstants = walletConstants,
|
||||
chainRegistry = chainRegistry,
|
||||
storageCache = storageCache,
|
||||
multiChainRuntimeCallsApi = multiChainRuntimeCallsApi
|
||||
)
|
||||
|
||||
@@ -225,9 +228,8 @@ class StakingFeatureModule {
|
||||
@FeatureScope
|
||||
fun provideBagListRepository(
|
||||
@Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource,
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
|
||||
chainRegistry: ChainRegistry
|
||||
): BagListRepository = LocalBagListRepository(localStorageSource, remoteStorageSource, chainRegistry)
|
||||
): BagListRepository = LocalBagListRepository(localStorageSource, chainRegistry)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
|
||||
+1
-4
@@ -26,7 +26,6 @@ import io.novafoundation.nova.feature_staking_impl.data.nominationPools.reposito
|
||||
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolMembersRepository
|
||||
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolStateRepository
|
||||
import io.novafoundation.nova.feature_staking_impl.data.nominationPools.repository.RealNominationPoolUnbondRepository
|
||||
import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository
|
||||
import io.novafoundation.nova.feature_staking_impl.data.repository.StakingConstantsRepository
|
||||
import io.novafoundation.nova.feature_staking_impl.data.repository.StakingRewardsRepository
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.StakingInteractor
|
||||
@@ -187,12 +186,10 @@ class NominationPoolModule {
|
||||
fun provideNominationPoolRewardCalculatorFactory(
|
||||
stakingSharedComputation: StakingSharedComputation,
|
||||
nominationPoolSharedComputation: NominationPoolSharedComputation,
|
||||
stakingRepository: StakingRepository,
|
||||
): NominationPoolRewardCalculatorFactory {
|
||||
return NominationPoolRewardCalculatorFactory(
|
||||
sharedStakingSharedComputation = stakingSharedComputation,
|
||||
nominationPoolSharedComputation = nominationPoolSharedComputation,
|
||||
stakingRepository = stakingRepository
|
||||
nominationPoolSharedComputation = nominationPoolSharedComputation
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+9
-16
@@ -26,7 +26,6 @@ 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
|
||||
|
||||
@@ -62,12 +61,7 @@ class StakingSharedComputation(
|
||||
val key = "ACTIVE_ERA:$chainId"
|
||||
|
||||
return computationalCache.useSharedFlow(key, scope) {
|
||||
flow {
|
||||
val era = stakingRepository.getActiveEraIndex(chainId)
|
||||
emit(era)
|
||||
|
||||
emitAll(stakingRepository.observeActiveEraIndex(chainId))
|
||||
}
|
||||
stakingRepository.observeActiveEraIndex(chainId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +70,7 @@ class StakingSharedComputation(
|
||||
|
||||
return computationalCache.useSharedFlow(key, scope) {
|
||||
activeEraFlow(chainId, scope).map { eraIndex ->
|
||||
val exposures = stakingRepository.getElectedValidatorsExposure(chainId, eraIndex)
|
||||
exposures to eraIndex
|
||||
stakingRepository.getElectedValidatorsExposure(chainId, eraIndex) to eraIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,14 +79,14 @@ class StakingSharedComputation(
|
||||
val key = "MIN_STAKE:$chainId"
|
||||
|
||||
return computationalCache.useSharedFlow(key, scope) {
|
||||
electedExposuresWithActiveEraFlow(chainId, scope).map { (exposures, activeEraIndex) ->
|
||||
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)
|
||||
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,
|
||||
|
||||
+3
-11
@@ -11,12 +11,12 @@ import io.novafoundation.nova.common.utils.reversed
|
||||
import io.novafoundation.nova.feature_account_api.data.model.AccountIdKeyMap
|
||||
import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap
|
||||
import io.novafoundation.nova.feature_staking_api.domain.model.Exposure
|
||||
import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository
|
||||
import io.novafoundation.nova.feature_staking_impl.data.StakingOption
|
||||
import io.novafoundation.nova.feature_staking_impl.data.chain
|
||||
import io.novafoundation.nova.feature_staking_api.domain.nominationPool.model.PoolId
|
||||
import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.common.electedExposuresInActiveEra
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.nominationPools.common.NominationPoolSharedComputation
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.rewards.RewardCalculator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -24,29 +24,21 @@ import kotlinx.coroutines.CoroutineScope
|
||||
class NominationPoolRewardCalculatorFactory(
|
||||
private val sharedStakingSharedComputation: StakingSharedComputation,
|
||||
private val nominationPoolSharedComputation: NominationPoolSharedComputation,
|
||||
private val stakingRepository: StakingRepository,
|
||||
) {
|
||||
|
||||
suspend fun create(stakingOption: StakingOption, sharedComputationScope: CoroutineScope): NominationPoolRewardCalculator {
|
||||
val chain = stakingOption.chain
|
||||
val chainId = chain.id
|
||||
// For parachains, staking exposures live on the parent relay chain
|
||||
val exposureChainId = chain.parentId ?: chainId
|
||||
val chainId = stakingOption.chain.id
|
||||
|
||||
val delegateOption = stakingOption.unwrapNominationPools()
|
||||
|
||||
val delegate = sharedStakingSharedComputation.rewardCalculator(delegateOption, sharedComputationScope)
|
||||
val allPoolAccounts = nominationPoolSharedComputation.allBondedPoolAccounts(chainId, sharedComputationScope)
|
||||
|
||||
val poolCommissions = nominationPoolSharedComputation.allBondedPools(chainId, sharedComputationScope)
|
||||
.mapValues { (_, pool) -> pool.commission?.current?.perbill }
|
||||
|
||||
val activeEra = stakingRepository.getActiveEraIndex(exposureChainId)
|
||||
val exposures = stakingRepository.getElectedValidatorsExposure(exposureChainId, activeEra)
|
||||
|
||||
return RealNominationPoolRewardCalculator(
|
||||
directStakingDelegate = delegate,
|
||||
exposures = exposures,
|
||||
exposures = sharedStakingSharedComputation.electedExposuresInActiveEra(stakingOption.assetWithChain.chain.id, sharedComputationScope),
|
||||
commissions = poolCommissions,
|
||||
poolStashesById = allPoolAccounts
|
||||
)
|
||||
|
||||
+1
-3
@@ -49,13 +49,11 @@ class RealNominationPoolsAlertsInteractor(
|
||||
return flowOfAll {
|
||||
val poolId = poolMember.poolId
|
||||
val poolStash = poolAccountDerivation.bondedAccountOf(poolId, chain.id)
|
||||
// Staking exposures live on the relay chain, not on parachains like Asset Hub
|
||||
val exposureChainId = chain.parentId ?: chain.id
|
||||
|
||||
combine(
|
||||
nominationPoolsSharedComputation.participatingPoolNominationsFlow(poolStash, poolId, chain.id, shareComputationScope),
|
||||
nominationPoolsSharedComputation.unbondingPoolsFlow(poolId, chain.id, shareComputationScope),
|
||||
stakingSharedComputation.electedExposuresWithActiveEraFlow(exposureChainId, shareComputationScope),
|
||||
stakingSharedComputation.electedExposuresWithActiveEraFlow(chain.id, shareComputationScope),
|
||||
) { poolNominations, unbondingPools, (eraStakers, activeEra) ->
|
||||
val alertsContext = AlertsResolutionContext(
|
||||
eraStakers = eraStakers,
|
||||
|
||||
+2
-5
@@ -47,16 +47,13 @@ class RealNominationPoolStakeSummaryInteractor(
|
||||
stakingOption: StakingOption,
|
||||
sharedComputationScope: CoroutineScope,
|
||||
): Flow<StakeSummary<PoolMemberStatus>> = flowOfAll {
|
||||
val chain = stakingOption.assetWithChain.chain
|
||||
val chainId = chain.id
|
||||
// Staking exposures live on the relay chain, not on parachains like Asset Hub
|
||||
val exposureChainId = chain.parentId ?: chainId
|
||||
val chainId = stakingOption.assetWithChain.chain.id
|
||||
val poolStash = poolAccountDerivation.bondedAccountOf(poolMember.poolId, chainId)
|
||||
|
||||
combineTransform(
|
||||
nominationPoolSharedComputation.participatingBondedPoolStateFlow(poolStash, poolMember.poolId, chainId, sharedComputationScope),
|
||||
nominationPoolSharedComputation.participatingPoolNominationsFlow(poolStash, poolMember.poolId, chainId, sharedComputationScope),
|
||||
stakingSharedComputation.electedExposuresWithActiveEraFlow(exposureChainId, sharedComputationScope)
|
||||
stakingSharedComputation.electedExposuresWithActiveEraFlow(chainId, sharedComputationScope)
|
||||
) { bondedPoolState, poolNominations, (eraStakers, activeEra) ->
|
||||
val activeStaked = bondedPoolState.amountOf(poolMember.points)
|
||||
|
||||
|
||||
+1
-4
@@ -52,9 +52,6 @@ class RealNominationPoolsUserRewardsInteractor(
|
||||
|
||||
private fun pendingRewardsFlow(accountId: AccountId, chainId: ChainId): Flow<Balance> {
|
||||
return flowOf { repository.getPendingRewards(accountId, chainId) }
|
||||
.catch {
|
||||
Log.e("NominationPoolsUserRewardsInteractor", "Failed to fetch pending rewards", it)
|
||||
emit(Balance.ZERO)
|
||||
}
|
||||
.catch { Log.e("NominationPoolsUserRewardsInteractor", "Failed to fetch pending rewards", it) }
|
||||
}
|
||||
}
|
||||
|
||||
+8
-18
@@ -13,6 +13,7 @@ import io.novafoundation.nova.feature_staking_impl.data.repository.VaraRepositor
|
||||
import io.novafoundation.nova.feature_staking_impl.data.stakingType
|
||||
import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.common.electedExposuresInActiveEra
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.common.eraTimeCalculator
|
||||
import io.novafoundation.nova.feature_staking_impl.domain.error.accountIdNotFound
|
||||
import io.novafoundation.nova.runtime.ext.Geneses
|
||||
@@ -46,10 +47,7 @@ class RewardCalculatorFactory(
|
||||
validatorsPrefs: AccountIdMap<ValidatorPrefs?>,
|
||||
scope: CoroutineScope
|
||||
): RewardCalculator = withContext(Dispatchers.Default) {
|
||||
// For parachains (e.g. Asset Hub), staking lives on the parent relay chain.
|
||||
// TotalIssuance must come from there, not from the parachain.
|
||||
val stakingChainId = stakingOption.assetWithChain.chain.parentId ?: stakingOption.assetWithChain.chain.id
|
||||
val totalIssuance = totalIssuanceRepository.getTotalIssuance(stakingChainId)
|
||||
val totalIssuance = totalIssuanceRepository.getTotalIssuance(stakingOption.assetWithChain.chain.id)
|
||||
|
||||
val validators = exposures.keys.mapNotNull { accountIdHex ->
|
||||
val exposure = exposures[accountIdHex] ?: accountIdNotFound(accountIdHex)
|
||||
@@ -62,20 +60,14 @@ class RewardCalculatorFactory(
|
||||
)
|
||||
}
|
||||
|
||||
stakingOption.createRewardCalculator(validators, totalIssuance, stakingChainId, scope)
|
||||
stakingOption.createRewardCalculator(validators, totalIssuance, scope)
|
||||
}
|
||||
|
||||
suspend fun create(stakingOption: StakingOption, scope: CoroutineScope): RewardCalculator = withContext(Dispatchers.Default) {
|
||||
val chain = stakingOption.assetWithChain.chain
|
||||
val chainId = chain.id
|
||||
// For parachains with a parent relay chain, staking exposures live on the relay chain
|
||||
val exposureChainId = chain.parentId ?: chainId
|
||||
val chainId = stakingOption.assetWithChain.chain.id
|
||||
|
||||
val activeEra = stakingRepository.getActiveEraIndex(exposureChainId)
|
||||
|
||||
val exposures = stakingRepository.getElectedValidatorsExposure(exposureChainId, activeEra)
|
||||
|
||||
val validatorsPrefs = stakingRepository.getValidatorPrefs(exposureChainId, exposures.keys)
|
||||
val exposures = shareStakingSharedComputation.get().electedExposuresInActiveEra(chainId, scope)
|
||||
val validatorsPrefs = stakingRepository.getValidatorPrefs(chainId, exposures.keys)
|
||||
|
||||
create(stakingOption, exposures, validatorsPrefs, scope)
|
||||
}
|
||||
@@ -83,7 +75,6 @@ class RewardCalculatorFactory(
|
||||
private suspend fun StakingOption.createRewardCalculator(
|
||||
validators: List<RewardCalculationTarget>,
|
||||
totalIssuance: BigInteger,
|
||||
stakingChainId: ChainId,
|
||||
scope: CoroutineScope
|
||||
): RewardCalculator {
|
||||
return when (unwrapNominationPools().stakingType) {
|
||||
@@ -91,9 +82,8 @@ class RewardCalculatorFactory(
|
||||
val custom = customRelayChainCalculator(validators, totalIssuance, scope)
|
||||
if (custom != null) return custom
|
||||
|
||||
// Query parachains from the relay chain, not from Asset Hub
|
||||
val activePublicParachains = parasRepository.activePublicParachains(stakingChainId)
|
||||
val inflationConfig = InflationConfig.create(stakingChainId, activePublicParachains)
|
||||
val activePublicParachains = parasRepository.activePublicParachains(assetWithChain.chain.id)
|
||||
val inflationConfig = InflationConfig.create(chain.id, activePublicParachains)
|
||||
|
||||
RewardCurveInflationRewardCalculator(validators, totalIssuance, inflationConfig)
|
||||
}
|
||||
|
||||
+2
-4
@@ -26,8 +26,7 @@ class RealStartMultiStakingInteractor(
|
||||
|
||||
override suspend fun calculateFee(selection: StartMultiStakingSelection): Fee {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val chain = selection.stakingOption.chain
|
||||
extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) {
|
||||
extrinsicService.estimateFee(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) {
|
||||
startStaking(selection)
|
||||
}
|
||||
}
|
||||
@@ -35,8 +34,7 @@ class RealStartMultiStakingInteractor(
|
||||
|
||||
override suspend fun startStaking(selection: StartMultiStakingSelection): Result<ExtrinsicExecutionResult> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val chain = selection.stakingOption.chain
|
||||
extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) {
|
||||
extrinsicService.submitExtrinsicAndAwaitExecution(selection.stakingOption.chain, TransactionOrigin.SelectedWallet) {
|
||||
startStaking(selection)
|
||||
}.requireOk()
|
||||
}
|
||||
|
||||
+2
-4
@@ -98,10 +98,8 @@ private class DirectStakingProperties(
|
||||
enoughAvailableToStake()
|
||||
}
|
||||
|
||||
private val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id
|
||||
|
||||
override suspend fun minStake(): Balance {
|
||||
return stakingSharedComputation.minStake(stakingChainId, scope)
|
||||
return stakingSharedComputation.minStake(stakingOption.chain.id, scope)
|
||||
}
|
||||
|
||||
private fun StartMultiStakingValidationSystemBuilder.noConflictingStaking() {
|
||||
@@ -127,7 +125,7 @@ private class DirectStakingProperties(
|
||||
private fun StartMultiStakingValidationSystemBuilder.maximumNominatorsReached() {
|
||||
maximumNominatorsReached(
|
||||
stakingRepository = stakingRepository,
|
||||
chainId = { stakingChainId },
|
||||
chainId = { stakingOption.chain.id },
|
||||
errorProducer = { StartMultiStakingValidationFailure.MaxNominatorsReached(stakingType) }
|
||||
)
|
||||
}
|
||||
|
||||
+1
-2
@@ -29,8 +29,7 @@ class DirectStakingRecommendation(
|
||||
|
||||
override suspend fun recommendedSelection(stake: Balance): StartMultiStakingSelection {
|
||||
val provider = recommendationSettingsProvider.await()
|
||||
val stakingChainId = stakingOption.chain.parentId ?: stakingOption.chain.id
|
||||
val maximumValidatorsPerNominator = stakingConstantsRepository.maxValidatorsPerNominator(stakingChainId, stake)
|
||||
val maximumValidatorsPerNominator = stakingConstantsRepository.maxValidatorsPerNominator(stakingOption.chain.id, stake)
|
||||
val recommendationSettings = provider.recommendedSettings(maximumValidatorsPerNominator)
|
||||
val recommendator = recommendator.await()
|
||||
|
||||
|
||||
+1
-11
@@ -13,7 +13,6 @@ 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 kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class AutomaticMultiStakingSelectionType(
|
||||
private val candidates: List<SingleStakingProperties>,
|
||||
@@ -58,16 +57,7 @@ class AutomaticMultiStakingSelectionType(
|
||||
}
|
||||
|
||||
private suspend fun typePropertiesFor(stake: Balance): SingleStakingProperties {
|
||||
for (candidate in candidates) {
|
||||
try {
|
||||
val minStake = candidate.minStake()
|
||||
if (minStake <= stake) return candidate
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
return candidates.findWithMinimumStake()
|
||||
return candidates.firstAllowingToStake(stake) ?: candidates.findWithMinimumStake()
|
||||
}
|
||||
|
||||
private suspend fun List<SingleStakingProperties>.firstAllowingToStake(stake: Balance): SingleStakingProperties? {
|
||||
|
||||
+4
-6
@@ -45,22 +45,20 @@ class ValidatorProvider(
|
||||
): List<Validator> {
|
||||
val chain = stakingOption.assetWithChain.chain
|
||||
val chainId = chain.id
|
||||
// For parachains (e.g. Asset Hub), staking validators live on the parent relay chain
|
||||
val stakingChainId = chain.parentId ?: chainId
|
||||
|
||||
val novaValidatorIds = validatorsPreferencesSource.getRecommendedValidatorIds(chainId)
|
||||
val electedValidatorExposures = stakingSharedComputation.electedExposuresInActiveEra(stakingChainId, scope)
|
||||
val electedValidatorExposures = stakingSharedComputation.electedExposuresInActiveEra(chainId, scope)
|
||||
|
||||
val requestedValidatorIds = sources.allValidatorIds(chainId, electedValidatorExposures, novaValidatorIds)
|
||||
// we always need validator prefs for elected validators to construct reward calculator
|
||||
val validatorIdsToQueryPrefs = electedValidatorExposures.keys + requestedValidatorIds
|
||||
|
||||
val validatorPrefs = stakingRepository.getValidatorPrefs(stakingChainId, validatorIdsToQueryPrefs)
|
||||
val validatorPrefs = stakingRepository.getValidatorPrefs(chainId, validatorIdsToQueryPrefs)
|
||||
val identities = identityRepository.getIdentitiesFromIdsHex(chainId, requestedValidatorIds)
|
||||
val slashes = stakingRepository.getSlashes(stakingChainId, requestedValidatorIds)
|
||||
val slashes = stakingRepository.getSlashes(chain.id, requestedValidatorIds)
|
||||
|
||||
val rewardCalculator = rewardCalculatorFactory.create(stakingOption, electedValidatorExposures, validatorPrefs, scope)
|
||||
val maxNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(stakingChainId)
|
||||
val maxNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId)
|
||||
|
||||
return requestedValidatorIds.map { accountIdHex ->
|
||||
val accountId = AccountIdKey.fromHex(accountIdHex).getOrThrow()
|
||||
|
||||
Reference in New Issue
Block a user