diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index d793816b..405cc64b 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -253,7 +253,7 @@ Sizin için en iyi seçeneği bulmak amacıyla tekliflerini karşılaştırın.< Tutar girin HEZ → DOT yönü manuel işlenir, 24 saate kadar sürebilir. HEZ → DOT yönü şu anda devre dışı. - USDT(Pez) → USDT(Pol) yönü şu anda devre dışı. + Bu tutar köprünün mevcut rezervini (%s USDT) aşıyor. Daha küçük bir tutar deneyin. Al/Sat Telefon ayarlarınızda %s OTG\'yi etkinleştir diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 6fff5081..a10dbc96 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -364,7 +364,7 @@ Insufficient balance Amount below minimum Enter amount - USDT(Pez)→USDT(Pol) swaps are temporarily unavailable. Waiting for 1:1 liquidity to be established. + This amount exceeds the bridge\'s current available reserve (%s USDT). Try a smaller amount. Bridge allowance OK (%s) Sign renewal (%s) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt index 09c827f1..f3647c0d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt @@ -14,8 +14,15 @@ package io.novafoundation.nova.feature_assets.domain.bridge.multisig object BridgeMultisigConstants { const val WUSDT_ASSET_ID = 1000 + const val POLKADOT_USDT_ASSET_ID = 1 const val MULTISIG_ADDRESS = "5GvwxmCDp3PC33KHoeWSgj3S7ocE7nzk1jiCCZMPSDBFeNcj" + /** Same multisig account, Polkadot Asset Hub SS58 encoding - where the real USDT backing + * wUSDT withdrawals actually sits. Used to check a specific withdrawal amount against the + * real reserve (see BridgeMultisigInteractor.getPolkadotUsdtReserve) instead of the old + * dead-external-service boolean check this replaced (217.77.6.126:3030/status belonged to + * the legacy bridge bot, stopped this same session - see res/validators-tiki.md). */ + const val MULTISIG_ADDRESS_POLKADOT = "15sF76THfpefUaKomHZSpssayRbsp6Yt6ESgMrLjzJCmpe66" const val AUTOMATION_KEY_ADDRESS = "5GQu4PFUb1f3MTJ7i7c1CtLgDk3TVvpSW1VbQCRmfkMoC8cM" const val THRESHOLD = 3 diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt index 9ef7c743..27ecfff3 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt @@ -38,6 +38,14 @@ interface BridgeMultisigInteractor { suspend fun getSignerState(): BridgeSignerState? suspend fun submitRenewalSignature(): Result + + /** Real USDT (base units) the multisig actually holds on Polkadot Asset Hub right now - the + * true backing for wUSDT->USDT withdrawals. Replaces the old wusdtToUsdtActive boolean + * fetched from the legacy bridge bot's :3030/status endpoint, which this session stopped + * (see res/validators-tiki.md) - that made the old check always report "inactive" + * regardless of real reserve. A specific withdrawal should be allowed whenever it's covered + * by this real balance, not gated on an unrelated dead service or on total supply parity. */ + suspend fun getPolkadotUsdtReserve(): BigInteger } @FeatureScope @@ -118,6 +126,18 @@ class RealBridgeMultisigInteractor @Inject constructor( }.getOrThrow().requireOk() } + override suspend fun getPolkadotUsdtReserve(): BigInteger { + val polkadotChain = chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB) + val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT.toAccountId().intoKey() + + return storageDataSource.query(polkadotChain.id) { + runtime.metadata.bridgeAssets().assetBalance.query( + BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID.toBigInteger(), + multisigAccountId, + ) + } ?: BigInteger.ZERO + } + private suspend fun queryRemainingAllowance(chain: Chain): BigInteger { val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS.toAccountId().intoKey() val delegateAccountId = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey() diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt index ebc5456b..f72852b2 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt @@ -48,6 +48,18 @@ val BridgeAssetsApi.approvalAmount: QueryableStorageEntry3 AssetAccount { balance, ... }` - only `balance` needed. + * Used to check a custody account's real held amount (e.g. the multisig's real USDT reserve on + * Polkadot Asset Hub), not the connected wallet's own balance. */ +context(StorageQueryContext) +val BridgeAssetsApi.assetBalance: QueryableStorageEntry2 + get() = storage2( + name = "Account", + binding = { decoded, _, _ -> bindNumber(decoded.castToStruct()["balance"]) }, + key2ToInternalConverter = AccountIdKey.scaleEncoder, + key2FromInternalConverter = AccountIdKey.scaleDecoder, + ) + @JvmInline value class BridgeMultisigApi(override val module: Module) : QueryableModule diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt index f15b02d6..414dcedd 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt @@ -21,14 +21,10 @@ import io.novafoundation.nova.runtime.ext.addressOf import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.json.JSONObject import java.math.BigDecimal import java.math.RoundingMode -import java.net.URL /** * DOT<->HEZ used to be a second pair here, retired 2026-07 in favor of the multisig-custodied @@ -60,7 +56,8 @@ class BridgeViewModel( const val FEE_PERCENT = 0.001 const val MIN_USDT = 1.0 - const val BRIDGE_STATUS_API = "http://217.77.6.126:3030/status" + // USDT has 6 decimals on both Polkadot and Pezkuwi Asset Hub. + val USDT_DECIMALS_DIVISOR: BigDecimal = BigDecimal.TEN.pow(6) } private val _pair = MutableLiveData(BridgePair.USDT) @@ -121,12 +118,15 @@ class BridgeViewModel( val fillAmountEvent: LiveData> = _fillAmountEvent private var currentAmount: Double = 0.0 - private var isWusdtToUsdtActive: Boolean = false + /** Real USDT the multisig actually holds on Polkadot Asset Hub - see + * BridgeMultisigInteractor.getPolkadotUsdtReserve for why this replaced a dead external + * status check that always reported "inactive" regardless of the real reserve. */ + private var polkadotUsdtReserve: BigDecimal = BigDecimal.ZERO private var availableBalance: BigDecimal = BigDecimal.ZERO private var balanceJob: Job? = null init { - fetchBridgeStatus() + fetchReserveStatus() refreshSignerState() updateCards() loadPairOptions() @@ -171,7 +171,7 @@ class BridgeViewModel( currentAmount = amount calculateOutput() updateInsufficientBalanceState() - updateButtonState() + updateWarningState() // re-check the entered amount against the cached reserve } fun maxClicked() { @@ -208,27 +208,15 @@ class BridgeViewModel( router.back() } - private fun fetchBridgeStatus() { + private fun fetchReserveStatus() { launch { - try { - isWusdtToUsdtActive = withContext(Dispatchers.IO) { - fetchStatusFromApi() - } - updateWarningState() + polkadotUsdtReserve = try { + val raw = bridgeMultisigInteractor.getPolkadotUsdtReserve() + BigDecimal(raw).divide(USDT_DECIMALS_DIVISOR) } catch (e: Exception) { - isWusdtToUsdtActive = false - updateWarningState() + BigDecimal.ZERO } - } - } - - private fun fetchStatusFromApi(): Boolean { - return try { - val response = URL(BRIDGE_STATUS_API).readText() - val json = JSONObject(response) - json.optBoolean("wusdtToUsdtActive", false) - } catch (e: Exception) { - false + updateWarningState() } } @@ -237,14 +225,20 @@ class BridgeViewModel( when (dir) { BridgeDirection.WUSDT_TO_USDT -> { - _showWarning.postValue(true) - if (!isWusdtToUsdtActive) { + val requested = BigDecimal.valueOf(currentAmount) + val exceedsReserve = currentAmount > 0 && requested > polkadotUsdtReserve + _showWarning.postValue(exceedsReserve) + if (exceedsReserve) { _warningBlocked.postValue(true) - _warningText.postValue(resourceManager.getString(R.string.bridge_wusdt_to_usdt_blocked)) + _warningText.postValue( + resourceManager.getString( + R.string.bridge_wusdt_to_usdt_blocked, + polkadotUsdtReserve.setScale(2, RoundingMode.DOWN).stripTrailingZeros().toPlainString() + ) + ) } else { _warningBlocked.postValue(false) _warningText.postValue("") - _showWarning.postValue(false) } } else -> { @@ -274,11 +268,12 @@ class BridgeViewModel( private fun updateButtonState() { val dir = _direction.value ?: return + val requested = BigDecimal.valueOf(currentAmount) _buttonState.value = when { currentAmount <= 0 -> ButtonState.DISABLED currentAmount < MIN_USDT -> ButtonState.DISABLED - BigDecimal.valueOf(currentAmount) > availableBalance -> ButtonState.DISABLED - dir == BridgeDirection.WUSDT_TO_USDT && !isWusdtToUsdtActive -> ButtonState.DISABLED + requested > availableBalance -> ButtonState.DISABLED + dir == BridgeDirection.WUSDT_TO_USDT && requested > polkadotUsdtReserve -> ButtonState.DISABLED else -> ButtonState.NORMAL } } @@ -292,7 +287,7 @@ class BridgeViewModel( } fun refreshBridgeStatus() { - fetchBridgeStatus() + fetchReserveStatus() refreshSignerState() }